commit 62e3f3e391026eacf3fd82ce32076dc04d475bd3 Author: 孙小云 Date: Fri Dec 5 14:14:47 2025 +0800 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..eef220e --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +docker/volumes \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000..a104248 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,37 @@ +--- +name: "[ BUG ] " +about: 关于wvp的bug,与zlm有关的建议直接在zlm的issue中提问 +title: 'BUG' +labels: 'wvp的bug' +assignees: '' + +--- + +**环境信息:** + + - 1. 部署方式 wvp-pro docker / zlm(docker) + 编译wvp-pro/ wvp-prp + zlm都是编译部署/ + - 2. 部署环境 windows / ubuntu/ centos ... + - 3. 端口开放情况 + - 4. 是否是公网部署 + - 5. 是否使用https + - 6. 接入设备/平台品牌 + - 7. 你做过哪些尝试 + - 8. 代码更新时间 + - 9. 是否是4G设备接入 + +**描述错误** +描述下您遇到的问题 + +**如何复现** +有明确复现步骤的问题会很容易被解决 + +**截图** + +**抓包文件** + +**日志** +``` +日志内容放这里, 文件的话请直接上传 +``` + + diff --git a/.github/ISSUE_TEMPLATE/new.md b/.github/ISSUE_TEMPLATE/new.md new file mode 100644 index 0000000..7961421 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new.md @@ -0,0 +1,13 @@ +--- +name: "[ 新功能 ]" +about: 新功能 +title: '希望wVP实现的新功能,此功能应与你的具体业务无关' +labels: '' +assignees: '' + +--- + +**项目的详细需求** + +**这样的实现什么作用** + diff --git a/.github/ISSUE_TEMPLATE/solve.md b/.github/ISSUE_TEMPLATE/solve.md new file mode 100644 index 0000000..473dbd1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/solve.md @@ -0,0 +1,31 @@ +--- +name: "[ 技术咨询 ] " +about: 对于使用中遇到问题 +title: '技术咨询' +labels: '技术咨询' +assignees: '' + +--- + +**环境信息:** + + - 1. 部署方式 wvp-pro docker / zlm(docker) + 编译wvp-pro/ wvp-prp + zlm都是编译部署/ + - 2. 部署环境 windows / ubuntu/ centos ... + - 3. 端口开放情况 + - 4. 是否是公网部署 + - 5. 是否使用https + - 6. 方便的话提供下使用的设备品牌或平台 + - 7. 你做过哪些尝试 + - 8. 代码更新时间(旧版本请更新最新版本代码测试) + + +**内容描述:** + +**截图** + +**抓包文件** + +**日志** +``` +日志内容放这里, 文件的话请直接上传 +``` diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5e41eeb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,75 @@ +name: release-ubuntu + +on: + push: + tags: + - "v*.*.*" # 触发条件是推送标签 如git tag v2.7.4 git push origin v2.7.4 + +jobs: + build-ubuntu: + runs-on: ubuntu-latest + strategy: + matrix: + arch: [amd64] + max-parallel: 1 # 最大并行数 + steps: + - name: Checkout + uses: actions/checkout@v4 # github action运行环境 + + - name: Create release # 创建文件夹 + run: | + rm -rf release + mkdir release + echo ${{ github.sha }} > Release.txt + cp Release.txt LICENSE release/ + cat Release.txt + + - name: Set up JDK 1.8 + uses: actions/setup-java@v4 + with: + # Eclipse基金会维护的开源Java发行版 因为github action参考java的用这个 所以用这个 + # 还有microsoft(微软维护的openjdk发行版) oracle(商用SDK)等 + distribution: 'temurin' + java-version: '8' + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' # Node.js版本 20系列的最新稳定版 + + - name: Compile backend + run: | + mvn package + mvn package -P war + + - name: Compile frontend + run: | + cd ./web + npm install + npm run build:prod + cd ../ + + - name: Package Files + run: | + cp -r ./src/main/resources/static release/ # 复制前端文件 + cp ./target/*.jar release/ # 复制 JAR 文件 + cp ./src/main/resources/application-dev.yml release/application.yml + + BRANCH=${{ github.event.base_ref }} + BRANCH_NAME=$(echo "$BRANCH" | grep -oP 'refs/heads/\K.*') + echo "BRANCH_NAME= ${BRANCH_NAME}" + # 如果无法获取,使用默认分支 + if [[ -z "BRANCH_NAME" ]]; then + BRANCH_NAME="${{ github.event.repository.default_branch }}" + fi + + TAG_NAME="${GITHUB_REF#refs/tags/}" + ZIP_FILE_NAME="${BRANCH_NAME}-${TAG_NAME}.zip" + zip -r "$ZIP_FILE_NAME" release + echo "ZIP_FILE_NAME=$ZIP_FILE_NAME" >> $GITHUB_ENV + + - name: Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: ${{ env.ZIP_FILE_NAME }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c937a5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Compiled class file +*.class + +# Log file +*.log +logs/* +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ +src/main/resources/application-*.yml +# Package Files # +#*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar +*.iml +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +/.idea/* +/target/* +/.idea/ +/target/ + +/src/main/resources/static/ +certificates +/.vs +/docker/volumes diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..94f5d68 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "be.teletask.onvif-java"] + path = be.teletask.onvif-java + url = https://gitee.com/pan648540858/be.teletask.onvif-java.git diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..57cc8e5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 swwhaha + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f985bfa --- /dev/null +++ b/README.md @@ -0,0 +1,183 @@ +![logo](doc/_media/logo.png) +# 开箱即用的国标28181和部标808+1078协议视频平台 + +[![Build Status](https://travis-ci.org/xia-chu/ZLMediaKit.svg?branch=master)](https://travis-ci.org/xia-chu/ZLMediaKit) +[![license](http://img.shields.io/badge/license-MIT-green.svg)](https://github.com/xia-chu/ZLMediaKit/blob/master/LICENSE) +[![JAVA](https://img.shields.io/badge/language-java-red.svg)](https://en.cppreference.com/) +[![platform](https://img.shields.io/badge/platform-linux%20|%20macos%20|%20windows-blue.svg)](https://github.com/xia-chu/ZLMediaKit) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-yellow.svg)](https://github.com/xia-chu/ZLMediaKit/pulls) + + +WEB VIDEO PLATFORM是一个基于GB28181-2016、部标808、部标1078标准实现的开箱即用的网络视频平台,负责实现核心信令与设备管理后台部分,支持NAT穿透,支持海康、大华、宇视等品牌的IPC、NVR接入。支持国标级联,支持将不带国标功能的摄像机/直播流/直播推流转发到其他国标平台。 + +流媒体服务基于@夏楚 ZLMediaKit [https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit) +播放器使用@dexter jessibuca [https://github.com/langhuihui/jessibuca/tree/v3](https://github.com/langhuihui/jessibuca/tree/v3) +播放器使用@Numberwolf-Yanlong h265web.js [https://github.com/numberwolf/h265web.js](https://github.com/numberwolf/h265web.js) +前端页面基于vue-admin-template构建 [https://github.com/PanJiaChen/vue-admin-template?tab=readme-ov-file](https://github.com/PanJiaChen/vue-admin-template?tab=readme-ov-file) + +# 应用场景: +- 支持浏览器无插件播放摄像头视频。 +- 支持国标设备(摄像机、平台、NVR等)设备接入 +- 支持rtsp, rtmp,直播设备设备接入,充分利旧。 +- 支持国标级联。多平台级联。跨网视频预览。 +- 支持跨网网闸平台互联。 + + +# 文档 +wvp使用文档 [https://doc.wvp-pro.cn](https://doc.wvp-pro.cn) +ZLM使用文档 [https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit) + +# gitee仓库 +https://gitee.com/pan648540858/wvp-GB28181-pro.git + +# 截图 + + + + + + + + + + + + + + + + + +
登录页面
首页
分屏播放
国标设备列表
行政区划管理
业务分组管理
录制计划
平台信息
+ +# 功能特性 +- [X] 集成web界面 +- [X] 兼容性良好 +- [X] 跨平台服务,一次编译多端部署, 可以同时用于x86和arm架构 +- [X] 接入设备 + - [X] 视频预览 + - [X] 支持主码流子码流切换 + - [X] 无限制接入路数,能接入多少设备只取决于你的服务器性能 + - [X] 云台控制,控制设备转向,拉近,拉远 + - [X] 预置位查询,使用与设置 + - [X] 查询NVR/IPC上的录像与播放,支持指定时间播放与下载 + - [X] 无人观看自动断流,节省流量 + - [X] 视频设备信息同步 + - [X] 离在线监控 + - [X] 支持直接输出RTSP、RTMP、HTTP-FLV、Websocket-FLV、HLS多种协议流地址 + - [X] 支持通过一个流地址直接观看摄像头,无需登录以及调用任何接口 + - [X] 支持UDP和TCP两种国标信令传输模式 + - [X] 支持UDP和TCP两种国标流传输模式 + - [X] 支持检索,通道筛选 + - [X] 支持通道子目录查询 + - [X] 支持过滤音频,防止杂音影响观看 + - [X] 支持国标网络校时 + - [X] 支持播放H264和H265 + - [X] 报警信息处理,支持向前端推送报警信息 + - [X] 语音对讲 + - [X] 支持业务分组和行政区划树自定义展示以及级联推送 + - [X] 支持订阅与通知方法 + - [X] 移动位置订阅 + - [X] 移动位置通知处理 + - [X] 报警事件订阅 + - [X] 报警事件通知处理 + - [X] 设备目录订阅 + - [X] 设备目录通知处理 + - [X] 移动位置查询和显示 + - [X] 支持手动添加设备和给设备设置单独的密码 +- [X] 支持平台对接接入 +- [X] 支持国标级联 + - [X] 国标通道向上级联 + - [X] WEB添加上级平台 + - [X] 注册 + - [X] 心跳保活 + - [X] 通道选择 + - [X] 支持通道编号自定义, 支持每个平台使用不同的通道编号 + - [X] 通道推送 + - [X] 点播 + - [X] 云台控制 + - [X] 平台状态查询 + - [X] 平台信息查询 + - [X] 平台远程启动 + - [X] 每个级联平台可自定义的虚拟目录 + - [X] 目录订阅与通知 + - [X] 录像查看与播放 + - [X] GPS订阅与通知(直播推流) + - [X] 语音对讲 + - [X] 支持同时级联到多个上级平台 +- [X] 支持自动配置ZLM媒体服务, 减少因配置问题所出现的问题; +- [X] 支持流媒体节点集群,负载均衡。 +- [X] 支持启用udp多端口模式, 提高udp模式下媒体传输性能; +- [X] 支持公网部署; +- [X] 支持wvp与zlm分开部署,提升平台并发能力 +- [X] 支持拉流RTSP/RTMP,分发为各种流格式,或者推送到其他国标平台 +- [X] 支持推流RTSP/RTMP,分发为各种流格式,或者推送到其他国标平台 +- [X] 支持推流鉴权 +- [X] 支持接口鉴权 +- [X] 云端录像,推流/代理/国标视频均可以录制在云端服务器,支持预览和下载 +- [X] 支持打包可执行jar和war +- [X] 支持跨域请求,支持前后端分离部署 +- [X] 支持Mysql,Postgresql,金仓等数据库 +- [X] 支持录制计划, 根据设定的时间对通道进行录制. 暂不支持将录制的内容转发到国标上级 +- [X] 支持国标信令集群 +- [X] 新增支持部标808和部标1078,大量新特性不一一列表了。支持作为网关被国标上级调用部标设备 +- [X] 支持电子地图。支持展示通道位置,支持在地图上修改通道位置。支持了数据分层抽稀数据能力,百万级数据也可以轻松展示。提供标准的矢量瓦片图层,常见地图引擎都可以直接展示。 +- [X] 借用zlm闭源版本新能力,可以支持录像保存至s3存储,支持minio。 + +# 闭源内容 +- [X] 国标增强版: 支持国标28181-2022协议,支持巡航轨迹查询,PTZ精准控制,存储卡格式化,设备软件升级,OSD配置,h265+aac,支持辅码流,录像倒放等。 +- [X] 全功能版: + - [X] 支持开源所有功能 + - [X] ONVIF协议 + - 设备检索 + - 实时图像预览 + - 录像回放、回放倍速控制 + - 云台控制、预置位控制、云台绝对定位、看守位 + - 聚焦控制 + - 设备重启 + - 设备时间设置以及跟系统时间的差值比较 + - 恢复出厂设置 + - 自动获取设备品牌等信息、支持展示DNS信息、支持协议的展示 + - 国标级联点播、自动点播等。 + - [X] 国网B接口协议 + - 设备注册 + - 资源获取 + - 预览 + - 云台控制 + - 预置位控制等, + - 可免费定制支持语音对讲、录像回放和抓拍图像。 + - [X] 支持按权限分配可以使用的通道 + - [X] 支持表格导出 + - [X] 拉流代理支持按照品牌拼接url。 + - [X] 播放鉴权,更加安全。 + + +# 授权协议 +本项目自有代码使用宽松的MIT协议,在保留版权信息的情况下可以自由应用于各自商用、非商业的项目。 但是本项目也零碎的使用了一些其他的开源代码,在商用的情况下请自行替代或剔除; 由于使用本项目而产生的商业纠纷或侵权行为一概与本项目及开发者无关,请自行承担法律风险。 在使用本项目代码时,也应该在授权协议中同时表明本项目依赖的第三方库的协议 + +# 技术支持 + +# 付费社群 + + +> 加入三天内不满意可以直接自行推出,星球会直接退款给大家。需要发票可以在星球app中直接咨询星球客服获取。 + +> 星球还提供了包括闭源的全功能试用包, 会随时更新。 + +> 付费社群即可以对作者提供支持,也可以为大家更加快速的解决问题。如果暂时无法加入,给项目点个星也是极大的鼓励。 + + +[知识星球](https://t.zsxq.com/0d8VAD3Dm)专栏列表:, +- [使用入门系列一:WVP-PRO能做什么](https://t.zsxq.com/0dLguVoSp) + +有偿技术支持,一对一开发辅导,闭源内容合作请发送邮件到648540858@qq.com咨询 + +# 致谢 +感谢作者[夏楚](https://github.com/xia-chu) 提供这么棒的开源流媒体服务框架,并在开发过程中给予支持与帮助。 +感谢作者[dexter langhuihui](https://github.com/langhuihui)和[Numberwolf-Yanlong](https://github.com/numberwolf/h265web.js) 开源这么好用的WEB播放器。 +感谢各位大佬的赞助以及对项目的指正与帮助。包括但不限于代码贡献、问题反馈、资金捐赠等各种方式的支持!以下排名不分先后: +[lawrencehj](https://github.com/lawrencehj) [Smallwhitepig](https://github.com/Smallwhitepig) [swwhaha](https://github.com/swwheihei) +[hotcoffie](https://github.com/hotcoffie) [xiaomu](https://github.com/nikmu) [TristingChen](https://github.com/TristingChen) +[chenparty](https://github.com/chenparty) [Hotleave](https://github.com/hotleave) [ydwxb](https://github.com/ydwxb) +[ydpd](https://github.com/ydpd) [szy833](https://github.com/szy833) [ydwxb](https://github.com/ydwxb) [Albertzhu666](https://github.com/Albertzhu666) +[mk1990](https://github.com/mk1990) [SaltFish001](https://github.com/SaltFish001) diff --git a/bin/wvp.sh b/bin/wvp.sh new file mode 100644 index 0000000..d4d1c85 --- /dev/null +++ b/bin/wvp.sh @@ -0,0 +1,115 @@ +#!/bin/bash + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +function log() { + message="[Polaris Log]: $1 " + case "$1" in + *"Fail"* | *"Error"* | *"请使用 root 或 sudo 权限运行此脚本"*) + echo -e "${RED}${message}${NC}" 2>&1 | tee -a + ;; + *"Success"*) + echo -e "${GREEN}${message}${NC}" 2>&1 | tee -a + ;; + *"Ignore"* | *"Jump"*) + echo -e "${YELLOW}${message}${NC}" 2>&1 | tee -a + ;; + *) + echo -e "${BLUE}${message}${NC}" 2>&1 | tee -a + ;; + esac +} +echo +cat </dev/null | head -n 1` +PID_FILE=pid.file +RUNNING=N +PWD=`pwd` + +######### DO NOT MODIFY ######## + +if [ -f $PID_FILE ]; then + PID=`cat $PID_FILE` + if [ ! -z "$PID" ] && kill -0 $PID 2>/dev/null; then + RUNNING=Y + fi +fi + +start() +{ + if [ $RUNNING == "Y" ]; then + echo "Application already started" + else + if [ -z "$JARFILE" ] + then + echo "ERROR: jar file not found" + else + nohup java $JAVA_OPT -Djava.security.egd=file:/dev/./urandom -jar $PWD/$JARFILE > nohup.out 2>&1 & + echo $! > $PID_FILE + echo "Application $JARFILE starting..." + tail -f nohup.out + fi + fi +} + +stop() +{ + if [ $RUNNING == "Y" ]; then + kill -9 $PID + rm -f $PID_FILE + echo "Application stopped" + else + echo "Application not running" + fi +} + +restart() +{ + stop + start +} + +case "$1" in + + 'start') + start + ;; + + 'stop') + stop + ;; + + 'restart') + restart + ;; + + *) + echo "Usage: $0 { start | stop | restart }" + exit 1 + ;; +esac +exit 0 + diff --git a/buildPackage.sh b/buildPackage.sh new file mode 100755 index 0000000..913a0d8 --- /dev/null +++ b/buildPackage.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# 获取当前日期并格式化为 YYYY-MM-DD 的形式 +current_date=$(date +"%Y-%m-%d") + +mkdir -p "$current_date"/数据库 + +cp -r ./数据库/2.7.3 "$current_date"/数据库 + +cp src/main/resources/配置详情.yml "$current_date" +cp src/main/resources/application-dev.yml "$current_date"/application.yml + +cp ./target/wvp-pro-*.jar "$current_date" + +zip -r "$current_date".zip "$current_date" + +rm -rf "$current_date" + +exit 0 + diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..c5c4006 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,112 @@ +# 介绍 + +> 开箱即用的28181协议视频平台 + +# 概述 + +- WVP-PRO基于GB/T + 28181-2016标准实现的流媒体平台,依托优秀的开源流媒体服务[ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit) + ,提供完善丰富的功能。 +- GB/T 28181-2016 中文标准名称是《公共安全视频监控联网系统信息传输、交换、控制技术要求》是监控领域的国家标准。大量应用于政府视频平台。 +- 通过28181协议你可以将IPC摄像头接入平台,可以观看也可以使用28181/rtsp/rtmp/flv等协议将视频流分发到其他平台。 + +# 特性 + +- 实现标准的28181信令,兼容常见的品牌设备,比如海康、大华、宇视等品牌的IPC、NVR以及平台。 +- 支持将国标设备级联到其他国标平台,也支持将不支持国标的设备的图像或者直播推送到其他国标平台 +- 前端完善,自带完整前端页面,无需二次开发可直接部署使用。 +- 完全开源,且使用MIT许可协议。保留版权的情况下可以用于商业项目。 +- 支持多流媒体节点负载均衡。 + +# 付费社群 + +[![社群](_media/shequ.png "shequ")](https://t.zsxq.com/0d8VAD3Dm) +> 收费是为了提供更好的服务,也是对作者更大的激励。加入星球的用户三天后可以私信我留下微信号,我会拉大家入群。加入三天内不满意可以直接退款,大家不需要有顾虑,来白嫖三天也不是不可以。 + +# 我们实现了哪些国标功能 + +**作为上级平台** + +- [X] 注册 +- [X] 注销 +- [X] 实时视音频点播 +- [X] 设备控制 + - [X] 云台控制 + - [X] 远程启动 + - [X] 录像控制 + - [X] 报警布防/撤防 + - [X] 报警复位 + - [X] 强制关键帧 + - [X] 拉框放大 + - [X] 拉框缩小 + - [X] 看守位控制 + - [X] 设备配置 +- [X] 报警事件通知和分发 +- [X] 设备目录订阅 +- [X] 网络设备信息查询 + - [X] 设备目录查询 + - [X] 设备状态查询 + - [X] 设备配置查询 + - [X] 设备预置位查询 +- [X] 状态信息报送 +- [X] 设备视音频文件检索 +- [X] 历史视音频的回放 + - [X] 播放 + - [X] 暂停 + - [X] 进/退 + - [X] 停止 +- [X] 视音频文件下载 +- [X] 校时 +- [X] 订阅和通知 + - [X] 事件订阅 + - [X] 移动设备位置订阅 + - [X] 报警订阅 + - [X] 目录订阅 +- [X] 语音广播 +- [X] 语音喊话 + +**作为下级平台** + +- [X] 注册 +- [X] 注销 +- [X] 实时视音频点播 +- [X] 设备控制 + - [X] 云台控制 + - [ ] 远程启动 + - [X] 录像控制 + - [X] 报警布防/撤防 + - [X] 报警复位 + - [X] 强制关键帧 + - [X] 拉框放大 + - [X] 拉框缩小 + - [X] 看守位控制 + - [ ] 设备配置 +- [ ] 报警事件通知和分发 +- [X] 设备目录订阅 +- [X] 网络设备信息查询 + - [X] 设备目录查询 + - [X] 设备状态查询 + - [ ] 设备配置查询 + - [X] 设备预置位查询 +- [X] 状态信息报送 +- [X] 设备视音频文件检索 +- [X] 历史视音频的回放 + - [X] 播放 + - [x] 暂停 + - [x] 进/退 + - [x] 停止 +- [X] 视音频文件下载 +- [ ] ~~校时~~ +- [X] 订阅和通知 + - [X] 事件订阅 + - [X] 移动设备位置订阅 + - [ ] 报警订阅 + - [X] 目录订阅 +- [X] 语音广播 +- [X] 语音喊话 + +# 社区 + +代码目前托管在GitHub和Gitee,Gitee目前作为加速仓库使用,不接受issue。 +GitHub: [https://github.com/648540858/wvp-GB28181-pro](https://github.com/648540858/wvp-GB28181-pro) +Gitee: [https://gitee.com/pan648540858/wvp-GB28181-pro](https://gitee.com/pan648540858/wvp-GB28181-pro) \ No newline at end of file diff --git a/doc/_content/ability/_media/cascade1.png b/doc/_content/ability/_media/cascade1.png new file mode 100644 index 0000000..9ba8280 Binary files /dev/null and b/doc/_content/ability/_media/cascade1.png differ diff --git a/doc/_content/ability/_media/cascade2.png b/doc/_content/ability/_media/cascade2.png new file mode 100644 index 0000000..4dd62cf Binary files /dev/null and b/doc/_content/ability/_media/cascade2.png differ diff --git a/doc/_content/ability/_media/cascade3.png b/doc/_content/ability/_media/cascade3.png new file mode 100644 index 0000000..f95f5d5 Binary files /dev/null and b/doc/_content/ability/_media/cascade3.png differ diff --git a/doc/_content/ability/_media/cascade4.png b/doc/_content/ability/_media/cascade4.png new file mode 100644 index 0000000..9db85b5 Binary files /dev/null and b/doc/_content/ability/_media/cascade4.png differ diff --git a/doc/_content/ability/_media/img.png b/doc/_content/ability/_media/img.png new file mode 100644 index 0000000..6a0c550 Binary files /dev/null and b/doc/_content/ability/_media/img.png differ diff --git a/doc/_content/ability/_media/img_1.png b/doc/_content/ability/_media/img_1.png new file mode 100644 index 0000000..31995c3 Binary files /dev/null and b/doc/_content/ability/_media/img_1.png differ diff --git a/doc/_content/ability/_media/img_10.png b/doc/_content/ability/_media/img_10.png new file mode 100644 index 0000000..030502d Binary files /dev/null and b/doc/_content/ability/_media/img_10.png differ diff --git a/doc/_content/ability/_media/img_11.png b/doc/_content/ability/_media/img_11.png new file mode 100644 index 0000000..cb0f3d5 Binary files /dev/null and b/doc/_content/ability/_media/img_11.png differ diff --git a/doc/_content/ability/_media/img_12.png b/doc/_content/ability/_media/img_12.png new file mode 100644 index 0000000..d6fe877 Binary files /dev/null and b/doc/_content/ability/_media/img_12.png differ diff --git a/doc/_content/ability/_media/img_13.png b/doc/_content/ability/_media/img_13.png new file mode 100644 index 0000000..6be1128 Binary files /dev/null and b/doc/_content/ability/_media/img_13.png differ diff --git a/doc/_content/ability/_media/img_14.png b/doc/_content/ability/_media/img_14.png new file mode 100644 index 0000000..2471204 Binary files /dev/null and b/doc/_content/ability/_media/img_14.png differ diff --git a/doc/_content/ability/_media/img_15.png b/doc/_content/ability/_media/img_15.png new file mode 100644 index 0000000..f167811 Binary files /dev/null and b/doc/_content/ability/_media/img_15.png differ diff --git a/doc/_content/ability/_media/img_16.png b/doc/_content/ability/_media/img_16.png new file mode 100644 index 0000000..3564dd0 Binary files /dev/null and b/doc/_content/ability/_media/img_16.png differ diff --git a/doc/_content/ability/_media/img_17.png b/doc/_content/ability/_media/img_17.png new file mode 100644 index 0000000..5886286 Binary files /dev/null and b/doc/_content/ability/_media/img_17.png differ diff --git a/doc/_content/ability/_media/img_18.png b/doc/_content/ability/_media/img_18.png new file mode 100644 index 0000000..3b33b21 Binary files /dev/null and b/doc/_content/ability/_media/img_18.png differ diff --git a/doc/_content/ability/_media/img_19.png b/doc/_content/ability/_media/img_19.png new file mode 100644 index 0000000..f5242af Binary files /dev/null and b/doc/_content/ability/_media/img_19.png differ diff --git a/doc/_content/ability/_media/img_2.png b/doc/_content/ability/_media/img_2.png new file mode 100644 index 0000000..f9f2f55 Binary files /dev/null and b/doc/_content/ability/_media/img_2.png differ diff --git a/doc/_content/ability/_media/img_20.png b/doc/_content/ability/_media/img_20.png new file mode 100644 index 0000000..387eb69 Binary files /dev/null and b/doc/_content/ability/_media/img_20.png differ diff --git a/doc/_content/ability/_media/img_21.png b/doc/_content/ability/_media/img_21.png new file mode 100644 index 0000000..560e2a7 Binary files /dev/null and b/doc/_content/ability/_media/img_21.png differ diff --git a/doc/_content/ability/_media/img_22.png b/doc/_content/ability/_media/img_22.png new file mode 100644 index 0000000..b48e15c Binary files /dev/null and b/doc/_content/ability/_media/img_22.png differ diff --git a/doc/_content/ability/_media/img_23.png b/doc/_content/ability/_media/img_23.png new file mode 100644 index 0000000..8e1f454 Binary files /dev/null and b/doc/_content/ability/_media/img_23.png differ diff --git a/doc/_content/ability/_media/img_24.png b/doc/_content/ability/_media/img_24.png new file mode 100644 index 0000000..e522481 Binary files /dev/null and b/doc/_content/ability/_media/img_24.png differ diff --git a/doc/_content/ability/_media/img_25.png b/doc/_content/ability/_media/img_25.png new file mode 100644 index 0000000..35900bd Binary files /dev/null and b/doc/_content/ability/_media/img_25.png differ diff --git a/doc/_content/ability/_media/img_3.png b/doc/_content/ability/_media/img_3.png new file mode 100644 index 0000000..efe688c Binary files /dev/null and b/doc/_content/ability/_media/img_3.png differ diff --git a/doc/_content/ability/_media/img_4.png b/doc/_content/ability/_media/img_4.png new file mode 100644 index 0000000..f548cec Binary files /dev/null and b/doc/_content/ability/_media/img_4.png differ diff --git a/doc/_content/ability/_media/img_5.png b/doc/_content/ability/_media/img_5.png new file mode 100644 index 0000000..6959c78 Binary files /dev/null and b/doc/_content/ability/_media/img_5.png differ diff --git a/doc/_content/ability/_media/img_6.png b/doc/_content/ability/_media/img_6.png new file mode 100644 index 0000000..04c42bc Binary files /dev/null and b/doc/_content/ability/_media/img_6.png differ diff --git a/doc/_content/ability/_media/img_7.png b/doc/_content/ability/_media/img_7.png new file mode 100644 index 0000000..1a8edbf Binary files /dev/null and b/doc/_content/ability/_media/img_7.png differ diff --git a/doc/_content/ability/_media/img_8.png b/doc/_content/ability/_media/img_8.png new file mode 100644 index 0000000..02fa66f Binary files /dev/null and b/doc/_content/ability/_media/img_8.png differ diff --git a/doc/_content/ability/_media/img_9.png b/doc/_content/ability/_media/img_9.png new file mode 100644 index 0000000..708e901 Binary files /dev/null and b/doc/_content/ability/_media/img_9.png differ diff --git a/doc/_content/ability/auto_play.md b/doc/_content/ability/auto_play.md new file mode 100644 index 0000000..2ce3012 --- /dev/null +++ b/doc/_content/ability/auto_play.md @@ -0,0 +1,3 @@ + + +# 自动点播 diff --git a/doc/_content/ability/cascade.md b/doc/_content/ability/cascade.md new file mode 100644 index 0000000..0766a7a --- /dev/null +++ b/doc/_content/ability/cascade.md @@ -0,0 +1,56 @@ + + +# 国标级联的使用 + +国标28181不同平台之间支持两种连接方式,平级和上下级,WVP目前支持向上级级联。 + +## 1 接入平台 + +### 1.1 wvp-pro + +#### 1.1.1 wvp-pro管理页面点击添加 + +![cascade1](_media/cascade1.png) + +#### 1.1.2 填入wvp-pro上级平台信息 + +![cascade1](_media/img_4.png) +![cascade1](_media/img_5.png) + +#### 1.1.3 编辑wvp-pro上级设备信息,开启订阅 + +![cascade1](_media/img_6.png) + +### 1.2 大华平台 + +### 1.3 海康平台 + +### 1.4 liveGBS + +#### 1.4.1. wvp-pro管理页面点击添加 + +![添加](_media/cascade1.png) + +#### 1.4.2. 填入liveGBS平台信息 + +![填入liveGBS平台信息1](_media/cascade2.png) +![填入liveGBS平台信息2](_media/cascade3.png) + +#### 1.4.3. 编辑liveGBS设备信息,开启目录订阅 + +![cascade1](_media/cascade4.png) + +#### 1.4.4. 编辑liveGBS设备信息,开启GPS订阅 + +![cascade1](_media/img_7.png) + +## 2 添加目录与通道 + +1. 级联平台添加目录信息 + ![cascade1](_media/img_1.png) +2. 为目录添加通道 + ![cascade1](_media/img_2.png) +3. 设置默认流目录 + 如果需要后续自动生成的流信息都在某一个节点下,可以在对应节点右键设置为默认 + ![cascade1](_media/img_3.png) + diff --git a/doc/_content/ability/cascade2.md b/doc/_content/ability/cascade2.md new file mode 100644 index 0000000..00f4fcb --- /dev/null +++ b/doc/_content/ability/cascade2.md @@ -0,0 +1,71 @@ + + +# 国标级联的使用 + +国标28181不同平台之间支持两种连接方式,平级和上下级,WVP目前支持向上级级联。 + +## 添加上级平台 + +在国标级联页面点击“添加”按钮,以推送到上级WVP为例子,参看[接入设备](./_content/ability/device.md) +![cascade17](_media/img_17.png) + +1. 名称 + 上级平台看到的下级平台名称; +2. 本地IP + 本地连接上级使用的具体哪个网卡; +3. SIP认证用户名 + 可以设置为与"设备国标编号"一致; +4. 注册周期 + 间隔多久发起一次注册,单位秒; +5. 心跳周期 + 间隔多久发送一次心跳,一般上级平台三次收不到心跳就会认为下级离线了, 所以建议{心跳周期}x3 < 注册周期; +6. SDP发流IP + 调用媒体节点发送视频流给上级时,使用的本地IP; +7. 信令传输 + 信令传输模式,支持udp和TCP,没有特殊需求,默认UDP即可; +8. 目录分组 + 上级发送"CATALOG"消息查询通道信息,每一条消息中携带几条通道信息,默认为1,增大该值,可以加快通道发送速度; +9. 字符集 + 发送给上级"MESSAGE"消息中的消息体使用的编码格式,国标28181-2016默认为GB2312; +10. 行政区划 + 如果勾选"其他选项/推送平台信息"选项,会给上级推送平台信息,这里就是平台的行政区划信息 +11. 平台厂商 + 如果勾选"其他选项/推送平台信息"选项,会给上级推送平台信息,这里就是平台的平台厂商信息 +12. 平台型号 + 如果勾选"其他选项/推送平台信息"选项,会给上级推送平台信息,这里就是平台的平台型号信息 +13. 平台安装地址 + 如果勾选"其他选项/推送平台信息"选项,会给上级推送平台信息,这里就是平台的平台安装地址信息 +14. 其他选项 + - RTCP保活 + 在上级的流传输模式为UDP时,因为UDP的无状态特性,会无法知道上级是否在正常收流,启用RTCP保活时,就可以主动发送RTCP消息确认上级是否在正常收流, + 异常情况下,可以下级主动停止发流; + - 消息通道 + 支持通过报警消息给上级级WVP推送消息,消息内容由redis消息发送给wvp,wvp编辑成报警消息发送给上级; + - 主动推送通道 + WVP模拟一条目录订阅信息,然后在共享通道变化时,发送CATAOLOG事件给上级,通知具体的通道变化, + 目前支持的状态有: 状态改变事件 ON:上线,OFF:离线,VLOST:视频丢失,DEFECT:故障,ADD:增加,DEL:删除,UPDATE:更新; + - 推送平台信息 + 勾选此项,上级收到的通道信息中会多出一个平台信息的通道.内容在平台的编辑中修改; + - 推送分组信息 + 勾选此项,如果你共享的通道分配了具体的业务分组以及虚拟组织,那么上级收到的通道中会包括业务分组以及虚拟组织节点信息; + - 推送行政区划 + 勾选此项,如果你共享的通道分配了具体的行政区划,那么上级收到的通道中会包括行政区划信息; + +国标级联列表出现了级联的这个平台;同时状态显示为在线,如果状态为离线那么可能是你的服务信息配置有误或者网络不通。 +订阅信息列有三个图标,表示上级开启订阅,从左到右依次是:报警订阅,目录订阅,移动位置订阅。 + +## 通道共享 + +点击你要推送的平台的“通道共享”按钮。 +![cascade18](_media/img_18.png) + +1. 添加状态选择"未共享"可以将具体的通道共享给上级; +2. 添加状态选择"已共享"可以看到已经共享的通道,并且支持为这个通道在这个平台设备专门的名称和编号; +3. 点击"按设备添加"可以将某个国标设备下的所有通道共享给上级; +4. 点击"按设备移除"可以将某个国标设备下的所有通道取消共享给上级; +5. 点击"全部添加"可以将所有通道共享给上级; +6. 点击"全部移除"可以将所有通道共享给上级; + +## 推送通道 + +WVP会将所有通道信息按照目录订阅消息通知形式,发送ADD事件给上级. \ No newline at end of file diff --git a/doc/_content/ability/channel.md b/doc/_content/ability/channel.md new file mode 100644 index 0000000..36dda2b --- /dev/null +++ b/doc/_content/ability/channel.md @@ -0,0 +1,25 @@ +# 通道管理 + +通道管理为了对已经分配国标编号的通道进行统一的行政区划和业务分组管理,国标中对于组织结构有两种表示方式,一种是按照行政区划,一种是业务分组+虚拟组织的方式. +行政区划结构固定,比如: 北京/市辖区/昌平区, 通道可以挂载道何一级行政区划下. 业务分组比较灵活, 可以按照自己的随意取名, +但是通道只能放在业务分组下的虚拟组织里,不能放在业务分组下. + +## 行政区划 + +左侧树结构为行政区划结构, 通过数据鼠标右键可以操作,包括: 刷新节点,新建节点,编辑节点,删除节点,添加设备( +可以将某个国标设备下的通道全部添加道某一个节点下),移除设备(可以将某个国标设备下的通道全部从这个节点移除) +右侧伪通道列表, 对于非国标接入的设备只有配置了国标编号后才可以在这里进行操作。 +选择左侧的节点后,可以点击右侧的“添加通道”, 选择需要的通道添加到改节点下,如果找不到通道, 可以选择“异常挂载通道”,点击清理后重新回来选择。 +![行政区划](_media/img_21.png) + +## 业务分组 + +左侧树结构为业务分组结构, 通过数据鼠标右键可以操作,包括: 刷新节点,新建节点,编辑节点,删除节点,添加设备( +可以将某个国标设备下的通道全部添加道某一个节点下),移除设备(可以将某个国标设备下的通道全部从这个节点移除) +业务分组下不能挂载设备,所以没有选择该节点的单选框. +右侧为通道列表, 对于非国标接入的设备只有配置了国标编号后才可以在这里进行操作。 + +选择左侧的节点后,可以点击右侧的“添加通道”, 选择需要的通道添加到改节点下。 +如果找不到通道, 可以选择“异常挂载通道”,点击清理后重新回来选择。 +注意,根资源组下的那一级为业务分组类型不可以直接挂载设备,需要继续建立节点,后续的节点的都是虚拟组织类型, 就可以挂载通道了。 +![业务分组](_media/img_22.png) diff --git a/doc/_content/ability/cloud_record.md b/doc/_content/ability/cloud_record.md new file mode 100644 index 0000000..14beff7 --- /dev/null +++ b/doc/_content/ability/cloud_record.md @@ -0,0 +1,13 @@ + + +# 云端录像 + +![云端录像](_media/img_26.png) +云端录像是对录制在zlm服务下的录像文件的管理,录像的文件路径默认在ZLM/www/record下。 + +- 国标设备是否录像: 可以再WVP的配置中user-settings.record-sip设置为true那么每次点播以及录像回放都会录像; +- 推流设备是否录像: 可以再WVP的配置中user-settings.record-push-live设置为true; +- 拉流代理的是否录像: 在添加和编辑拉流代理时可以指定, 每次点播都会进行录像 +- 录像文件存储路径配置: 可以修改media.record-path来修改录像路径,但是如果有旧的录像文件,请不要迁移,因为数据库记录了每一个录像的绝对路径,一旦修改会造成找到文件,无法定时移除以及播放 +- 录像保存时间: 可以修改media.record-day来修改录像保存时间,单位是天; + diff --git a/doc/_content/ability/continuous_broadcast.md b/doc/_content/ability/continuous_broadcast.md new file mode 100644 index 0000000..06708e7 --- /dev/null +++ b/doc/_content/ability/continuous_broadcast.md @@ -0,0 +1,104 @@ +# 语音对讲 + +## 流程和原理 + +语音对讲在国标28181-2016中分为broadcast(广播)和talk(对讲)两种模式,broadcast模式是从服务端把音频传送到设备端,是单向的, +需要结合点播视频来实现双向对讲,talk模式支持双向,不过wvp只处理了和broadcast一样的把音频传递设备,这样两种模式可以使用一样的逻辑处理即可。 +不同的设备对于两种模式的支持不同且通常差异很大,不同的设备对同一个设备的支持也有一些不同,所以语音对讲中的兼容和适配也是问题最多的。talk模式因为在国标28181-2022中已经移除,所以这里不再讨论它了。 + +### 1. broadcast模式流程 + +```plantuml +@startuml +"WVP-PRO" -> "设备": 语音广播通知 +"WVP-PRO" <-- "设备": 200OK +"WVP-PRO" <- "设备": 语音广播应答 +"WVP-PRO" --> "设备": 200OK +"WVP-PRO" <- "设备": Invite +"WVP-PRO" --> "设备": 200OK(携带SDP消息体) +"WVP-PRO" <-- "设备": ACK +"ZLMediaKit" -> "设备": 向设备发送语音流 +@enduml +``` + +与点播的流程不同的是,这里的invite消息是由设备发送给wvp的,wvp按照invite协商的方式给设备推送语音流,所有对讲的使用那种方式(UDP/TCP被动/TCP主动)传输语音流由设备决定 + +## 使用条件与限制 + +因为invite消息是由设备发送给wvp的,这决定了发送语音流的方式,这也就决定了有的设备不能用于公网对讲,比如大部分的海康设备只支持udp方式收流( +目前新版的海康设备已经在着手解决这个问题),那么wvp发流时只能按照sdp中指定的ip端口发流,所以如果wvp在公网,设备在内网中,那么wvp无法连接设备提供的IP,发流也就失败了。 +与海康不同的,大华以及很多执法记录仪厂商是支持tcp主动方式取流的,这样是可以实现公网对讲的。 + +## 使用ffmpeg快速测试 + +由于浏览器对于音频的采集需要网页支持https才可以,所以如果想要实现网页音频对讲,那么你必须给wvp和zlm配置证书以使用https。 +测试阶段如果只是想测试功能可以用ffmpeg来模拟语音流,推送到wvp后可以实现把音频文件推送到摄像头。 +测试命令格式如下: + +```shell +ffmpeg -re -i {音频文件} -acodec pcm_alaw -ar 8000 -ac 1 -f rtsp 'rtsp://{zlm的IP}:{zlm的RTSP端口}/broadcast/{设备国标编号}_{通道国标编号}?sign={md5(pushKey)}' +``` + +例如 + +```shell +ffmpeg -re -i test.mp3 -acodec pcm_alaw -ar 8000 -ac 1 -f rtsp 'rtsp://192.168.1.3:22554/broadcast/34020000001320000001_34020000001320000001?sign=41db35390ddad33f83944f44b8b75ded' +``` + +测试流程如下: + +```plantuml +@startuml +"FFMPEG" -> "ZLMediaKit": 推流到zlm +"WVP-PRO" <- "ZLMediaKit": 通知收到语音对讲推流,携带设备和通道信息 +"WVP-PRO" -> "设备": 开始语音对讲 +"WVP-PRO" <-- "设备": 语音对讲建立成功,携带收流端口 +"WVP-PRO" -> "ZLMediaKit": 通知zlm将流推送到设备收流端口 +"ZLMediaKit" -> "设备": 向设备推流 +@enduml +``` + +如果听到设备播放你推送的音频,那么意味着调用成功,此过程推流即可需要调用任何接口 + +## 生产环境网页发起语音对讲 + +生产环境下使用语音对讲,如果是自己的客户端设备那么直接上面的ffmpeg测试方式,按照固定格式推流到zlm即可。 +对于WEB程序,主要是局域网和公网的区别,两个原因: + +1. 很多设备不支持公网对讲 +2. 公网和局域网获取证书实现https支持的方式不同 + +### 公网使用 + +公网你可以直接使用证书厂商或者云服务器厂商提供的证书,这是很方便的。 + +### 局域网使用 + +局域网你需要为wvp和zlm生成自签名证书,这里我推荐一种生成自签名证书相对方便的方式, +此方式为linux下的一种方式。 +下载证书生成工具: +[https://github.com/FiloSottile/mkcert/releases/tag/v1.4.4](https://github.com/FiloSottile/mkcert/releases/tag/v1.4.4) +安装此工具, 进入解压的工具目录,执行 + +```shell +./mkcert-v1.4.4-linux-amd64 -install +``` + +生成pem证书 + +```shell +./mkcert-v1.4.4-linux-amd64 局域网IP 局域网IP2 局域网IP3 +``` + +你会得到两文件*-key.pem和*.pem, 此文件配置到wvp后既可实现证书的加载 +生成zlm使用的证书 + +```shell +cat *.pem *-key.pem> ./zlm.pem +``` + +得到的文件就是可以给zlm使用的证书 +zlm下使用证书有两种方式: + +1. 替换zlm下的default.pem, 即删除此文件并把zlm.pem重命名为default.pem +2. 在启动zlm的使用添加 `-s zlm.pem` \ No newline at end of file diff --git a/doc/_content/ability/continuous_recording.md b/doc/_content/ability/continuous_recording.md new file mode 100644 index 0000000..78c1ae5 --- /dev/null +++ b/doc/_content/ability/continuous_recording.md @@ -0,0 +1,16 @@ + + +# 7*24不间断录像 + +目前如果要实现不间断录像如果只是关闭无人观看停止推流是不够的,设备可能经历断网,重启,都会导致录像的中断,目前给大家提供一种可用的临时方案。 + +**原理:** wvp支持使用流地址自动点播,即你拿到一个流地址直接去播放,即使设备处于未点播状态,wvp会自动帮你点播;ZLM +的拉流代理成功后会无限重试,只要流一恢复就可以拉起来,基于这两个原理。 +**方案如下:** + +1. wvp的配置中user-settings->auto-apply-play设置为团true,开启自动点播; +2. 点击你要录像的通道,点击播放页面左下角的“更多地址”,点击rtsp,此时复制了rtsp地址到剪贴板; +3. 在拉流代理中添加一路流,地址填写你复制的地址,启用成功即可。 + **前提:** +1. wvp使用多端口收流,不然你无法得到一个固定的流地址,也就无法实现自动点播。 + diff --git a/doc/_content/ability/device.md b/doc/_content/ability/device.md new file mode 100644 index 0000000..87dd350 --- /dev/null +++ b/doc/_content/ability/device.md @@ -0,0 +1,72 @@ + + +# 接入设备 + +## 国标28181设备 + +设备接入主要是需要在设备上配置28181上级也就是WVP-PRO的信息,只有信息一致的情况才可以注册成功。设备注册成功后打开WVP-> +国标设备,可以看到新增加的设备;[设备使用](./_content/ability/device_use.md), +主要有以下字段需要配置: + +- sip->port + 28181服务监听的端口 + +- sip->domain + domain宜采用ID统一编码的前十位编码。 + +- sip->id + 28181服务ID + +- sip->password + 28181服务密码 + +- 配置信息在如下位置 + +![_media/img_16.png](_media/img_16.png) +*** + +### 1. 大华摄像头 + +![_media/img_10.png](_media/img_10.png) + +### 2. 大华NVR + +![_media/img_11.png](_media/img_11.png) + +### 3. 宇视科技 + +![_media/img_25.png](_media/img_25.png) + +### 3. 艾科威视摄像头 + +![_media/img_15.png](_media/img_15.png) + +### 4. 水星摄像头 + +![_media/img_12.png](_media/img_12.png) + +### 5. 海康摄像头 + +![_media/img_9.png](_media/img_9.png) + +## 直播推流设备 + +这里以obs推流为例,很多无人机也是一样的,设置下推流地址就可以接入了 + +1. 从wvp获取推流地址, 选择节点管理菜单,查看要推流的节点; + ![_media/img_19.png](_media/img_19.png) +2. 拼接推流地址 + 得到的rtsp地址就是: rtsp://{流IP}:{RTSP PORT}/{app}/{stream} + 得到的rtmp地址就是: rtsp://{流IP}:{RTMP PORT}/{app}/{stream} + 其中流IP是设备可以连接到zlm的IP,端口是对应协议的端口号, app和stream自己定义就可以. +3. 增加推流鉴权信息 + wvp默认开启推流鉴权,拼接好的地址是不能直接推送的,会被返回鉴权失败,参考[推流规则](_content/ability/push?id=推流规则) +4. 推流成功后可以再推流列表中看到推流设备,可以播放 + 此方式只支持设备实时流的播放,无其他功能, 推流信息在推流结束后会自动移除,在列表里就看不到了,如果需要推流信息需要为设备配置国标编号,这样才可以作为wvp的一个永久通道存在. + +## 接入非国标IPC设备或者其他流地址形式的设备 + +这类设备的接入主要通过拉流代理的方式接入,原理就是zlm主动像播放器一样拉取这个流缓存在自己服务器供其他人播放.可以解决源设备并发访问能力差的问题. +在拉流代理/添加代理后可以直接播放, 拉流代理也是同样只支持播放当前配置的流. + +[设备使用](_content/ability/device_use.md) diff --git a/doc/_content/ability/device_use.md b/doc/_content/ability/device_use.md new file mode 100644 index 0000000..6cb54dc --- /dev/null +++ b/doc/_content/ability/device_use.md @@ -0,0 +1,62 @@ + + +# 国标设备 + +### 更新设备通道 + +点击列表末尾的“刷新”按钮,可以看到一个圆形进度条,等进度结束提示成功后即可更新完成,如果通道数量有变化你可以看点击左上角的![刷新](_media/img_14.png) +即可看到通道数量的变化;如果通道数量仍未0,那么可能时对方尚未推送通道给你。 + +### 查看设备通道 + +点击列表末尾的“通道”按钮, + +### 编辑设备 + +点击列表末尾的“编辑”按钮,即可在打开的弹窗中对设备功能进行修改 + +- 设备名称 + 如何未能从设备里读取到设备名称或者需要自己重命名,那么可以修改此选项。 +- 密码 + 支持为设备配置独立的密码. +- 收流IP + 如果你需要设备从指定的网络地址接入视频流,那么可以配置此IP,设备将会发流到这个IP,比如多网卡接入的服务器.或者存在网络映射的情况. +- 流媒体ID + 固定设备使用的流媒体ID,默认根据负载自动分配. +- 字符集 + 修改读取设备数据时使用的字符集,默认为GB2312,但是GB2312收录的汉字不全,所以有时候回遇到乱码,可以修改为UTF-8来解决。 +- 目录订阅 + 填写订阅周期即可对设备开启目录订阅,设备如果支持目录订阅那么设备在通道信息发生变化时就会通知WVP哪些通道发生了那些变化,包括通道增加/删除/更新/上线/下线/视频丢失/故障。0为取消订阅。 + 一般NVR和平台对接可以开启此选项,直接接摄像机开启此选项意义不大。 +- 移动位置订阅 + 对设备开启移动位置订阅,设备如果支持目录订阅那么设备位置发生变化时会通知到WVP,一般执法记录仪可以开启此选项,对固定位置的设备意义不大。 +- SSRC校验 + 为了解决部分设备出现的串流问题,可以打开此选项。ZLM会严格按照给定的ssrc处理视频流。部分设备流信息不标准,开启可能导致无法点播。 +- 作为消息通道 + wvp支持通过报警消息给下级WVP互相推送消息,消息内容由redis消息发送给wvp,wvp编辑成报警消息发送给下级 +- 收到ACK后发流 + 语音对讲策略: 不同的设备对于语音对讲的收流时机要求不一,勾选后会在收到设备发送的ack后再开始发流,不勾选则在回复200OK后开始发流,目前已知大华设备不勾选,海康需要勾选. + +### 删除设备 + +可以删除WVP中的设备信息,如果设备28181配置未更改,那么设备在下一次注册后仍然会注册上来。 + +### 点播视频 + +进入通道列表后,点击列表末尾的“播放”按钮,稍等即可弹出播放页面 + +### 设备录像 + +进入通道列表后,点击列表末尾的“设备录像”按钮,也可以在播放页面点击录像查询进入录像查看页面,选择要查看的日期即可对录像进行播放和下载。 + +### 云台控制 + +可以对支持云台功能的设备进行上下左右的转动以及拉近拉远的操作。 + +### 获取视频的播放器地址 + +视频点播成功后在实时视频页面,点击“更多地址”可以看到所有的播放地址,地址是否可以播放与你是否完整编译启用zlm功能有关,更与网络有关。 + +### 语音对讲 + +[语音对讲](_content/ability/continuous_broadcast.md) \ No newline at end of file diff --git a/doc/_content/ability/gis.md b/doc/_content/ability/gis.md new file mode 100644 index 0000000..5932df9 --- /dev/null +++ b/doc/_content/ability/gis.md @@ -0,0 +1,43 @@ + + +# 电子地图 + +WVP提供了简单的电子地图用于设备的定位以及移动设备的轨迹信息,电子地图基于开源的地图引擎openlayers开发。 + +### 查看设备定位 + +1. 可以在设备列表点击“定位”按钮,自动跳转到电子地图页面; +2. 在电子地图页面在设备上右键点击“定位”获取设备/平台下的所有通道位置。 +3. 单击通道信息可以定位到具体的通道 + +### 查询设备轨迹 + +查询轨迹需要提前配置save-position-history选项开启轨迹信息的保存,目前WVP此处未支持分库分表,对于大数据量的轨迹信息无法胜任,有需求请自行二次开发或者定制开发。 +在电子地图页面在设备上右键点击“查询轨迹”获取设备轨迹信息。 + +PS: 目前的底图仅用用作演示和学习,商用情况请自行购买授权使用。 + +### 更换底图以及底图配置 + +目前WVP支持使用了更换底图,配置文件在web_src/static/js/config.js,请修改后重新编译前端文件。 + +```javascript +window.mapParam = { + // 开启/关闭地图功能 + enable: true, + // 坐标系 GCJ-02 WGS-84, + coordinateSystem: "GCJ-02", + // 地图瓦片地址 + tilesUrl: "http://webrd0{1-4}.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8", + // 瓦片大小 + tileSize: 256, + // 默认层级 + zoom:10, + // 默认地图中心点 + center:[116.41020, 39.915119], + // 地图最大层级 + maxZoom:18, + // 地图最小层级 + minZoom: 3 +} +``` diff --git a/doc/_content/ability/node_manager.md b/doc/_content/ability/node_manager.md new file mode 100644 index 0000000..a8d6b10 --- /dev/null +++ b/doc/_content/ability/node_manager.md @@ -0,0 +1,20 @@ + + +# 节点管理 + +![节点管理](_media/img_26.png) + +WVP支持单个WVP多个ZLM的方案来扩展WVP的视频并发能力,并发点播是因为带宽和性能的原因,单个ZLM节点能支持的路数有限,所以WVP增加了ZLM集群来扩展并发并且保证ZLM的高可用。 + +## 默认节点 + +WVP中为了保证功能的完整性,ZLM节点至少要有一个默认节点,这个节点不是在管理页面添加的,而是在WVP的配置文件中配置的,这个节点不可在页面删除。每次启动会自动从配置文件中读取配置写入数据库备用。 + +## 新增节点 + +启动你要添加的zlm节点,然后点击“添加节点”按钮输入zlm的ip, +http端口,SECRET。点击测试测试完成则开始对节点进行详细的设置,如果你的zlm是使用docker启动的,可能存在zlm使用的端口与宿主机端口不一致的情况,需要在这里一一配置。 + +## wvp使用多个节点的原理 + +wvp会把连接的节点统一记录在redis中,并记录zlm的负载情况,当新的请求到来时,会取出负载最低的那个zlm进行使用。以此保证节点负载均衡。 diff --git a/doc/_content/ability/online_doc.md b/doc/_content/ability/online_doc.md new file mode 100644 index 0000000..50d5070 --- /dev/null +++ b/doc/_content/ability/online_doc.md @@ -0,0 +1,3 @@ + + +# 在线文档 diff --git a/doc/_content/ability/proxy.md b/doc/_content/ability/proxy.md new file mode 100644 index 0000000..7080442 --- /dev/null +++ b/doc/_content/ability/proxy.md @@ -0,0 +1,32 @@ + + +# 拉流代理 + +不是所有的摄像机都支持国标或者推流的,但是这些设备可以得到一个视频播放地址,通常为rtsp协议, +以大华为例: + +```text +rtsp://{user}:{passwd}@{ipc_ip}:{rtsp_port}/cam/realmonitor?channel=1&subtype=0 +``` + +可以得到这样一个流地址,可以直接用vlc进行播放,此时我们可以通过拉流代理功能将这个设备推送给其他国标平台了。 +流程如下: + +```plantuml +@startuml +"摄像机" <- "ZLMediaKit": 1. 流去流信息到ZLM +"ZLMediaKit" -> "WVP-PRO": 2. 收到hook通知得到流信息 +"上级国标平台" -> "WVP-PRO": 3. 点播这路视频 +"WVP-PRO" -> "ZLMediaKit": 4. 通知推流到上级国标平台 +@enduml +``` + +## 添加代理 + +拉流代理支持两种方式: + +1. ZLM中直接代理流,支持RTSP/RTMP,不支持转码; +2. 借助ffmpeg完成拉转,可以通过修改ffmpeg拉转参数完成转码。 + 点击页面的“添加代理”,添加信息后保存即可,如果你需要共享推流信息到其他国标平台,那么你需要编辑/国标通道配置,配置国标编码. + +`PS: ffmpeg默认模板不需修改,需要修改`参数自行去ZLM配置文件中添加一个即可。 diff --git a/doc/_content/ability/push.md b/doc/_content/ability/push.md new file mode 100644 index 0000000..8998a4b --- /dev/null +++ b/doc/_content/ability/push.md @@ -0,0 +1,58 @@ + + +# 推流列表 + +## 功能说明 + +WVP支持三种图像输入方式,直播,[拉流代理](_content/ability/proxy.md),[国标](_content/ability/device.md),直播设备接入流程如下 + +```plantuml +@startuml +"直播设备" -> "ZLMediaKit": 1. 发起推流 +"ZLMediaKit" -> "WVP-PRO": 2. 收到hook通知得到流信息 +"上级国标平台" -> "WVP-PRO": 3. 点播这路视频 +"WVP-PRO" -> "ZLMediaKit": 4. 通知推流到上级国标平台 +@enduml +``` + +1. 默认情况下WVP收到推流信息后,列表中出现这条推流信息,如果你需要共享推流信息到其他国标平台,那么你需要编辑/国标通道配置,配置国标编码. +2. WVP也支持推流前导入大量通道直接推送给上级,点击“下载模板”按钮,根据示例修改模板后,点击“通道导入”按钮导入通道数据. + +## 生成推流地址 +可以在推流列表里点击‘生成推流地址’按钮,得到新地址后直接复制到推流设备。 + +## 推拉流鉴权规则 + +为了保护服务器的WVP默认开启推流鉴权(目前不支持关闭此功能) + +### 推流规则 + +推流时需要携带推流鉴权的签名sign,sign=md5(pushKey),pushKey来自用户表,每个用户会有一个不同的pushKey. +例如app=test,stream=live,pushKey=1000,ip=192.168.1.4, port=10554 那么推流地址为: + +``` +rtsp://192.168.1.4:10554/test/live?sign=a9b7ba70783b617e9998dc4dd82eb3c5 +``` + +支持推流时自定义播放鉴权Id,参数名为callId,此时sign=md5(callId_pushKey) +例如app=test,stream=live,pushKey=1000,callId=12345678, ip=192.168.1.4, port=10554 那么推流地址为: + +``` +rtsp://192.168.1.4:10554/test/live?callId=12345678&sign=c8e6e01dde2d60c66dcea8d2498ffef1 +``` + +### 播放规则 + +默认情况播放不需要鉴权,但是如果推流时携带了callId,那么播放时必须携带callId +例如app=test,stream=live,无callId, ip=192.168.1.4, port=10554 那么播放地址为: + +``` +rtsp://192.168.1.4:10554/test/live +``` + +例如app=test,stream=live,callId=12345678, ip=192.168.1.4, port=10554 那么播放地址为: + +``` +rtsp://192.168.1.4:10554/test/live?callId=12345678 +``` + diff --git a/doc/_content/ability/user.md b/doc/_content/ability/user.md new file mode 100644 index 0000000..776fe6b --- /dev/null +++ b/doc/_content/ability/user.md @@ -0,0 +1,3 @@ + + +# 用户管理 diff --git a/doc/_content/about_doc.md b/doc/_content/about_doc.md new file mode 100644 index 0000000..02e3613 --- /dev/null +++ b/doc/_content/about_doc.md @@ -0,0 +1,7 @@ + + +# 关于本文档 + +本文档开源在gitee上,[https://gitee.com/pan648540858/wvp-pro-doc.git](https://gitee.com/pan648540858/wvp-pro-doc.git) +,如果文档出现任何错误或者不易理解的语句,请大家提ISSUE帮助我及时更正。欢迎大家提交PR一起维护这份文档,让更多的人可以使用到这个开源的视频平台。 + diff --git a/doc/_content/broadcast.md b/doc/_content/broadcast.md new file mode 100644 index 0000000..355e761 --- /dev/null +++ b/doc/_content/broadcast.md @@ -0,0 +1,29 @@ +# 原理图 + +## 使用ffmpeg测试语音对讲原理 + +```plantuml +@startuml +"FFMPEG" -> "ZLMediaKit": 推流到zlm +"WVP-PRO" <- "ZLMediaKit": 通知收到语音对讲推流,携带设备和通道信息 +"WVP-PRO" -> "设备": 开始语音对讲 +"WVP-PRO" <-- "设备": 语音对讲建立成功,携带收流端口 +"WVP-PRO" -> "ZLMediaKit": 通知zlm将流推送到设备收流端口 +"ZLMediaKit" -> "设备": 向设备推流 +@enduml +``` + +## 使用网页测试语音对讲原理 + +```plantuml +@startuml +"前端页面" -> "WVP-PRO": 请求推流地址 +"前端页面" <-- "WVP-PRO": 返回推流地址 +"前端页面" -> "ZLMediaKit": 使用webrtc推流到zlm,以下过程相同 +"WVP-PRO" <- "ZLMediaKit": 通知收到语音对讲推流,携带设备和通道信息 +"WVP-PRO" -> "设备": 开始语音对讲 +"WVP-PRO" <-- "设备": 语音对讲建立成功,携带收流端口 +"WVP-PRO" -> "ZLMediaKit": 通知zlm将流推送到设备收流端口 +"ZLMediaKit" -> "设备": 向设备推流 +@enduml +``` \ No newline at end of file diff --git a/doc/_content/disclaimers.md b/doc/_content/disclaimers.md new file mode 100644 index 0000000..b0b2e64 --- /dev/null +++ b/doc/_content/disclaimers.md @@ -0,0 +1,5 @@ +# 免责声明 + +WVP-PRO自有代码使用宽松的MIT协议,在保留版权信息的情况下可以自由应用于各自商用、非商业的项目。 +但是本项目也零碎的使用了一些其他的开源代码,在商用的情况下请自行替代或剔除; 由于使用本项目而产生的商业纠纷或侵权行为一概与本项目及开发者无关,请自行承担法律风险。 +在使用本项目代码时,也应该在授权协议中同时表明本项目依赖的第三方库的协议 \ No newline at end of file diff --git a/doc/_content/introduction/_media/img.png b/doc/_content/introduction/_media/img.png new file mode 100644 index 0000000..fa03cd8 Binary files /dev/null and b/doc/_content/introduction/_media/img.png differ diff --git a/doc/_content/introduction/_media/img_1.png b/doc/_content/introduction/_media/img_1.png new file mode 100644 index 0000000..b4a62af Binary files /dev/null and b/doc/_content/introduction/_media/img_1.png differ diff --git a/doc/_content/introduction/_media/img_2.png b/doc/_content/introduction/_media/img_2.png new file mode 100644 index 0000000..a5961d6 Binary files /dev/null and b/doc/_content/introduction/_media/img_2.png differ diff --git a/doc/_content/introduction/compile.md b/doc/_content/introduction/compile.md new file mode 100644 index 0000000..659fe11 --- /dev/null +++ b/doc/_content/introduction/compile.md @@ -0,0 +1,124 @@ + + +# 编译 + +WVP-PRO不只是实现了国标28181的协议,本身也是一个完整的视频平台。所以对于新手来说,你可能需要一些耐心来完成。遇到问题不要焦躁,你可以 + +1. 百度 +2. 加入星球体提问;[知识星球](https://t.zsxq.com/0d8VAD3Dm) +3. 向作者发送邮件648540858@qq.com,寻求技术支持(有偿); + +WVP-PRO使用Spring boot开发,maven管理依赖。对于熟悉spring开发的朋友是很容易进行编译部署以及运行的。 +下面将提供一种通用方法方便大家运行项目。 + +## 1 服务介绍 + +| 服务 | 作用 | 是否必须 | +|------------|------------------------------------------|------| +| WVP-PRO | 实现国标28181的信令以及视频平台相关的功能 | 是 | +| ZLMediaKit | 为WVP-PRO提供国标28181的媒体部分的实现,以及各种视频流格式的分发支持 | 是 | + +## 2 安装依赖 + +| 依赖 | 版本 | 用途 | 开发环境需要 | 生产环境需要 | +|--------|-------|-------------|--------|--------| +| jdk | >=1.8 | 运行与编译java代码 | 是 | 是 | +| maven | >=3.3 | 管理java代码依赖 | 否 | 否 | +| git | | 下载/更新/提交代码 | 否 | 否 | +| nodejs | | 编译于运行前端文件 | 否 | 否 | +| npm | | 管理前端文件依赖 | 否 | 否 | + +如果你是一个新手,建议你使用linux或者macOS平台。windows不推荐。 + +ubuntu环境,以ubuntu 18为例: + +``` bash +apt-get install -y openjdk-11-jre git maven nodejs npm +``` + +centos环境,以centos 8为例: + +```bash +yum install -y java-1.8.0-openjdk.x86_64 git maven nodejs npm +``` + +window环境,以windows10为例: + +```bash +这里不细说了,百度或者谷歌一搜一大把,基本都是下一步下一步,然后配置环境变量。 +``` + +## 3 安装mysql以及redis + +这里依然是参考网上教程,自行安装吧。 + +## 4 编译ZLMediaKit + +参考ZLMediaKit[WIKI](https://github.com/ZLMediaKit/ZLMediaKit/wiki) +,如果需要使用语音对讲功能,请参考[zlm启用webrtc编译指南](https://github.com/ZLMediaKit/ZLMediaKit/wiki/zlm%E5%90%AF%E7%94%A8webrtc%E7%BC%96%E8%AF%91%E6%8C%87%E5%8D%97) +,开启zlm的webrtc功能。截取一下关键步骤: + +```bash +# 国内用户推荐从同步镜像网站gitee下载 +git clone --depth 1 https://gitee.com/xia-chu/ZLMediaKit +cd ZLMediaKit +# 千万不要忘记执行这句命令 +git submodule update --init +``` + +## 5 编译WVP-PRO + +### 5.1 可以通过git克隆,也可以在项目下载点击下载 + +![点击下载](_media/img_1.png) +![点击下载](_media/img_2.png) +从gitee克隆 + +```bash +git clone https://gitee.com/pan648540858/wvp-GB28181-pro.git +``` + +从github克隆 + +```bash +git clone https://github.com/648540858/wvp-GB28181-pro.git +``` + +### 5.2 编译前端页面 + +```shell script +cd wvp-GB28181-pro/web/ +npm --registry=https://registry.npmmirror.com install +npm run build:prod +``` + +编译如果报错, 一般都是网络问题, 导致的依赖包下载失败 +编译完成后在src/main/resources下出现static目录 +**编译完成一般是这个样子,中间没有报红的错误信息** +![编译成功](_media/img.png) + +### 5.3 生成可执行jar + +```bash +cd wvp-GB28181-pro +mvn package +``` + +### 5.4 生成war + +```bash +cd wvp-GB28181-pro +mvn package -P war +``` + +编译如果报错, 一般都是网络问题, 导致的依赖包下载失败 +编译完成后在target目录下出现 `wvp-pro-VERSION.jar` 和 `wvp-pro-VERSION.war` 文件。 +接下来[配置服务](./_content/introduction/config.md) + + + + + + + + diff --git a/doc/_content/introduction/config.md b/doc/_content/introduction/config.md new file mode 100644 index 0000000..7beec14 --- /dev/null +++ b/doc/_content/introduction/config.md @@ -0,0 +1,166 @@ + + +# 配置 + +对于首次测试或者新手同学,我建议在局域网测试,并且关闭服务器与客户机的防火墙测试。建议部署在linux进行测试。 + +```plantuml +@startuml +"WVP-PRO" -> "ZLMediaKit": RESTful 接口 +"WVP-PRO" <-- "ZLMediaKit": Web Hook 接口 +@enduml +``` + +WVP-PRO通过调用ZLMediaKit的RESTful接口实现对ZLMediaKit行为的控制; ZLMediaKit通过Web Hook 接口把消息通知WVP-PRO。通过这种方式,实现了两者的互通。 +对于最简单的配置,你不需要修改ZLMediaKit的任何默认配置。你只需要在WVP-PRO中配置的ZLMediaKit信息即可 + +## 1 WVP配置文件位置 + +基于spring boot的开发方式,配置文件的加载是很灵活的。默认在src/main/resources/application.yml,部分配置项是可选,你不需要全部配置在配置文件中, +完全的配置说明可以参看"src/main/resources/配置详情.yml"。 + +### 1.1 默认加载配置文件方式 + +使用maven打包后的target里,已经存在了配置文件,默认加载配置文件为application.yml,查看内容发现其中spring.profiles.active配置的内容,将入配置的值为dev,那么具体加载的配置文件就是application-dev.yml,如果配置的值找不到对应的配置文件,修改值为dev。 + +```shell +cd wvp-GB28181-pro/target +java -jar wvp-pro-*.jar +``` + +## 2 配置WVP-PRO + +wvp支持多种数据库,包括Mysql,Postgresql,金仓等,配置任选一种即可。 + +### 2.1 数据库配置 + +#### 2.1.1 初始化数据库 + +首先使用创建数据库,然后使用sql/初始化.sql初始化数据库,如果是从旧版升级上来的,使用升级sql更新。 + +#### 2.1.2 Mysql数据库配置 + +数据库名称以wvp为例 + +```yaml +spring: + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://127.0.0.1:3306/wvp?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true + username: root + password: root123 +``` + +#### 2.1.3 Postgresql数据库配置 + +数据库名称以wvp为例 + +```yaml +spring: + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://127.0.0.1:3306/wvp?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true&allowPublicKeyRetrieval=true + username: root + password: 12345678 + +pagehelper: + helper-dialect: postgresql +``` + +#### 2.1.4 金仓数据库配置 + +数据库名称以wvp为例 + +```yaml +spring: + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.kingbase8.Driver + url: jdbc:kingbase8://127.0.0.1:3306/wvp?useUnicode=true&characterEncoding=utf8 + username: root + password: 12345678 + + +pagehelper: + helper-dialect: postgresql +``` + +### 2.2 Redis数据库配置 + +配置wvp中的redis连接信息,建议wvp自己单独使用一个db。 + +### 2.3 配置服务启动端口(可直接使用默认配置) + +```yaml +# [可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口 +server: + port: 18080 +``` + +### 2.4 配置28181相关信息(可直接使用默认配置) + +```yaml +# 作为28181服务器的配置 +sip: + # [可选] 28181服务监听的端口 + port: 5060 + # 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007) + # 后两位为行业编码,定义参照附录D.3 + # 3701020049标识山东济南历下区 信息行业接入 + # [可选] + domain: 3402000000 + # [可选] + id: 34020000002000000001 + # [可选] 默认设备认证密码,后续扩展使用设备单独密码, 移除密码将不进行校验 + password: 12345678 +``` + +### 2.5 配置ZLMediaKit连接信息 + +```yaml +#zlm 默认服务器配置 +media: + id: zlmediakit-local + # [必须修改] zlm服务器的内网IP + ip: 172.19.128.50 + # [可选] 有公网IP就配置公网IP, 不可用域名 + wan_ip: + # [必须修改] zlm服务器的http.port + http-port: 9092 + # [可选] zlm服务器访问WVP所使用的IP, 默认使用127.0.0.1,zlm和wvp没有部署在同一台服务器时必须配置 + hook-ip: 172.19.128.50 + # [必选选] zlm服务器的hook.admin_params=secret + secret: TWSYFgYJOQWB4ftgeYut8DW4wbs7pQnj + # 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试 + rtp: + # [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输 + enable: true + # [可选] 在此范围内选择端口用于媒体流传输, 必须提前在zlm上配置该属性,不然自动配置此属性可能不成功 + port-range: 30000,35000 # 端口范围 + # [可选] 国标级联在此范围内选择端口发送媒体流, + send-port-range: 40000,40300 # 端口范围 +``` + +### 2.4 策略配置 + +```yaml +# [根据业务需求配置] +user-settings: + # 点播/录像回放 等待超时时间,单位:毫秒 + play-timeout: 180000 + # [可选] 自动点播, 使用固定流地址进行播放时,如果未点播则自动进行点播, 需要rtp.enable=true + auto-apply-play: true + # 推流直播是否录制 + record-push-live: true + # 国标是否录制 + record-sip: true + # 国标点播 按需拉流, true:有人观看拉流,无人观看释放, false:拉起后不自动释放 + stream-on-demand: true +``` + +更多完整的配置信息参考"src/main/resources/配置详情.yml"文件,需要那个配置项,复制到正在使用的配置文件中对应的文件即可。 + +如果配置信息无误,你可以启动zlm,再启动wvp来测试了,启动成功的话,你可以在wvp的日志下看到zlm已连接的提示。 +接下来[部署到服务器](./_content/introduction/deployment.md), 如果你只是本地运行直接在本地运行即可。 diff --git a/doc/_content/introduction/deployment.md b/doc/_content/introduction/deployment.md new file mode 100644 index 0000000..28dc8d6 --- /dev/null +++ b/doc/_content/introduction/deployment.md @@ -0,0 +1,51 @@ + + +# 部署 + +**请仔细阅读以下内容** + +1. WVP-PRO与ZLM支持分开部署; +2. 需要开放的端口 + | 服务 | 端口 | 类型 | 必选 | + |-----|:-------------------------|-------------|-------| + | wvp | server.port | tcp | 是 | + | wvp | sip.port | udp and tcp | 是 | + | zlm | http.port | tcp | 是 | + | zlm | http.sslport | tcp | 否 | + | zlm | rtmp.port | tcp | 否 | + | zlm | rtmp.sslport | tcp | 否 | + | zlm | rtsp.port | udp and tcp | 否 | + | zlm | rtsp.sslport | udp and tcp | 否 | + | zlm | rtp_proxy.port | udp and tcp | 单端口开放 | + | zlm | rtp.port-range(在wvp中配置) | udp and tcp | 多端口开放 | + +3. 测试环境部署建议所有服务部署在一台主机,关闭防火墙,减少因网络出现问题的可能; +4. 生产环境按需开放端口,但是建议修改默认端口,尤其是5060端口,易受到攻击; +5. zlm使用docker部署的情况,请使用host模式,或者端口映射一致,比如映射5060,应将外部端口也映射为5060端口; +6. zlm与wvp会保持高频率的通信,所以不要去将wvp与zlm分属在两个网络,比如wvp在内网,zlm却在公网的情况。 +7. 启动服务,以linux为例 + **启动WVP-PRO** + +```shell +nohup java -jar wvp-pro-*.jar & +``` + +**war包:** +下载Tomcat后将war包放入webapps中,启动Tomcat以解压war包,停止Tomcat后,删除ROOT目录以及war包,将解压后的war包目录重命名为ROOT,将配置文件中的Server.port配置为与Tomcat端口一致 +然后启动Tomcat。 +**启动ZLM** + +```shell +nohup ./MediaServer -d -m 3 & +``` + +### 前后端分离部署 + +前端基于 [vue-admin-template](https://github.com/PanJiaChen/vue-admin-template/blob/master/README-zh.md) 构建, 参考这儿即可。 + +### 默认账号和密码 + +部署完毕后,可以通过访问 ip加端口的方式访问 WVP ,WVP的默认登录账号和密码均为 admin。 + + + diff --git a/doc/_content/qa/_media/img.png b/doc/_content/qa/_media/img.png new file mode 100644 index 0000000..d6c29d7 Binary files /dev/null and b/doc/_content/qa/_media/img.png differ diff --git a/doc/_content/qa/_media/img_1.png b/doc/_content/qa/_media/img_1.png new file mode 100644 index 0000000..5b74d95 Binary files /dev/null and b/doc/_content/qa/_media/img_1.png differ diff --git a/doc/_content/qa/_media/img_2.png b/doc/_content/qa/_media/img_2.png new file mode 100644 index 0000000..4aaa7fe Binary files /dev/null and b/doc/_content/qa/_media/img_2.png differ diff --git a/doc/_content/qa/_media/img_3.png b/doc/_content/qa/_media/img_3.png new file mode 100644 index 0000000..27f8a96 Binary files /dev/null and b/doc/_content/qa/_media/img_3.png differ diff --git a/doc/_content/qa/_media/img_4.png b/doc/_content/qa/_media/img_4.png new file mode 100644 index 0000000..aa3b88e Binary files /dev/null and b/doc/_content/qa/_media/img_4.png differ diff --git a/doc/_content/qa/_media/img_5.png b/doc/_content/qa/_media/img_5.png new file mode 100644 index 0000000..76e6faf Binary files /dev/null and b/doc/_content/qa/_media/img_5.png differ diff --git a/doc/_content/qa/bug.md b/doc/_content/qa/bug.md new file mode 100644 index 0000000..55e4caf --- /dev/null +++ b/doc/_content/qa/bug.md @@ -0,0 +1,18 @@ + + +# 反馈bug + +代码是在不断的完善的,不断修改会修复旧的问题也有可能引入新的问题,所以遇到BUG是很正常的一件事。所以遇到问题不要烦燥,咱们就事论事就好了。 + +## 如何反馈 + +1. 在知识星球提问。 +2. 更新代码,很可能你遇到问题别人已经更早的遇到了,或者是作者自己发现了,已经解决了,所以你可以更新代码再次进行测试; +3. 可以在github提ISSUE,我几乎每天都会去看issue,你的问题我会尽快给予答复; + +> 有偿支持可以给我发邮件, 648540858@qq.com + +## 社群 + +[![社群](../../_media/shequ.png "shequ")](https://t.zsxq.com/0d8VAD3Dm) +> 收费是为了提供更好的服务,也是对作者更大的激励。加入星球的用户三天后可以私信我留下微信号,我会拉大家入群。加入三天内不满意可以直接退款,大家不需要有顾虑,来白嫖三天也不是不可以。 \ No newline at end of file diff --git a/doc/_content/qa/development.md b/doc/_content/qa/development.md new file mode 100644 index 0000000..ec9df9e --- /dev/null +++ b/doc/_content/qa/development.md @@ -0,0 +1,21 @@ + + +# 参与到开发中来 + +非常欢迎有兴趣的小伙伴一起来维护这个项目 + +## 与开发有关的信息 + +- 开发语言:后端java + 前端vue; +- jdk版本: 1.8; +- 作者自用开发ide: jetbrains intellij idea; +- nodejs/npm版本:v10.19.0/6.14.4; +- 后端使用Spring boot框架开发; +- 项目大量使用了异步操作; +- 跟代码学流程需要参考28181文档,只看代码你会很懵的; +- 必须学会[抓包](_content/skill/tcpdump.md),这是必须的 + +## 提交代码 + +大家可以通过fork项目的方式提交自己的代码,然后提交PR,我来合并到主线。提交代码的过程中我们需要遵循“**阿里编码规约** +”,现有代码也有很多代码没有做到,但是我们在朝这个方向努力。 \ No newline at end of file diff --git a/doc/_content/qa/img.png b/doc/_content/qa/img.png new file mode 100644 index 0000000..d6c29d7 Binary files /dev/null and b/doc/_content/qa/img.png differ diff --git a/doc/_content/qa/play_error.md b/doc/_content/qa/play_error.md new file mode 100644 index 0000000..a474399 --- /dev/null +++ b/doc/_content/qa/play_error.md @@ -0,0 +1,74 @@ + + +# 点播错误 + +排查点播错误你首先要清楚[点播的基本流程](_content/theory/play.md),一般的流程如下: + +```plantuml +@startuml +"WEB用户" -> "WVP-PRO": 1. 发起点播请求 +"设备" <- "WVP-PRO": 2. Invite(携带SDP消息体) +"设备" --> "WVP-PRO": 3. 200OK(携带SDP消息体) +"设备" <-- "WVP-PRO": 4. Ack +"设备" -> "ZLMediaKit": 5. 发送实时流 +"WVP-PRO" <- "ZLMediaKit": 6. 流改变事件 +"WEB用户" <-- "WVP-PRO": 7. 回复流播放地址(携带流地址) +"WVP-PRO" <- "ZLMediaKit": 8. 无人观看事件 +"设备" <- "WVP-PRO": 9 Bye消息 +"设备" --> "WVP-PRO": 10 200OK +@enduml +``` + +针对几种常见的错误,我们来分析一下,也方便大家对号入座解决常见的问题 + +## 点播收到错误码 + +这个错误一般表现为点击"播放"按钮后很快得到一个错误。 + +1. **400错误码** + 出现400错误玛时一般是这样的流程是这样的 + +```plantuml +@startuml +"WEB用户" -> "WVP-PRO": 1. 发起点播请求 +"设备" <- "WVP-PRO": 2. Invite(携带SDP消息体) +"设备" --> "WVP-PRO": 3. 400错误 +@enduml +``` + +此时通常是设备认为WVP发送了错误的消息给它,它认为消息不全或者错误所以直接返回400错误,此时我们需要[抓包](_content/skill/tcpdump.md) +来分析是否缺失了内容,也可以直接联系对方询问为什么返回了400。 +WVP不能保证兼容所有的设备,有些实现不规范的设备可能在对接时就会出现上述问题,你可以联系作者帮忙对接。 + +2. **500错误码** + 500或者大于500小于600的错误码一般多是设备内部出了问题,解决方式有两个,第一种直接联系设备/平台客服寻求解决;第二种,如果你有确定可以对接这个设备的平台那么可以把对接这个平台的抓包和对接wvp的抓包同时发送给我,我来尝试解决。 + +## 点播超时 + +点播超时的情况大致分为两种:点播超时和收流超时 + +1. **点播超时** + 点播超时错误一般为信令的超时,比如长时间为收到对方的回复,可能出现在流程中 “3. 200OK(携带SDP消息体) + ”这个位置,即我们发送点播消息,但是设备没有回复,可能的原因: + +> 1. 设备内部错误,未能回复消息 +> 2. 网络原因消息未到到达设备 + +大部分时候是原因2,所以遇到这个错误我们首先要排查我们我的网路,如果你是公网部署,那么也可能时心跳周期太长,导致的路由NAT失效,WVP的消息无法通道原来的IP端口号发送给设备。 + +2. **收流超时** + 收流超时可能发生在流程中的5和6,可能的原因有: + +> 1. 设备发送了流但是发送到了错误的ip和端口上,而这个信息是在invite消息的sdp中指定的,就是流程2Invite(携带SDP消息体) + 中,而这个错误很可能来自你的配置错误,比如你设置了127.0.0.1导致设备网127.0.0.1上发流,或者是你WVP在公网,但是你给设备了一个内网ip,导致设备无法把流发送过来; +> 2. 设备内部错误未发送流; +> 2. 设备发送了流,但是流无法识别,可能存在于流不规范和网络很差的情况下; +> 3. 设备发送了流,zlm也收到了,但是zlm无法通过hook通知到wvp,此时原因是你可以检查zlm的配置文件中的hook配置,看看是否无法从zlm连接到wvp; +> 4. 设备发送了流,但是开启SSRC校验,设备的流不够规范采用错误的ssrc,导致zlm选择丢弃; + +针对这些可能的错误原因我建议的排查顺序: + +- 关闭ssrc校验; +- 查看zlm配置的hook是否可以连接到zlm; +- 查看zlm日志是否有流注册; +- 抓包查看流的信息,看看流是否正常发送,甚至可以导出发送原始流,用vlc播放,看看是否可以播放。 diff --git a/doc/_content/qa/regiser_error.md b/doc/_content/qa/regiser_error.md new file mode 100644 index 0000000..7d4e96c --- /dev/null +++ b/doc/_content/qa/regiser_error.md @@ -0,0 +1,10 @@ + + +# 设备注册不上来的解决办法 + +一般的原因有两个 + +1. 信息填写错误,比如密码错误; +2. 网络不通导致注册消息无法发送到WVP; + +遇到问题首先仔细校验填写信息,例如海康可能需要勾选鉴权才可以输入密码。网络问题请自行测试。 \ No newline at end of file diff --git a/doc/_content/qa/start_error.md b/doc/_content/qa/start_error.md new file mode 100644 index 0000000..3e5749d --- /dev/null +++ b/doc/_content/qa/start_error.md @@ -0,0 +1,26 @@ + + +# 启动时报错 + +启动时的报错大部分时候是因为你的配置有问题,比如mysql没连接上,redis没连接上,18080/15060端口占用了,这些都会导致启动是报错,修改配置配置之后都可以解决; +下面我整理的一些常见的错误,大家可以先对号入座的简单排查下。 +> **常见错误** + +![_media/img.png](_media/img.png) +**错误原因:** redis配置错误,可能原因: redis未启动/ip错误/端口错误/网络不通 +--- +![_media/img_1.png](_media/img_1.png) +**错误原因:** redis配置错误,可能原因: 密码错误 +--- +![_media/img_2.png](_media/img_2.png) +**错误原因:** mysql配置错误,可能原因: mysql未启动/ip错误/端口错误/网络不通 +--- +![_media/img_3.png](_media/img_3.png) +**错误原因:** mysql配置错误,可能原因: 用户名/密码错误 +--- +![_media/img_4.png](_media/img_4.png) +**错误原因:** SIP配置错误,可能原因: SIP端口被占用 +--- +![_media/img_5.png](_media/img_5.png) +**错误原因:** WVP Tomcat端口配置错误,可能原因: server.port端口被占用 +--- \ No newline at end of file diff --git a/doc/_content/skill/_media/img.png b/doc/_content/skill/_media/img.png new file mode 100644 index 0000000..a9bc95f Binary files /dev/null and b/doc/_content/skill/_media/img.png differ diff --git a/doc/_content/skill/_media/img_1.png b/doc/_content/skill/_media/img_1.png new file mode 100644 index 0000000..e08e4e1 Binary files /dev/null and b/doc/_content/skill/_media/img_1.png differ diff --git a/doc/_content/skill/_media/img_2.png b/doc/_content/skill/_media/img_2.png new file mode 100644 index 0000000..2af0ecc Binary files /dev/null and b/doc/_content/skill/_media/img_2.png differ diff --git a/doc/_content/skill/tcpdump.md b/doc/_content/skill/tcpdump.md new file mode 100644 index 0000000..fa0ee88 --- /dev/null +++ b/doc/_content/skill/tcpdump.md @@ -0,0 +1,94 @@ + + +# 抓包 + +如果说对于网络编程,有什么工具是必会的,我觉得抓包肯定是其中之一了。作为GB/T +28181调试过程中最重要的手段,我觉得如果你真对他有兴趣,或者系统遇到问题可以最快的得到解决,那么抓包你就一定要学会了。 + +## 抓包工具的选择 + +### 1. Wireshark + +在具备图形界面的系统上,比如windows,linux发行版ubuntu,opensuse等,我一般直接使用Wireshark直接进行抓包,也方便进行内容的查看。 + +### 2. Tcpdump + +在使用命令行的系统,比如linux服务器,我一般使用Tcpdump进行抓包,无需额外安装,系统一般自带,抓包的到的文件,可以使用Wireshark打开,在图形界面下方便查看内容。 + +## 工具安装 + +Wireshark的安装很简单,根据提示一步步点击就好了,在linux需要解决权限的问题,如果和我一样使用图形界面的linux发行版的话,可以参看如下步骤; +windows的小伙伴直接略过即可 + +```shell +# 1. 添加wireshark用户组 +sudo groupadd wireshark +# 2. 将dumpcap更改为wireshark用户组 +sudo chgrp wireshark /usr/bin/dumpcap +# 3. 让wireshark用户组有root权限使用dumpcap +sudo chmod 4755 /usr/bin/dumpcap +# 4. 将需要使用的用户名加入wireshark用户组 +sudo gpasswd -a $USER wireshark +``` + +tcpdump一般linux都是自带,无需安装,可以这样验证;显示版本信息即是已安装 + +```shell +tcpdump --version +``` + +## 开始抓包 + +### 使用Wireshark + +在28181中我一般只关注sip包和rtp包,所以我一般是直接过滤sip和rtp,可以输入框输入 `sip or rtp`这样即可,如果设备来源比较多还可以加上ip和端口号的过滤 +`(sip or rtp )and ip.addr==192.168.1.3 and udp.port==5060` +详细的过滤规则可以自行百度,我可以提供一些常用的给大家参考 +![img.png](_media/img.png) +**只过滤SIP:** + +```shell +sip +``` + +**只获取rtp数据:** + +```shell +rtp +``` + +**默认方式:** + +```shell +sip or rtp +``` + +**过滤IP:** + +```shell + sip and ip.addr==192.168.1.3 +``` + +**过滤端口:** + +```shell + sip and udp.port==5060 +``` + +输入命令开启抓包后,此时可以进行操作,比如点播,录像回访等,操作完成回到Wireshark点击红色的停止即可,需要保存文件可以点击 +`文件->导出特定分组`导出过滤后的数据,也可以直接`文件->另存为`保存未过滤的数据。 + +### 使用tcpdump + +对于服务器抓包,为了得到足够完整的数据,我一般会要求直接抓取网卡数据而不过滤,如下: +抓取网卡首先需要获取网卡名,在linux我一般使用`ip addr`获取网卡信息,如下所示: +![img_1.png](_media/img_1.png) + +```shell +sudo tcpdump -i wlp3s0 -w demo.pcap +``` + +![img_2.png](_media/img_2.png) +命令行会停留在这个位置,此时可以进行操作,比如点播,录像回放等,操作完成回到命令行使用`Ctrl+C` +结束命令行,在当前目录下得到demo.pcap,将这个文件下载到图形界面操作系统里,即可使用Wireshark查看了 +更多的操作可以参考: [https://www.cnblogs.com/jiujuan/p/9017495.html](https://www.cnblogs.com/jiujuan/p/9017495.html) diff --git a/doc/_content/theory/_media/img.png b/doc/_content/theory/_media/img.png new file mode 100644 index 0000000..ecf62e9 Binary files /dev/null and b/doc/_content/theory/_media/img.png differ diff --git a/doc/_content/theory/_media/img_1.png b/doc/_content/theory/_media/img_1.png new file mode 100644 index 0000000..2dc8cc8 Binary files /dev/null and b/doc/_content/theory/_media/img_1.png differ diff --git a/doc/_content/theory/_media/img_2.png b/doc/_content/theory/_media/img_2.png new file mode 100644 index 0000000..7e2ddde Binary files /dev/null and b/doc/_content/theory/_media/img_2.png differ diff --git a/doc/_content/theory/_media/img_3.png b/doc/_content/theory/_media/img_3.png new file mode 100644 index 0000000..5fc5ef4 Binary files /dev/null and b/doc/_content/theory/_media/img_3.png differ diff --git a/doc/_content/theory/_media/img_4.png b/doc/_content/theory/_media/img_4.png new file mode 100644 index 0000000..d5df7ce Binary files /dev/null and b/doc/_content/theory/_media/img_4.png differ diff --git a/doc/_content/theory/_media/img_5.png b/doc/_content/theory/_media/img_5.png new file mode 100644 index 0000000..47daffc Binary files /dev/null and b/doc/_content/theory/_media/img_5.png differ diff --git a/doc/_content/theory/_media/img_6.png b/doc/_content/theory/_media/img_6.png new file mode 100644 index 0000000..6c67ef4 Binary files /dev/null and b/doc/_content/theory/_media/img_6.png differ diff --git a/doc/_content/theory/_media/img_7.png b/doc/_content/theory/_media/img_7.png new file mode 100644 index 0000000..fc204aa Binary files /dev/null and b/doc/_content/theory/_media/img_7.png differ diff --git a/doc/_content/theory/_media/img_8.png b/doc/_content/theory/_media/img_8.png new file mode 100644 index 0000000..9b43641 Binary files /dev/null and b/doc/_content/theory/_media/img_8.png differ diff --git a/doc/_content/theory/_media/img_9.png b/doc/_content/theory/_media/img_9.png new file mode 100644 index 0000000..c3aa1ff Binary files /dev/null and b/doc/_content/theory/_media/img_9.png differ diff --git a/doc/_content/theory/broadcast_cascade.md b/doc/_content/theory/broadcast_cascade.md new file mode 100644 index 0000000..1770b11 --- /dev/null +++ b/doc/_content/theory/broadcast_cascade.md @@ -0,0 +1,47 @@ + + +# 点播流程 + +> 以下为WVP-PRO级联语音喊话流程。 + +```plantuml +@startuml +"上级平台" -> "下级平台": 1. 发起语音喊话请求 +"上级平台" <-- "下级平台": 2. 200OK +"上级平台" <- "下级平台": 3. 回复Result OK +"上级平台" --> "下级平台": 4. 200OK + +"下级平台" -> "设备": 5. 发起语音喊话请求 +"下级平台" <-- "设备": 6. 200OK +"下级平台" <- "设备": 7. 回复Result OK +"下级平台" --> "设备": 8. 200OK + +"下级平台" <- "设备": 9. invite(broadcast) +"下级平台" --> "设备": 10. 100 trying +"下级平台" --> "设备": 11. 200OK SDP +"下级平台" <-- "设备": 12. ack + +"上级平台" <- "下级平台": 13. invite(broadcast) +"上级平台" --> "下级平台": 14. 100 trying +"上级平台" --> "下级平台": 15. 200OK SDP +"上级平台" <-- "下级平台": 16. ack + +"上级平台" -> "下级平台": 17. 推送RTP +"下级平台" -> "设备": 18. 推送RTP + +@enduml +``` + +## 注册流程描述如下: + +1. 用户从网页或调用接口发起点播请求; +2. WVP-PRO向摄像机发送Invite消息,消息头域中携带 Subject字段,表明点播的视频源ID、发送方媒体流序列号、ZLMediaKit接收流使用的IP、端口号、 + 接收端媒体流序列号等参数,SDP消息体中 s字段为“Play”代表实时点播,y字段描述SSRC值,f字段描述媒体参数。 +3. 摄像机向WVP-PRO回复200OK,消息体中描述了媒体流发送者发送媒体流的IP、端口、媒体格式、SSRC字段等内容。 +4. WVP-PRO向设备回复Ack, 会话建立成功。 +5. 设备向ZLMediaKit发送实时流。 +6. ZLMediaKit向WVP-PRO发送流改变事件。 +7. WVP-PRO向WEB用户回复播放地址。 +8. ZLMediaKit向WVP发送流无人观看事件。 +9. WVP-PRO向设备回复Bye, 结束会话。 +10. 设备回复200OK,会话结束成功。 diff --git a/doc/_content/theory/code.md b/doc/_content/theory/code.md new file mode 100644 index 0000000..2d9a9be --- /dev/null +++ b/doc/_content/theory/code.md @@ -0,0 +1,29 @@ + + +# 统一编码规则 + +## D.1 编码规则 A + +>   编码规则 A 由中心编码(8位)、行业编码(2位)、类型编码(3位)和序号(7位)四个码段共20位十 +> 进制数字字符构成,即系统编码 =中心编码 + 行业编码 + 类型编码 + 序号。 +>   编码规则 A 的详细说明见表 D.1。其中,中心编码指用户或设备所归属的监控中心的编码,按照监控中心所在地的行政区划代码确定, +> 当不是基层单位时空余位为0。行政区划代码采用 GB/T2260— 2007规定的行政区划代码表示。行业编码是指用户或设备所归属的行业,行业编码对照表见 +> D.3。 +> 类型编码指定了设备或用户的具体类型,其中的前端设备包含公安系统和非公安系统的前端设备,终端用 +> 户包含公安系统和非公安系统的终端用户。 +![img_7.png](_media/img_7.png) +![img_1.png](_media/img_1.png) +![img_2.png](_media/img_2.png) + +## D.2 编码规则 B + +>   编码规则 B由中心编码(8位)、行业编码(2位)、序号(4位)和类型编码(2位)四个码段构成,即系 +> 统编码 =中心编码 + 行业编码 +序号+类型编码。编码规则 B的详细说明见表 D.2。 +![img_3.png](_media/img_3.png) +![img_4.png](_media/img_4.png) + +## D.3 行业编码对照表 + +>   行业编码对照表见表 D.3。 +![img_5.png](_media/img_5.png) +![img_6.png](_media/img_6.png) \ No newline at end of file diff --git a/doc/_content/theory/img.png b/doc/_content/theory/img.png new file mode 100644 index 0000000..9b43641 Binary files /dev/null and b/doc/_content/theory/img.png differ diff --git a/doc/_content/theory/play.md b/doc/_content/theory/play.md new file mode 100644 index 0000000..5fb75c0 --- /dev/null +++ b/doc/_content/theory/play.md @@ -0,0 +1,34 @@ + + +# 点播流程 + +> 以下为WVP-PRO点播流程。点播成功前的任何一个环节出现问题都可能出现点播超时,这也是排查点播超时的依据。 + +```plantuml +@startuml +"WEB用户" -> "WVP-PRO": 1. 发起点播请求 +"设备" <- "WVP-PRO": 2. Invite(携带SDP消息体) +"设备" --> "WVP-PRO": 3. 200OK(携带SDP消息体) +"设备" <-- "WVP-PRO": 4. Ack +"设备" -> "ZLMediaKit": 5. 发送实时流 +"WVP-PRO" <- "ZLMediaKit": 6. 流改变事件 +"WEB用户" <-- "WVP-PRO": 7. 回复流播放地址(携带流地址) +"WVP-PRO" <- "ZLMediaKit": 8. 无人观看事件 +"设备" <- "WVP-PRO": 9 Bye消息 +"设备" --> "WVP-PRO": 10 200OK +@enduml +``` + +## 注册流程描述如下: + +1. 用户从网页或调用接口发起点播请求; +2. WVP-PRO向摄像机发送Invite消息,消息头域中携带 Subject字段,表明点播的视频源ID、发送方媒体流序列号、ZLMediaKit接收流使用的IP、端口号、 + 接收端媒体流序列号等参数,SDP消息体中 s字段为“Play”代表实时点播,y字段描述SSRC值,f字段描述媒体参数。 +3. 摄像机向WVP-PRO回复200OK,消息体中描述了媒体流发送者发送媒体流的IP、端口、媒体格式、SSRC字段等内容。 +4. WVP-PRO向设备回复Ack, 会话建立成功。 +5. 设备向ZLMediaKit发送实时流。 +6. ZLMediaKit向WVP-PRO发送流改变事件。 +7. WVP-PRO向WEB用户回复播放地址。 +8. ZLMediaKit向WVP发送流无人观看事件。 +9. WVP-PRO向设备回复Bye, 结束会话。 +10. 设备回复200OK,会话结束成功。 diff --git a/doc/_content/theory/register.md b/doc/_content/theory/register.md new file mode 100644 index 0000000..2bc839a --- /dev/null +++ b/doc/_content/theory/register.md @@ -0,0 +1,21 @@ + + +# 注册流程 + +WVP-PRO目前仅支持国标中描述的基本注册流程,也是最常用的, +> 基本注册即采用IETFRFC3261规定的基于数字摘要的挑战应答式安全技术进行注册. + +```plantuml +@startuml +"设备" -> "WVP-PRO": 1. Register +"设备" <-- "WVP-PRO": 2. 401 Unauthorized +"设备" -> "WVP-PRO": 3. Register +"设备" <-- "WVP-PRO": 4. 200 OK +@enduml +``` + +> 注册流程描述如下: +> 1. 摄像机向WVP-PRO服务器发送 Register请求; +> 2. WVP-PRO向摄像机发送响应401,并在响应的消息头 WWW_Authenticate字段中给出适合摄像机的认证体制和参数; +> 3. 摄像机重新向WVP-PRO发送 Register请求,在请求的 Authorization字段给出信任书, 包含认证信息; +> 4. WVP-PRO对请求进行验证,如果检查出 摄像机身份合法,向摄像机发送成功响应 200OK,如果身份不合法则发送拒绝服务应答。 diff --git a/doc/_coverpage.md b/doc/_coverpage.md new file mode 100644 index 0000000..4f76958 --- /dev/null +++ b/doc/_coverpage.md @@ -0,0 +1,17 @@ + +![logo](_media/logo-mini.png) + +# WVP-PRO 2.0 + +> 开箱即用的28181协议视频平台。 + +- 基于GB/T28181-2016标准信令实现,兼容GB/T28181-2011。 +- 自带完整前端页面,开箱即用。 +- 完全开源,且使用MIT许可协议。可以在保留版权信息的基础上商用。 + +[GitHub](https://github.com/648540858/wvp-GB28181-pro) +[Gitee](https://gitee.com/pan648540858/wvp-GB28181-pro) + + + +[//]: # ([comment]: <> (![color](#f0f0f0))) diff --git a/doc/_media/1.png b/doc/_media/1.png new file mode 100644 index 0000000..827a925 Binary files /dev/null and b/doc/_media/1.png differ diff --git a/doc/_media/1372762149.jpg b/doc/_media/1372762149.jpg new file mode 100644 index 0000000..471ec07 Binary files /dev/null and b/doc/_media/1372762149.jpg differ diff --git a/doc/_media/2.png b/doc/_media/2.png new file mode 100644 index 0000000..d84b41c Binary files /dev/null and b/doc/_media/2.png differ diff --git a/doc/_media/3.png b/doc/_media/3.png new file mode 100644 index 0000000..318e461 Binary files /dev/null and b/doc/_media/3.png differ diff --git a/doc/_media/4.png b/doc/_media/4.png new file mode 100644 index 0000000..32ccf47 Binary files /dev/null and b/doc/_media/4.png differ diff --git a/doc/_media/5.png b/doc/_media/5.png new file mode 100644 index 0000000..30eccc3 Binary files /dev/null and b/doc/_media/5.png differ diff --git a/doc/_media/6.png b/doc/_media/6.png new file mode 100644 index 0000000..bceeea3 Binary files /dev/null and b/doc/_media/6.png differ diff --git a/doc/_media/7.png b/doc/_media/7.png new file mode 100644 index 0000000..9301e9e Binary files /dev/null and b/doc/_media/7.png differ diff --git a/doc/_media/8.png b/doc/_media/8.png new file mode 100644 index 0000000..ee98ad4 Binary files /dev/null and b/doc/_media/8.png differ diff --git a/doc/_media/903207146.jpg b/doc/_media/903207146.jpg new file mode 100644 index 0000000..0bbc7e6 Binary files /dev/null and b/doc/_media/903207146.jpg differ diff --git a/doc/_media/favicon.ico b/doc/_media/favicon.ico new file mode 100644 index 0000000..bc5f8e6 Binary files /dev/null and b/doc/_media/favicon.ico differ diff --git a/doc/_media/log.jpg b/doc/_media/log.jpg new file mode 100644 index 0000000..18c57d0 Binary files /dev/null and b/doc/_media/log.jpg differ diff --git a/doc/_media/logo-mini.png b/doc/_media/logo-mini.png new file mode 100644 index 0000000..cc8078d Binary files /dev/null and b/doc/_media/logo-mini.png differ diff --git a/doc/_media/logo.jpg b/doc/_media/logo.jpg new file mode 100644 index 0000000..bcc9fb6 Binary files /dev/null and b/doc/_media/logo.jpg differ diff --git a/doc/_media/logo.png b/doc/_media/logo.png new file mode 100644 index 0000000..c5da2d4 Binary files /dev/null and b/doc/_media/logo.png differ diff --git a/doc/_media/shequ.png b/doc/_media/shequ.png new file mode 100644 index 0000000..c5aec98 Binary files /dev/null and b/doc/_media/shequ.png differ diff --git a/doc/_media/weixin.jpg b/doc/_media/weixin.jpg new file mode 100644 index 0000000..eda1260 Binary files /dev/null and b/doc/_media/weixin.jpg differ diff --git a/doc/_media/zhifubao.jpg b/doc/_media/zhifubao.jpg new file mode 100644 index 0000000..973996b Binary files /dev/null and b/doc/_media/zhifubao.jpg differ diff --git a/doc/_navbar.md b/doc/_navbar.md new file mode 100644 index 0000000..22ac93a --- /dev/null +++ b/doc/_navbar.md @@ -0,0 +1 @@ + diff --git a/doc/_sidebar.md b/doc/_sidebar.md new file mode 100644 index 0000000..4b28de9 --- /dev/null +++ b/doc/_sidebar.md @@ -0,0 +1,32 @@ + + +* **编译与部署** + * [编译](_content/introduction/compile.md) + * [配置](_content/introduction/config.md) + * [部署](_content/introduction/deployment.md) +* **功能与使用** + * [接入设备](_content/ability/device.md) + * [国标设备](_content/ability/device_use.md) + * [推流列表](_content/ability/push.md) + * [拉流代理](_content/ability/proxy.md) + * [云端录像](_content/ability/cloud_record.md) + * [节点管理](_content/ability/node_manager.md) + * [通道管理](_content/ability/channel.md) + * [国标级联](_content/ability/cascade2.md) +* **流程与原理** + * [统一编码规则](_content/theory/code.md) + * [注册流程](_content/theory/register.md) + * [点播流程](_content/theory/play.md) + * [级联语音喊话流程](_content/theory/broadcast_cascade.md) + * [语音对讲](_content/ability/continuous_broadcast.md) +* **必备技巧** + * [抓包](_content/skill/tcpdump.md) + +* **常见问答** + - [如何反馈BUG](_content/qa/bug.md) + - [如何参与开发](_content/qa/development.md) + - [启动报错的解决办法](_content/qa/start_error.md) + - [设备注册不上来的解决办法](_content/qa/regiser_error.md) + - [点播超时/报错的解决办法](_content/qa/play_error.md) +* [**免责声明**](_content/disclaimers.md) +* [**关于本文档**](_content/about_doc.md) diff --git a/doc/index.html b/doc/index.html new file mode 100644 index 0000000..a8c1d7d --- /dev/null +++ b/doc/index.html @@ -0,0 +1,59 @@ + + + + + WVP-PRO文档 + + + + + + + + + +
加载中
+ + + + + + + + + + + + diff --git a/doc/lib/css/vue.css b/doc/lib/css/vue.css new file mode 100644 index 0000000..847f385 --- /dev/null +++ b/doc/lib/css/vue.css @@ -0,0 +1 @@ +@import url("https://fonts.googleapis.com/css?family=Roboto+Mono|Source+Sans+Pro:300,400,600");*{-webkit-font-smoothing:antialiased;-webkit-overflow-scrolling:touch;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-text-size-adjust:none;-webkit-touch-callout:none;box-sizing:border-box}body:not(.ready){overflow:hidden}body:not(.ready) .app-nav,body:not(.ready)>nav,body:not(.ready) [data-cloak]{display:none}div#app{font-size:30px;font-weight:lighter;margin:40vh auto;text-align:center}div#app:empty:before{content:"Loading..."}img.emoji{height:1.2em}img.emoji,span.emoji{vertical-align:middle}span.emoji{font-family:Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-size:1.2em}.progress{background-color:#42b983;background-color:var(--theme-color,#42b983);height:2px;left:0;position:fixed;right:0;top:0;transition:width .2s,opacity .4s;width:0;z-index:999999}.search .search-keyword,.search a:hover{color:#42b983;color:var(--theme-color,#42b983)}.search .search-keyword{font-style:normal;font-weight:700}body,html{height:100%}body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#34495e;font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:15px;letter-spacing:0;margin:0;overflow-x:hidden}img{max-width:100%}a[disabled]{cursor:not-allowed;opacity:.6}kbd{border:1px solid #ccc;border-radius:3px;display:inline-block;font-size:12px!important;line-height:12px;margin-bottom:3px;padding:3px 5px;vertical-align:middle}li input[type=checkbox]{margin:0 .2em .25em 0;vertical-align:middle}.app-nav{margin:25px 60px 0 0;position:absolute;right:0;text-align:right;z-index:10}.app-nav.no-badge{margin-right:25px}.app-nav p{margin:0}.app-nav>a{margin:0 1rem;padding:5px 0}.app-nav li,.app-nav ul{display:inline-block;list-style:none;margin:0}.app-nav a{color:inherit;font-size:16px;text-decoration:none;transition:color .3s}.app-nav a.active,.app-nav a:hover{color:#42b983;color:var(--theme-color,#42b983)}.app-nav a.active{border-bottom:2px solid #42b983;border-bottom:2px solid var(--theme-color,#42b983)}.app-nav li{display:inline-block;margin:0 1rem;padding:5px 0;position:relative;cursor:pointer}.app-nav li ul{background-color:#fff;border:1px solid;border-color:#ddd #ddd #ccc;border-radius:4px;box-sizing:border-box;display:none;max-height:calc(100vh - 61px);overflow-y:auto;padding:10px 0;position:absolute;right:-15px;text-align:left;top:100%;white-space:nowrap}.app-nav li ul li{display:block;font-size:14px;line-height:1rem;margin:8px 14px;white-space:nowrap}.app-nav li ul a{display:block;font-size:inherit;margin:0;padding:0}.app-nav li ul a.active{border-bottom:0}.app-nav li:hover ul{display:block}.github-corner{border-bottom:0;position:fixed;right:0;text-decoration:none;top:0;z-index:1}.github-corner:hover .octo-arm{animation:octocat-wave .56s ease-in-out}.github-corner svg{color:#fff;fill:#42b983;fill:var(--theme-color,#42b983);height:80px;width:80px}main{display:block;position:relative;width:100vw;height:100%;z-index:0}main.hidden{display:none}.anchor{display:inline-block;text-decoration:none;transition:all .3s}.anchor span{color:#34495e}.anchor:hover{text-decoration:underline}.sidebar{border-right:1px solid rgba(0,0,0,.07);overflow-y:auto;padding:40px 0 0;position:absolute;top:0;bottom:0;left:0;transition:transform .25s ease-out;width:300px;z-index:20}.sidebar>h1{margin:0 auto 1rem;font-size:1.5rem;font-weight:300;text-align:center}.sidebar>h1 a{color:inherit;text-decoration:none}.sidebar>h1 .app-nav{display:block;position:static}.sidebar .sidebar-nav{line-height:2em;padding-bottom:40px}.sidebar li.collapse .app-sub-sidebar{display:none}.sidebar ul{margin:0 0 0 15px;padding:0}.sidebar li>p{font-weight:700;margin:0}.sidebar ul,.sidebar ul li{list-style:none}.sidebar ul li a{border-bottom:none;display:block}.sidebar ul li ul{padding-left:20px}.sidebar::-webkit-scrollbar{width:4px}.sidebar::-webkit-scrollbar-thumb{background:transparent;border-radius:4px}.sidebar:hover::-webkit-scrollbar-thumb{background:hsla(0,0%,53.3%,.4)}.sidebar:hover::-webkit-scrollbar-track{background:hsla(0,0%,53.3%,.1)}.sidebar-toggle{background-color:transparent;background-color:hsla(0,0%,100%,.8);border:0;outline:none;padding:10px;position:absolute;bottom:0;left:0;text-align:center;transition:opacity .3s;width:284px;z-index:30;cursor:pointer}.sidebar-toggle:hover .sidebar-toggle-button{opacity:.4}.sidebar-toggle span{background-color:#42b983;background-color:var(--theme-color,#42b983);display:block;margin-bottom:4px;width:16px;height:2px}body.sticky .sidebar,body.sticky .sidebar-toggle{position:fixed}.content{padding-top:60px;position:absolute;top:0;right:0;bottom:0;left:300px;transition:left .25s ease}.markdown-section{margin:0 auto;max-width:80%;padding:30px 15px 40px;position:relative}.markdown-section>*{box-sizing:border-box;font-size:inherit}.markdown-section>:first-child{margin-top:0!important}.markdown-section hr{border:none;border-bottom:1px solid #eee;margin:2em 0}.markdown-section iframe{border:1px solid #eee;width:1px;min-width:100%}.markdown-section table{border-collapse:collapse;border-spacing:0;display:block;margin-bottom:1rem;overflow:auto;width:100%}.markdown-section th{font-weight:700}.markdown-section td,.markdown-section th{border:1px solid #ddd;padding:6px 13px}.markdown-section tr{border-top:1px solid #ccc}.markdown-section p.tip,.markdown-section tr:nth-child(2n){background-color:#f8f8f8}.markdown-section p.tip{border-bottom-right-radius:2px;border-left:4px solid #f66;border-top-right-radius:2px;margin:2em 0;padding:12px 24px 12px 30px;position:relative}.markdown-section p.tip:before{background-color:#f66;border-radius:100%;color:#fff;content:"!";font-family:Dosis,Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:14px;font-weight:700;left:-12px;line-height:20px;position:absolute;height:20px;width:20px;text-align:center;top:14px}.markdown-section p.tip code{background-color:#efefef}.markdown-section p.tip em{color:#34495e}.markdown-section p.warn{background:rgba(66,185,131,.1);border-radius:2px;padding:1rem}.markdown-section ul.task-list>li{list-style-type:none}body.close .sidebar{transform:translateX(-300px)}body.close .sidebar-toggle{width:auto}body.close .content{left:0}@media print{.app-nav,.github-corner,.sidebar,.sidebar-toggle{display:none}}@media screen and (max-width:768px){.github-corner,.sidebar,.sidebar-toggle{position:fixed}.app-nav{margin-top:16px}.app-nav li ul{top:30px}main{height:auto;min-height:100vh;overflow-x:hidden}.sidebar{left:-300px;transition:transform .25s ease-out}.content{left:0;max-width:100vw;position:static;padding-top:20px;transition:transform .25s ease}.app-nav,.github-corner{transition:transform .25s ease-out}.sidebar-toggle{background-color:transparent;width:auto;padding:30px 30px 10px 10px}body.close .sidebar{transform:translateX(300px)}body.close .sidebar-toggle{background-color:hsla(0,0%,100%,.8);transition:background-color 1s;width:284px;padding:10px}body.close .content{transform:translateX(300px)}body.close .app-nav,body.close .github-corner{display:none}.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave .56s ease-in-out}}@keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}section.cover{position:relative;align-items:center;background-position:50%;background-repeat:no-repeat;background-size:cover;min-height:100vh;width:100%;display:none}section.cover.show{display:flex}section.cover.has-mask .mask{background-color:#fff;opacity:.8;position:absolute;top:0;bottom:0;width:100%}section.cover .cover-main{flex:1;margin:0 16px;text-align:center;position:relative}section.cover a{color:inherit}section.cover a,section.cover a:hover{text-decoration:none}section.cover p{line-height:1.5rem;margin:1em 0}section.cover h1{color:inherit;font-size:2.5rem;font-weight:300;margin:.625rem 0 2.5rem;position:relative;text-align:center}section.cover h1 a{display:block}section.cover h1 small{bottom:-.4375rem;font-size:1rem;position:absolute}section.cover blockquote{font-size:1.5rem;text-align:center}section.cover ul{line-height:1.8;list-style-type:none;margin:1em auto;max-width:500px;padding:0}section.cover .cover-main>p:last-child a{border-radius:2rem;border:1px solid #42b983;border-color:var(--theme-color,#42b983);box-sizing:border-box;color:#42b983;color:var(--theme-color,#42b983);display:inline-block;font-size:1.05rem;letter-spacing:.1rem;margin:.5rem 1rem;padding:.75em 2rem;text-decoration:none;transition:all .15s ease}section.cover .cover-main>p:last-child a:last-child{background-color:#42b983;background-color:var(--theme-color,#42b983);color:#fff}section.cover .cover-main>p:last-child a:last-child:hover{color:inherit;opacity:.8}section.cover .cover-main>p:last-child a:hover{color:inherit}section.cover blockquote>p>a{border-bottom:2px solid #42b983;border-bottom:2px solid var(--theme-color,#42b983);transition:color .3s}section.cover blockquote>p>a:hover{color:#42b983;color:var(--theme-color,#42b983)}.sidebar,body{background-color:#fff}.sidebar{color:#364149}.sidebar li{margin:6px 0}.sidebar ul li a{color:#505d6b;font-size:14px;font-weight:400;overflow:hidden;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.sidebar ul li a:hover{text-decoration:underline}.sidebar ul li ul{padding:0}.sidebar ul li.active>a{border-right:2px solid;color:#42b983;color:var(--theme-color,#42b983);font-weight:600}.app-sub-sidebar li:before{content:"-";padding-right:4px;float:left}.markdown-section h1,.markdown-section h2,.markdown-section h3,.markdown-section h4,.markdown-section strong{color:#2c3e50;font-weight:600}.markdown-section a{color:#42b983;color:var(--theme-color,#42b983);font-weight:600}.markdown-section h1{font-size:2rem;margin:0 0 1rem}.markdown-section h2{font-size:1.75rem;margin:45px 0 .8rem}.markdown-section h3{font-size:1.5rem;margin:40px 0 .6rem}.markdown-section h4{font-size:1.25rem}.markdown-section h5{font-size:1rem}.markdown-section h6{color:#777;font-size:1rem}.markdown-section figure,.markdown-section p{margin:1.2em 0}.markdown-section ol,.markdown-section p,.markdown-section ul{line-height:1.6rem;word-spacing:.05rem}.markdown-section ol,.markdown-section ul{padding-left:1.5rem}.markdown-section blockquote{border-left:4px solid #42b983;border-left:4px solid var(--theme-color,#42b983);color:#858585;margin:2em 0;padding-left:20px}.markdown-section blockquote p{font-weight:600;margin-left:0}.markdown-section iframe{margin:1em 0}.markdown-section em{color:#7f8c8d}.markdown-section code,.markdown-section output:after,.markdown-section pre{font-family:Roboto Mono,Monaco,courier,monospace}.markdown-section code,.markdown-section pre{background-color:#f8f8f8}.markdown-section output,.markdown-section pre{margin:1.2em 0;position:relative}.markdown-section output,.markdown-section pre>code{border-radius:2px;display:block}.markdown-section output:after,.markdown-section pre>code{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial}.markdown-section code{border-radius:2px;color:#e96900;margin:0 2px;padding:3px 5px;white-space:pre-wrap}.markdown-section>:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) code{font-size:.8rem}.markdown-section pre{padding:0 1.4rem;line-height:1.5rem;overflow:auto;word-wrap:normal}.markdown-section pre>code{color:#525252;font-size:.8rem;padding:2.2em 5px;line-height:inherit;margin:0 2px;max-width:inherit;overflow:inherit;white-space:inherit}.markdown-section output{padding:1.7rem 1.4rem;border:1px dotted #ccc}.markdown-section output>:first-child{margin-top:0}.markdown-section output>:last-child{margin-bottom:0}.markdown-section code:after,.markdown-section code:before,.markdown-section output:after,.markdown-section output:before{letter-spacing:.05rem}.markdown-section output:after,.markdown-section pre:after{color:#ccc;font-size:.6rem;font-weight:600;height:15px;line-height:15px;padding:5px 10px 0;position:absolute;right:0;text-align:right;top:0;content:attr(data-lang)}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8e908c}.token.namespace{opacity:.7}.token.boolean,.token.number{color:#c76b29}.token.punctuation{color:#525252}.token.property{color:#c08b30}.token.tag{color:#2973b7}.token.string{color:#42b983;color:var(--theme-color,#42b983)}.token.selector{color:#6679cc}.token.attr-name{color:#2973b7}.language-css .token.string,.style .token.string,.token.entity,.token.url{color:#22a2c9}.token.attr-value,.token.control,.token.directive,.token.unit{color:#42b983;color:var(--theme-color,#42b983)}.token.function,.token.keyword{color:#e96900}.token.atrule,.token.regex,.token.statement{color:#22a2c9}.token.placeholder,.token.variable{color:#3d8fd1}.token.deleted{text-decoration:line-through}.token.inserted{border-bottom:1px dotted #202746;text-decoration:none}.token.italic{font-style:italic}.token.bold,.token.important{font-weight:700}.token.important{color:#c94922}.token.entity{cursor:help}code .token{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;min-height:1.5rem;position:relative;left:auto} \ No newline at end of file diff --git a/doc/lib/js/docsify-copy-code.min.js b/doc/lib/js/docsify-copy-code.min.js new file mode 100644 index 0000000..9766a78 --- /dev/null +++ b/doc/lib/js/docsify-copy-code.min.js @@ -0,0 +1,9 @@ +/*! + * docsify-copy-code + * v2.1.1 + * https://github.com/jperasmus/docsify-copy-code + * (c) 2017-2020 JP Erasmus + * MIT license + */ +!function(){"use strict";function s(o){return(s="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(o){return typeof o}:function(o){return o&&"function"==typeof Symbol&&o.constructor===Symbol&&o!==Symbol.prototype?"symbol":typeof o})(o)}!function(o,e){void 0===e&&(e={});var t=e.insertAt;if(o&&"undefined"!=typeof document){var n=document.head||document.getElementsByTagName("head")[0],c=document.createElement("style");c.type="text/css","top"===t&&n.firstChild?n.insertBefore(c,n.firstChild):n.appendChild(c),c.styleSheet?c.styleSheet.cssText=o:c.appendChild(document.createTextNode(o))}}(".docsify-copy-code-button,.docsify-copy-code-button span{cursor:pointer;transition:all .25s ease}.docsify-copy-code-button{position:absolute;z-index:1;top:0;right:0;overflow:visible;padding:.65em .8em;border:0;border-radius:0;outline:0;font-size:1em;background:grey;background:var(--theme-color,grey);color:#fff;opacity:0}.docsify-copy-code-button span{border-radius:3px;background:inherit;pointer-events:none}.docsify-copy-code-button .error,.docsify-copy-code-button .success{position:absolute;z-index:-100;top:50%;right:0;padding:.5em .65em;font-size:.825em;opacity:0;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.docsify-copy-code-button.error .error,.docsify-copy-code-button.success .success{right:100%;opacity:1;-webkit-transform:translate(-115%,-50%);transform:translate(-115%,-50%)}.docsify-copy-code-button:focus,pre:hover .docsify-copy-code-button{opacity:1}"),document.querySelector('link[href*="docsify-copy-code"]')&&console.warn("[Deprecation] Link to external docsify-copy-code stylesheet is no longer necessary."),window.DocsifyCopyCodePlugin={init:function(){return function(o,e){o.ready(function(){console.warn("[Deprecation] Manually initializing docsify-copy-code using window.DocsifyCopyCodePlugin.init() is no longer necessary.")})}}},window.$docsify=window.$docsify||{},window.$docsify.plugins=[function(o,r){o.doneEach(function(){var o=Array.apply(null,document.querySelectorAll("pre[data-lang]")),c={buttonText:"Copy to clipboard",errorText:"Error",successText:"Copied"};r.config.copyCode&&Object.keys(c).forEach(function(t){var n=r.config.copyCode[t];"string"==typeof n?c[t]=n:"object"===s(n)&&Object.keys(n).some(function(o){var e=-1',''.concat(c.buttonText,""),''.concat(c.errorText,""),''.concat(c.successText,""),""].join("");o.forEach(function(o){o.insertAdjacentHTML("beforeend",e)})}),o.mounted(function(){document.querySelector(".content").addEventListener("click",function(o){if(o.target.classList.contains("docsify-copy-code-button")){var e="BUTTON"===o.target.tagName?o.target:o.target.parentNode,t=document.createRange(),n=e.parentNode.querySelector("code"),c=window.getSelection();t.selectNode(n),c.removeAllRanges(),c.addRange(t);try{document.execCommand("copy")&&(e.classList.add("success"),setTimeout(function(){e.classList.remove("success")},1e3))}catch(o){console.error("docsify-copy-code: ".concat(o)),e.classList.add("error"),setTimeout(function(){e.classList.remove("error")},1e3)}"function"==typeof(c=window.getSelection()).removeRange?c.removeRange(t):"function"==typeof c.removeAllRanges&&c.removeAllRanges()}})})}].concat(window.$docsify.plugins||[])}(); +//# sourceMappingURL=docsify-copy-code.min.js.map diff --git a/doc/lib/js/docsify-plantuml.min.js b/doc/lib/js/docsify-plantuml.min.js new file mode 100644 index 0000000..1690767 --- /dev/null +++ b/doc/lib/js/docsify-plantuml.min.js @@ -0,0 +1 @@ +!function(t){function e(a){if(n[a])return n[a].exports;var r=n[a]={i:a,l:!1,exports:{}};return t[a].call(r.exports,r,r.exports,e),r.l=!0,r.exports}var n={};e.m=t,e.c=n,e.d=function(t,n,a){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:a})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=0)}([function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var a=n(1);window.$docsify||(window.$docsify={}),window.$docsify.plugins=(window.$docsify.plugins||[]).concat(a.a)},function(t,e,n){"use strict";function a(t,e){var t=Object(o.a)(e.skin)+t,n=e.serverPath||"//www.plantuml.com/plantuml/svg/";if(e.renderSvgAsObject){var a=n+Object(l.encode)(r(t));return''}var a=n+Object(l.encode)(t);return''}function r(t){function e(t,e){for(var n=(a+e).split("/"),r=[],i=0,s=n.length;i>2,i=(3&t)<<4|e>>4,s=(15&e)<<2|n>>6,o=63&n,l="";return l+=a(63&r),l+=a(63&i),l+=a(63&s),l+=a(63&o)}e.exports=function(t){for(var e="",n=0;n0?e.windowBits=-e.windowBits:e.gzip&&e.windowBits>0&&e.windowBits<16&&(e.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new f,this.strm.avail_out=0;var n=o.deflateInit2(this.strm,e.level,e.method,e.windowBits,e.memLevel,e.strategy);if(n!==c)throw new Error(d[n]);if(e.header&&o.deflateSetHeader(this.strm,e.header),e.dictionary){var r;if(r="string"==typeof e.dictionary?h.string2buf(e.dictionary):"[object ArrayBuffer]"===u.call(e.dictionary)?new Uint8Array(e.dictionary):e.dictionary,(n=o.deflateSetDictionary(this.strm,r))!==c)throw new Error(d[n]);this._dict_set=!0}}function r(t,e){var n=new a(e);if(n.push(t,!0),n.err)throw n.msg||d[n.err];return n.result}function i(t,e){return e=e||{},e.raw=!0,r(t,e)}function s(t,e){return e=e||{},e.gzip=!0,r(t,e)}var o=t("./zlib/deflate"),l=t("./utils/common"),h=t("./utils/strings"),d=t("./zlib/messages"),f=t("./zlib/zstream"),u=Object.prototype.toString,c=0,_=-1,g=0,p=8;a.prototype.push=function(t,e){var n,a,r=this.strm,i=this.options.chunkSize;if(this.ended)return!1;a=e===~~e?e:!0===e?4:0,"string"==typeof t?r.input=h.string2buf(t):"[object ArrayBuffer]"===u.call(t)?r.input=new Uint8Array(t):r.input=t,r.next_in=0,r.avail_in=r.input.length;do{if(0===r.avail_out&&(r.output=new l.Buf8(i),r.next_out=0,r.avail_out=i),1!==(n=o.deflate(r,a))&&n!==c)return this.onEnd(n),this.ended=!0,!1;0!==r.avail_out&&(0!==r.avail_in||4!==a&&2!==a)||("string"===this.options.to?this.onData(h.buf2binstring(l.shrinkBuf(r.output,r.next_out))):this.onData(l.shrinkBuf(r.output,r.next_out)))}while((r.avail_in>0||0===r.avail_out)&&1!==n);return 4===a?(n=o.deflateEnd(this.strm),this.onEnd(n),this.ended=!0,n===c):2!==a||(this.onEnd(c),r.avail_out=0,!0)},a.prototype.onData=function(t){this.chunks.push(t)},a.prototype.onEnd=function(t){t===c&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=l.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg},n.Deflate=a,n.deflate=r,n.deflateRaw=i,n.gzip=s},{"./utils/common":5,"./utils/strings":6,"./zlib/deflate":9,"./zlib/messages":10,"./zlib/zstream":12}],5:[function(t,e,n){"use strict";function a(t,e){return Object.prototype.hasOwnProperty.call(t,e)}var r="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Int32Array;n.assign=function(t){for(var e=Array.prototype.slice.call(arguments,1);e.length;){var n=e.shift();if(n){if("object"!=typeof n)throw new TypeError(n+"must be non-object");for(var r in n)a(n,r)&&(t[r]=n[r])}}return t},n.shrinkBuf=function(t,e){return t.length===e?t:t.subarray?t.subarray(0,e):(t.length=e,t)};var i={arraySet:function(t,e,n,a,r){if(e.subarray&&t.subarray)return void t.set(e.subarray(n,n+a),r);for(var i=0;i=252?6:l>=248?5:l>=240?4:l>=224?3:l>=192?2:1;o[254]=o[254]=1,n.string2buf=function(t){var e,n,a,i,s,o=t.length,l=0;for(i=0;i>>6,e[s++]=128|63&n):n<65536?(e[s++]=224|n>>>12,e[s++]=128|n>>>6&63,e[s++]=128|63&n):(e[s++]=240|n>>>18,e[s++]=128|n>>>12&63,e[s++]=128|n>>>6&63,e[s++]=128|63&n);return e},n.buf2binstring=function(t){return a(t,t.length)},n.binstring2buf=function(t){for(var e=new r.Buf8(t.length),n=0,a=e.length;n4)h[r++]=65533,n+=s-1;else{for(i&=2===s?31:3===s?15:7;s>1&&n1?h[r++]=65533:i<65536?h[r++]=i:(i-=65536,h[r++]=55296|i>>10&1023,h[r++]=56320|1023&i)}return a(h,r)},n.utf8border=function(t,e){var n;for(e=e||t.length,e>t.length&&(e=t.length),n=e-1;n>=0&&128==(192&t[n]);)n--;return n<0?e:0===n?e:n+o[t[n]]>e?n:e}},{"./common":5}],7:[function(t,e,n){"use strict";function a(t,e,n,a){for(var r=65535&t|0,i=t>>>16&65535|0,s=0;0!==n;){s=n>2e3?2e3:n,n-=s;do{r=r+e[a++]|0,i=i+r|0}while(--s);r%=65521,i%=65521}return r|i<<16|0}e.exports=a},{}],8:[function(t,e,n){"use strict";function a(t,e,n,a){var i=r,s=a+n;t^=-1;for(var o=a;o>>8^i[255&(t^e[o])];return-1^t}var r=function(){for(var t,e=[],n=0;n<256;n++){t=n;for(var a=0;a<8;a++)t=1&t?3988292384^t>>>1:t>>>1;e[n]=t}return e}();e.exports=a},{}],9:[function(t,e,n){"use strict";function a(t,e){return t.msg=D[e],e}function r(t){return(t<<1)-(t>4?9:0)}function i(t){for(var e=t.length;--e>=0;)t[e]=0}function s(t){var e=t.state,n=e.pending;n>t.avail_out&&(n=t.avail_out),0!==n&&(O.arraySet(t.output,e.pending_buf,e.pending_out,n,t.next_out),t.next_out+=n,e.pending_out+=n,t.total_out+=n,t.avail_out-=n,e.pending-=n,0===e.pending&&(e.pending_out=0))}function o(t,e){Z._tr_flush_block(t,t.block_start>=0?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,s(t.strm)}function l(t,e){t.pending_buf[t.pending++]=e}function h(t,e){t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e}function d(t,e,n,a){var r=t.avail_in;return r>a&&(r=a),0===r?0:(t.avail_in-=r,O.arraySet(e,t.input,t.next_in,r,n),1===t.state.wrap?t.adler=N(t.adler,e,r,n):2===t.state.wrap&&(t.adler=R(t.adler,e,r,n)),t.next_in+=r,t.total_in+=r,r)}function f(t,e){var n,a,r=t.max_chain_length,i=t.strstart,s=t.prev_length,o=t.nice_match,l=t.strstart>t.w_size-ht?t.strstart-(t.w_size-ht):0,h=t.window,d=t.w_mask,f=t.prev,u=t.strstart+lt,c=h[i+s-1],_=h[i+s];t.prev_length>=t.good_match&&(r>>=2),o>t.lookahead&&(o=t.lookahead);do{if(n=e,h[n+s]===_&&h[n+s-1]===c&&h[n]===h[i]&&h[++n]===h[i+1]){i+=2,n++;do{}while(h[++i]===h[++n]&&h[++i]===h[++n]&&h[++i]===h[++n]&&h[++i]===h[++n]&&h[++i]===h[++n]&&h[++i]===h[++n]&&h[++i]===h[++n]&&h[++i]===h[++n]&&is){if(t.match_start=e,s=a,a>=o)break;c=h[i+s-1],_=h[i+s]}}}while((e=f[e&d])>l&&0!=--r);return s<=t.lookahead?s:t.lookahead}function u(t){var e,n,a,r,i,s=t.w_size;do{if(r=t.window_size-t.lookahead-t.strstart,t.strstart>=s+(s-ht)){O.arraySet(t.window,t.window,s,s,0),t.match_start-=s,t.strstart-=s,t.block_start-=s,n=t.hash_size,e=n;do{a=t.head[--e],t.head[e]=a>=s?a-s:0}while(--n);n=s,e=n;do{a=t.prev[--e],t.prev[e]=a>=s?a-s:0}while(--n);r+=s}if(0===t.strm.avail_in)break;if(n=d(t.strm,t.window,t.strstart+t.lookahead,r),t.lookahead+=n,t.lookahead+t.insert>=ot)for(i=t.strstart-t.insert,t.ins_h=t.window[i],t.ins_h=(t.ins_h<t.pending_buf_size-5&&(n=t.pending_buf_size-5);;){if(t.lookahead<=1){if(u(t),0===t.lookahead&&e===I)return bt;if(0===t.lookahead)break}t.strstart+=t.lookahead,t.lookahead=0;var a=t.block_start+n;if((0===t.strstart||t.strstart>=a)&&(t.lookahead=t.strstart-a,t.strstart=a,o(t,!1),0===t.strm.avail_out))return bt;if(t.strstart-t.block_start>=t.w_size-ht&&(o(t,!1),0===t.strm.avail_out))return bt}return t.insert=0,e===j?(o(t,!0),0===t.strm.avail_out?vt:kt):(t.strstart>t.block_start&&(o(t,!1),t.strm.avail_out),bt)}function _(t,e){for(var n,a;;){if(t.lookahead=ot&&(t.ins_h=(t.ins_h<=ot)if(a=Z._tr_tally(t,t.strstart-t.match_start,t.match_length-ot),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=ot){t.match_length--;do{t.strstart++,t.ins_h=(t.ins_h<=ot&&(t.ins_h=(t.ins_h<4096)&&(t.match_length=ot-1)),t.prev_length>=ot&&t.match_length<=t.prev_length){r=t.strstart+t.lookahead-ot,a=Z._tr_tally(t,t.strstart-1-t.prev_match,t.prev_length-ot),t.lookahead-=t.prev_length-1,t.prev_length-=2;do{++t.strstart<=r&&(t.ins_h=(t.ins_h<=ot&&t.strstart>0&&(r=t.strstart-1,(a=s[r])===s[++r]&&a===s[++r]&&a===s[++r])){i=t.strstart+lt;do{}while(a===s[++r]&&a===s[++r]&&a===s[++r]&&a===s[++r]&&a===s[++r]&&a===s[++r]&&a===s[++r]&&a===s[++r]&&rt.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=ot?(n=Z._tr_tally(t,1,t.match_length-ot),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(n=Z._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),n&&(o(t,!1),0===t.strm.avail_out))return bt}return t.insert=0,e===j?(o(t,!0),0===t.strm.avail_out?vt:kt):t.last_lit&&(o(t,!1),0===t.strm.avail_out)?bt:wt}function m(t,e){for(var n;;){if(0===t.lookahead&&(u(t),0===t.lookahead)){if(e===I)return bt;break}if(t.match_length=0,n=Z._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,n&&(o(t,!1),0===t.strm.avail_out))return bt}return t.insert=0,e===j?(o(t,!0),0===t.strm.avail_out?vt:kt):t.last_lit&&(o(t,!1),0===t.strm.avail_out)?bt:wt}function b(t,e,n,a,r){this.good_length=t,this.max_lazy=e,this.nice_length=n,this.max_chain=a,this.func=r}function w(t){t.window_size=2*t.w_size,i(t.head),t.max_lazy_match=E[t.level].max_lazy,t.good_match=E[t.level].good_length,t.nice_match=E[t.level].nice_length,t.max_chain_length=E[t.level].max_chain,t.strstart=0,t.block_start=0,t.lookahead=0,t.insert=0,t.match_length=t.prev_length=ot-1,t.match_available=0,t.ins_h=0}function v(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=Q,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new O.Buf16(2*it),this.dyn_dtree=new O.Buf16(2*(2*at+1)),this.bl_tree=new O.Buf16(2*(2*rt+1)),i(this.dyn_ltree),i(this.dyn_dtree),i(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new O.Buf16(st+1),this.heap=new O.Buf16(2*nt+1),i(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new O.Buf16(2*nt+1),i(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}function k(t){var e;return t&&t.state?(t.total_in=t.total_out=0,t.data_type=J,e=t.state,e.pending=0,e.pending_out=0,e.wrap<0&&(e.wrap=-e.wrap),e.status=e.wrap?ft:pt,t.adler=2===e.wrap?0:1,e.last_flush=I,Z._tr_init(e),L):a(t,M)}function y(t){var e=k(t);return e===L&&w(t.state),e}function x(t,e){return t&&t.state?2!==t.state.wrap?M:(t.state.gzhead=e,L):M}function z(t,e,n,r,i,s){if(!t)return M;var o=1;if(e===K&&(e=6),r<0?(o=0,r=-r):r>15&&(o=2,r-=16),i<1||i>V||n!==Q||r<8||r>15||e<0||e>9||s<0||s>G)return a(t,M);8===r&&(r=9);var l=new v;return t.state=l,l.strm=t,l.wrap=o,l.gzhead=null,l.w_bits=r,l.w_size=1<F||e<0)return t?a(t,M):M;if(o=t.state,!t.output||!t.input&&0!==t.avail_in||o.status===mt&&e!==j)return a(t,0===t.avail_out?W:M);if(o.strm=t,n=o.last_flush,o.last_flush=e,o.status===ft)if(2===o.wrap)t.adler=0,l(o,31),l(o,139),l(o,8),o.gzhead?(l(o,(o.gzhead.text?1:0)+(o.gzhead.hcrc?2:0)+(o.gzhead.extra?4:0)+(o.gzhead.name?8:0)+(o.gzhead.comment?16:0)),l(o,255&o.gzhead.time),l(o,o.gzhead.time>>8&255),l(o,o.gzhead.time>>16&255),l(o,o.gzhead.time>>24&255),l(o,9===o.level?2:o.strategy>=$||o.level<2?4:0),l(o,255&o.gzhead.os),o.gzhead.extra&&o.gzhead.extra.length&&(l(o,255&o.gzhead.extra.length),l(o,o.gzhead.extra.length>>8&255)),o.gzhead.hcrc&&(t.adler=R(t.adler,o.pending_buf,o.pending,0)),o.gzindex=0,o.status=ut):(l(o,0),l(o,0),l(o,0),l(o,0),l(o,0),l(o,9===o.level?2:o.strategy>=$||o.level<2?4:0),l(o,yt),o.status=pt);else{var u=Q+(o.w_bits-8<<4)<<8,c=-1;c=o.strategy>=$||o.level<2?0:o.level<6?1:6===o.level?2:3,u|=c<<6,0!==o.strstart&&(u|=dt),u+=31-u%31,o.status=pt,h(o,u),0!==o.strstart&&(h(o,t.adler>>>16),h(o,65535&t.adler)),t.adler=1}if(o.status===ut)if(o.gzhead.extra){for(d=o.pending;o.gzindex<(65535&o.gzhead.extra.length)&&(o.pending!==o.pending_buf_size||(o.gzhead.hcrc&&o.pending>d&&(t.adler=R(t.adler,o.pending_buf,o.pending-d,d)),s(t),d=o.pending,o.pending!==o.pending_buf_size));)l(o,255&o.gzhead.extra[o.gzindex]),o.gzindex++;o.gzhead.hcrc&&o.pending>d&&(t.adler=R(t.adler,o.pending_buf,o.pending-d,d)),o.gzindex===o.gzhead.extra.length&&(o.gzindex=0,o.status=ct)}else o.status=ct;if(o.status===ct)if(o.gzhead.name){d=o.pending;do{if(o.pending===o.pending_buf_size&&(o.gzhead.hcrc&&o.pending>d&&(t.adler=R(t.adler,o.pending_buf,o.pending-d,d)),s(t),d=o.pending,o.pending===o.pending_buf_size)){f=1;break}f=o.gzindexd&&(t.adler=R(t.adler,o.pending_buf,o.pending-d,d)),0===f&&(o.gzindex=0,o.status=_t)}else o.status=_t;if(o.status===_t)if(o.gzhead.comment){d=o.pending;do{if(o.pending===o.pending_buf_size&&(o.gzhead.hcrc&&o.pending>d&&(t.adler=R(t.adler,o.pending_buf,o.pending-d,d)),s(t),d=o.pending,o.pending===o.pending_buf_size)){f=1;break}f=o.gzindexd&&(t.adler=R(t.adler,o.pending_buf,o.pending-d,d)),0===f&&(o.status=gt)}else o.status=gt;if(o.status===gt&&(o.gzhead.hcrc?(o.pending+2>o.pending_buf_size&&s(t),o.pending+2<=o.pending_buf_size&&(l(o,255&t.adler),l(o,t.adler>>8&255),t.adler=0,o.status=pt)):o.status=pt),0!==o.pending){if(s(t),0===t.avail_out)return o.last_flush=-1,L}else if(0===t.avail_in&&r(e)<=r(n)&&e!==j)return a(t,W);if(o.status===mt&&0!==t.avail_in)return a(t,W);if(0!==t.avail_in||0!==o.lookahead||e!==I&&o.status!==mt){var _=o.strategy===$?m(o,e):o.strategy===q?p(o,e):E[o.level].func(o,e);if(_!==vt&&_!==kt||(o.status=mt),_===bt||_===vt)return 0===t.avail_out&&(o.last_flush=-1),L;if(_===wt&&(e===U?Z._tr_align(o):e!==F&&(Z._tr_stored_block(o,0,0,!1),e===T&&(i(o.head),0===o.lookahead&&(o.strstart=0,o.block_start=0,o.insert=0))),s(t),0===t.avail_out))return o.last_flush=-1,L}return e!==j?L:o.wrap<=0?H:(2===o.wrap?(l(o,255&t.adler),l(o,t.adler>>8&255),l(o,t.adler>>16&255),l(o,t.adler>>24&255),l(o,255&t.total_in),l(o,t.total_in>>8&255),l(o,t.total_in>>16&255),l(o,t.total_in>>24&255)):(h(o,t.adler>>>16),h(o,65535&t.adler)),s(t),o.wrap>0&&(o.wrap=-o.wrap),0!==o.pending?L:H)}function A(t){var e;return t&&t.state?(e=t.state.status)!==ft&&e!==ut&&e!==ct&&e!==_t&&e!==gt&&e!==pt&&e!==mt?a(t,M):(t.state=null,e===pt?a(t,P):L):M}function S(t,e){var n,a,r,s,o,l,h,d,f=e.length;if(!t||!t.state)return M;if(n=t.state,2===(s=n.wrap)||1===s&&n.status!==ft||n.lookahead)return M;for(1===s&&(t.adler=N(t.adler,e,f,0)),n.wrap=0,f>=n.w_size&&(0===s&&(i(n.head),n.strstart=0,n.block_start=0,n.insert=0),d=new O.Buf8(n.w_size),O.arraySet(d,e,f-n.w_size,n.w_size,0),e=d,f=n.w_size),o=t.avail_in,l=t.next_in,h=t.input,t.avail_in=f,t.next_in=0,t.input=e,u(n);n.lookahead>=ot;){a=n.strstart,r=n.lookahead-(ot-1);do{n.ins_h=(n.ins_h<=0;)t[e]=0}function r(t,e,n,a,r){this.static_tree=t,this.extra_bits=e,this.extra_base=n,this.elems=a,this.max_length=r,this.has_stree=t&&t.length}function i(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}function s(t){return t<256?it[t]:it[256+(t>>>7)]}function o(t,e){t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255}function l(t,e,n){t.bi_valid>$-n?(t.bi_buf|=e<>$-t.bi_valid,t.bi_valid+=n-$):(t.bi_buf|=e<>>=1,n<<=1}while(--e>0);return n>>>1}function f(t){16===t.bi_valid?(o(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):t.bi_valid>=8&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)}function u(t,e){var n,a,r,i,s,o,l=e.dyn_tree,h=e.max_code,d=e.stat_desc.static_tree,f=e.stat_desc.has_stree,u=e.stat_desc.extra_bits,c=e.stat_desc.extra_base,_=e.stat_desc.max_length,g=0;for(i=0;i<=Y;i++)t.bl_count[i]=0;for(l[2*t.heap[t.heap_max]+1]=0,n=t.heap_max+1;n_&&(i=_,g++),l[2*a+1]=i,a>h||(t.bl_count[i]++,s=0,a>=c&&(s=u[a-c]),o=l[2*a],t.opt_len+=o*(i+s),f&&(t.static_len+=o*(d[2*a+1]+s)));if(0!==g){do{for(i=_-1;0===t.bl_count[i];)i--;t.bl_count[i]--,t.bl_count[i+1]+=2,t.bl_count[_]--,g-=2}while(g>0);for(i=_;0!==i;i--)for(a=t.bl_count[i];0!==a;)(r=t.heap[--n])>h||(l[2*r+1]!==i&&(t.opt_len+=(i-l[2*r+1])*l[2*r],l[2*r+1]=i),a--)}}function c(t,e,n){var a,r,i=new Array(Y+1),s=0;for(a=1;a<=Y;a++)i[a]=s=s+n[a-1]<<1;for(r=0;r<=e;r++){var o=t[2*r+1];0!==o&&(t[2*r]=d(i[o]++,o))}}function _(){var t,e,n,a,i,s=new Array(Y+1);for(n=0,a=0;a>=7;a8?o(t,t.bi_buf):t.bi_valid>0&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0}function m(t,e,n,a){p(t),a&&(o(t,n),o(t,~n)),N.arraySet(t.pending_buf,t.window,e,n,t.pending),t.pending+=n}function b(t,e,n,a){var r=2*e,i=2*n;return t[r]>1;n>=1;n--)w(t,i,n);r=l;do{n=t.heap[1],t.heap[1]=t.heap[t.heap_len--],w(t,i,1),a=t.heap[1],t.heap[--t.heap_max]=n,t.heap[--t.heap_max]=a,i[2*r]=i[2*n]+i[2*a],t.depth[r]=(t.depth[n]>=t.depth[a]?t.depth[n]:t.depth[a])+1,i[2*n+1]=i[2*a+1]=r,t.heap[1]=r++,w(t,i,1)}while(t.heap_len>=2);t.heap[--t.heap_max]=t.heap[1],u(t,e),c(i,h,t.bl_count)}function y(t,e,n){var a,r,i=-1,s=e[1],o=0,l=7,h=4;for(0===s&&(l=138,h=3),e[2*(n+1)+1]=65535,a=0;a<=n;a++)r=s,s=e[2*(a+1)+1],++o=3&&0===t.bl_tree[2*nt[e]+1];e--);return t.opt_len+=3*(e+1)+5+5+4,e}function B(t,e,n,a){var r;for(l(t,e-257,5),l(t,n-1,5),l(t,a-4,4),r=0;r>>=1)if(1&n&&0!==t.dyn_ltree[2*e])return D;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return I;for(e=32;e0?(t.strm.data_type===U&&(t.strm.data_type=C(t)),k(t,t.l_desc),k(t,t.d_desc),s=z(t),r=t.opt_len+3+7>>>3,(i=t.static_len+3+7>>>3)<=r&&(r=i)):r=i=n+5,n+4<=r&&-1!==e?S(t,e,n,a):t.strategy===R||i===r?(l(t,(j<<1)+(a?1:0),3),v(t,at,rt)):(l(t,(F<<1)+(a?1:0),3),B(t,t.l_desc.max_code+1,t.d_desc.max_code+1,s+1),v(t,t.dyn_ltree,t.dyn_dtree)),g(t),a&&p(t)}function Z(t,e,n){return t.pending_buf[t.d_buf+2*t.last_lit]=e>>>8&255,t.pending_buf[t.d_buf+2*t.last_lit+1]=255&e,t.pending_buf[t.l_buf+t.last_lit]=255&n,t.last_lit++,0===e?t.dyn_ltree[2*n]++:(t.matches++,e--,t.dyn_ltree[2*(st[n]+H+1)]++,t.dyn_dtree[2*s(e)]++),t.last_lit===t.lit_bufsize-1}var N=t("../utils/common"),R=4,D=0,I=1,U=2,T=0,j=1,F=2,L=29,H=256,M=H+1+L,P=30,W=19,K=2*M+1,Y=15,$=16,q=7,G=256,X=16,J=17,Q=18,V=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],tt=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],et=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],nt=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],at=new Array(2*(M+2));a(at);var rt=new Array(2*P);a(rt);var it=new Array(512);a(it);var st=new Array(256);a(st);var ot=new Array(L);a(ot);var lt=new Array(P);a(lt);var ht,dt,ft,ut=!1;n._tr_init=A,n._tr_stored_block=S,n._tr_flush_block=O,n._tr_tally=Z,n._tr_align=E},{"../utils/common":5}],12:[function(t,e,n){"use strict";function a(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}e.exports=a},{}]},{},[3])(3)})},function(t,e,n){var a,a;!function(e){t.exports=e()}(function(){return function(){function t(e,n,r){function i(o,l){if(!n[o]){if(!e[o]){var h="function"==typeof a&&a;if(!l&&h)return a(o,!0);if(s)return s(o,!0);var d=new Error("Cannot find module '"+o+"'");throw d.code="MODULE_NOT_FOUND",d}var f=n[o]={exports:{}};e[o][0].call(f.exports,function(t){return i(e[o][1][t]||t)},f,f.exports,t,e,n,r)}return n[o].exports}for(var s="function"==typeof a&&a,o=0;o=97?e-61:e>=65?e-55:e>=48?e-48:"?"}function r(t){var e=a(t[0]),n=a(t[1]),r=a(t[2]);return[e<<2|n>>4&63,n<<4&240|r>>2&15,r<<6&192|63&a(t[3])]}e.exports=function(t){var e="",n=0;for(n=0;n=0&&e.windowBits<16&&(e.windowBits=-e.windowBits,0===e.windowBits&&(e.windowBits=-15)),!(e.windowBits>=0&&e.windowBits<16)||t&&t.windowBits||(e.windowBits+=32),e.windowBits>15&&e.windowBits<48&&0==(15&e.windowBits)&&(e.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new f,this.strm.avail_out=0;var n=s.inflateInit2(this.strm,e.windowBits);if(n!==h.Z_OK)throw new Error(d[n]);if(this.header=new u,s.inflateGetHeader(this.strm,this.header),e.dictionary&&("string"==typeof e.dictionary?e.dictionary=l.string2buf(e.dictionary):"[object ArrayBuffer]"===c.call(e.dictionary)&&(e.dictionary=new Uint8Array(e.dictionary)),e.raw&&(n=s.inflateSetDictionary(this.strm,e.dictionary))!==h.Z_OK))throw new Error(d[n])}function r(t,e){var n=new a(e);if(n.push(t,!0),n.err)throw n.msg||d[n.err];return n.result}function i(t,e){return e=e||{},e.raw=!0,r(t,e)}var s=t("./zlib/inflate"),o=t("./utils/common"),l=t("./utils/strings"),h=t("./zlib/constants"),d=t("./zlib/messages"),f=t("./zlib/zstream"),u=t("./zlib/gzheader"),c=Object.prototype.toString;a.prototype.push=function(t,e){var n,a,r,i,d,f=this.strm,u=this.options.chunkSize,_=this.options.dictionary,g=!1;if(this.ended)return!1;a=e===~~e?e:!0===e?h.Z_FINISH:h.Z_NO_FLUSH,"string"==typeof t?f.input=l.binstring2buf(t):"[object ArrayBuffer]"===c.call(t)?f.input=new Uint8Array(t):f.input=t,f.next_in=0,f.avail_in=f.input.length;do{if(0===f.avail_out&&(f.output=new o.Buf8(u),f.next_out=0,f.avail_out=u),n=s.inflate(f,h.Z_NO_FLUSH),n===h.Z_NEED_DICT&&_&&(n=s.inflateSetDictionary(this.strm,_)),n===h.Z_BUF_ERROR&&!0===g&&(n=h.Z_OK,g=!1),n!==h.Z_STREAM_END&&n!==h.Z_OK)return this.onEnd(n),this.ended=!0,!1;f.next_out&&(0!==f.avail_out&&n!==h.Z_STREAM_END&&(0!==f.avail_in||a!==h.Z_FINISH&&a!==h.Z_SYNC_FLUSH)||("string"===this.options.to?(r=l.utf8border(f.output,f.next_out),i=f.next_out-r,d=l.buf2string(f.output,r),f.next_out=i,f.avail_out=u-i,i&&o.arraySet(f.output,f.output,r,i,0),this.onData(d)):this.onData(o.shrinkBuf(f.output,f.next_out)))),0===f.avail_in&&0===f.avail_out&&(g=!0)}while((f.avail_in>0||0===f.avail_out)&&n!==h.Z_STREAM_END);return n===h.Z_STREAM_END&&(a=h.Z_FINISH),a===h.Z_FINISH?(n=s.inflateEnd(this.strm),this.onEnd(n),this.ended=!0,n===h.Z_OK):a!==h.Z_SYNC_FLUSH||(this.onEnd(h.Z_OK),f.avail_out=0,!0)},a.prototype.onData=function(t){this.chunks.push(t)},a.prototype.onEnd=function(t){t===h.Z_OK&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=o.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg},n.Inflate=a,n.inflate=r,n.inflateRaw=i,n.ungzip=r},{"./utils/common":5,"./utils/strings":6,"./zlib/constants":8,"./zlib/gzheader":10,"./zlib/inflate":12,"./zlib/messages":14,"./zlib/zstream":15}],5:[function(t,e,n){"use strict";function a(t,e){return Object.prototype.hasOwnProperty.call(t,e)}var r="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Int32Array;n.assign=function(t){for(var e=Array.prototype.slice.call(arguments,1);e.length;){var n=e.shift();if(n){if("object"!=typeof n)throw new TypeError(n+"must be non-object");for(var r in n)a(n,r)&&(t[r]=n[r])}}return t},n.shrinkBuf=function(t,e){return t.length===e?t:t.subarray?t.subarray(0,e):(t.length=e,t)};var i={arraySet:function(t,e,n,a,r){if(e.subarray&&t.subarray)return void t.set(e.subarray(n,n+a),r);for(var i=0;i=252?6:l>=248?5:l>=240?4:l>=224?3:l>=192?2:1;o[254]=o[254]=1,n.string2buf=function(t){var e,n,a,i,s,o=t.length,l=0;for(i=0;i>>6,e[s++]=128|63&n):n<65536?(e[s++]=224|n>>>12,e[s++]=128|n>>>6&63,e[s++]=128|63&n):(e[s++]=240|n>>>18,e[s++]=128|n>>>12&63,e[s++]=128|n>>>6&63,e[s++]=128|63&n);return e},n.buf2binstring=function(t){return a(t,t.length)},n.binstring2buf=function(t){for(var e=new r.Buf8(t.length),n=0,a=e.length;n4)h[r++]=65533,n+=s-1;else{for(i&=2===s?31:3===s?15:7;s>1&&n1?h[r++]=65533:i<65536?h[r++]=i:(i-=65536,h[r++]=55296|i>>10&1023,h[r++]=56320|1023&i)}return a(h,r)},n.utf8border=function(t,e){var n;for(e=e||t.length,e>t.length&&(e=t.length),n=e-1;n>=0&&128==(192&t[n]);)n--;return n<0?e:0===n?e:n+o[t[n]]>e?n:e}},{"./common":5}],7:[function(t,e,n){"use strict";function a(t,e,n,a){for(var r=65535&t|0,i=t>>>16&65535|0,s=0;0!==n;){s=n>2e3?2e3:n,n-=s;do{r=r+e[a++]|0,i=i+r|0}while(--s);r%=65521,i%=65521}return r|i<<16|0}e.exports=a},{}],8:[function(t,e,n){"use strict";e.exports={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8}},{}],9:[function(t,e,n){"use strict";function a(t,e,n,a){var i=r,s=a+n;t^=-1;for(var o=a;o>>8^i[255&(t^e[o])];return-1^t}var r=function(){for(var t,e=[],n=0;n<256;n++){t=n;for(var a=0;a<8;a++)t=1&t?3988292384^t>>>1:t>>>1;e[n]=t}return e}();e.exports=a},{}],10:[function(t,e,n){"use strict";function a(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}e.exports=a},{}],11:[function(t,e,n){"use strict";e.exports=function(t,e){var n,a,r,i,s,o,l,h,d,f,u,c,_,g,p,m,b,w,v,k,y,x,z,B,C;n=t.state,a=t.next_in,B=t.input,r=a+(t.avail_in-5),i=t.next_out,C=t.output,s=i-(e-t.avail_out),o=i+(t.avail_out-257),l=n.dmax,h=n.wsize,d=n.whave,f=n.wnext,u=n.window,c=n.hold,_=n.bits,g=n.lencode,p=n.distcode,m=(1<>>24,c>>>=v,_-=v,0===(v=w>>>16&255))C[i++]=65535&w;else{if(!(16&v)){if(0==(64&v)){w=g[(65535&w)+(c&(1<>>=v,_-=v),_<15&&(c+=B[a++]<<_,_+=8,c+=B[a++]<<_,_+=8),w=p[c&b];n:for(;;){if(v=w>>>24,c>>>=v,_-=v,!(16&(v=w>>>16&255))){if(0==(64&v)){w=p[(65535&w)+(c&(1<l){t.msg="invalid distance too far back",n.mode=30;break t}if(c>>>=v,_-=v,v=i-s,y>v){if((v=y-v)>d&&n.sane){t.msg="invalid distance too far back",n.mode=30;break t}if(x=0,z=u,0===f){if(x+=h-v,v2;)C[i++]=z[x++],C[i++]=z[x++],C[i++]=z[x++],k-=3;k&&(C[i++]=z[x++],k>1&&(C[i++]=z[x++]))}else{x=i-y;do{C[i++]=C[x++],C[i++]=C[x++],C[i++]=C[x++],k-=3}while(k>2);k&&(C[i++]=C[x++],k>1&&(C[i++]=C[x++]))}break}}break}}while(a>3,a-=k,_-=k<<3,c&=(1<<_)-1,t.next_in=a,t.next_out=i,t.avail_in=a>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24)}function r(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new b.Buf16(320),this.work=new b.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function i(t){var e;return t&&t.state?(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=T,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new b.Buf32(gt),e.distcode=e.distdyn=new b.Buf32(pt),e.sane=1,e.back=-1,E):N}function s(t){var e;return t&&t.state?(e=t.state,e.wsize=0,e.whave=0,e.wnext=0,i(t)):N}function o(t,e){var n,a;return t&&t.state?(a=t.state,e<0?(n=0,e=-e):(n=1+(e>>4),e<48&&(e&=15)),e&&(e<8||e>15)?N:(null!==a.window&&a.wbits!==e&&(a.window=null),a.wrap=n,a.wbits=e,s(t))):N}function l(t,e){var n,a;return t?(a=new r,t.state=a,a.window=null,n=o(t,e),n!==E&&(t.state=null),n):N}function h(t){return l(t,mt)}function d(t){if(bt){var e;for(p=new b.Buf32(512),m=new b.Buf32(32),e=0;e<144;)t.lens[e++]=8;for(;e<256;)t.lens[e++]=9;for(;e<280;)t.lens[e++]=7;for(;e<288;)t.lens[e++]=8;for(y(z,t.lens,0,288,p,0,t.work,{bits:9}),e=0;e<32;)t.lens[e++]=5;y(B,t.lens,0,32,m,0,t.work,{bits:5}),bt=!1}t.lencode=p,t.lenbits=9,t.distcode=m,t.distbits=5}function f(t,e,n,a){var r,i=t.state;return null===i.window&&(i.wsize=1<=i.wsize?(b.arraySet(i.window,e,n-i.wsize,i.wsize,0),i.wnext=0,i.whave=i.wsize):(r=i.wsize-i.wnext,r>a&&(r=a),b.arraySet(i.window,e,n-a,r,i.wnext),a-=r,a?(b.arraySet(i.window,e,n-a,a,0),i.wnext=a,i.whave=i.wsize):(i.wnext+=r,i.wnext===i.wsize&&(i.wnext=0),i.whave>>8&255,n.check=v(n.check,At,2,0),u=0,c=0,n.mode=j;break}if(n.flags=0,n.head&&(n.head.done=!1),!(1&n.wrap)||(((255&u)<<8)+(u>>8))%31){t.msg="incorrect header check",n.mode=ut;break}if((15&u)!==U){t.msg="unknown compression method",n.mode=ut;break}if(u>>>=4,c-=4,yt=8+(15&u),0===n.wbits)n.wbits=yt;else if(yt>n.wbits){t.msg="invalid window size",n.mode=ut;break}n.dmax=1<>8&1),512&n.flags&&(At[0]=255&u,At[1]=u>>>8&255,n.check=v(n.check,At,2,0)),u=0,c=0,n.mode=F;case F:for(;c<32;){if(0===l)break t;l--,u+=r[s++]<>>8&255,At[2]=u>>>16&255,At[3]=u>>>24&255,n.check=v(n.check,At,4,0)),u=0,c=0,n.mode=L;case L:for(;c<16;){if(0===l)break t;l--,u+=r[s++]<>8),512&n.flags&&(At[0]=255&u,At[1]=u>>>8&255,n.check=v(n.check,At,2,0)),u=0,c=0,n.mode=H;case H:if(1024&n.flags){for(;c<16;){if(0===l)break t;l--,u+=r[s++]<>>8&255,n.check=v(n.check,At,2,0)),u=0,c=0}else n.head&&(n.head.extra=null);n.mode=M;case M:if(1024&n.flags&&(p=n.length,p>l&&(p=l),p&&(n.head&&(yt=n.head.extra_len-n.length,n.head.extra||(n.head.extra=new Array(n.head.extra_len)),b.arraySet(n.head.extra,r,s,p,yt)),512&n.flags&&(n.check=v(n.check,r,p,s)),l-=p,s+=p,n.length-=p),n.length))break t;n.length=0,n.mode=P;case P:if(2048&n.flags){if(0===l)break t;p=0;do{yt=r[s+p++],n.head&&yt&&n.length<65536&&(n.head.name+=String.fromCharCode(yt))}while(yt&&p>9&1,n.head.done=!0),t.adler=n.check=0,n.mode=q;break;case Y:for(;c<32;){if(0===l)break t;l--,u+=r[s++]<>>=7&c,c-=7&c,n.mode=ht;break}for(;c<3;){if(0===l)break t;l--,u+=r[s++]<>>=1,c-=1,3&u){case 0:n.mode=X;break;case 1:if(d(n),n.mode=nt,e===S){u>>>=2,c-=2;break t}break;case 2:n.mode=V;break;case 3:t.msg="invalid block type",n.mode=ut}u>>>=2,c-=2;break;case X:for(u>>>=7&c,c-=7&c;c<32;){if(0===l)break t;l--,u+=r[s++]<>>16^65535)){t.msg="invalid stored block lengths",n.mode=ut;break}if(n.length=65535&u,u=0,c=0,n.mode=J,e===S)break t;case J:n.mode=Q;case Q:if(p=n.length){if(p>l&&(p=l),p>h&&(p=h),0===p)break t;b.arraySet(i,r,s,p,o),l-=p,s+=p,h-=p,o+=p,n.length-=p;break}n.mode=q;break;case V:for(;c<14;){if(0===l)break t;l--,u+=r[s++]<>>=5,c-=5,n.ndist=1+(31&u),u>>>=5,c-=5,n.ncode=4+(15&u),u>>>=4,c-=4,n.nlen>286||n.ndist>30){t.msg="too many length or distance symbols",n.mode=ut;break}n.have=0,n.mode=tt;case tt:for(;n.have>>=3,c-=3}for(;n.have<19;)n.lens[St[n.have++]]=0;if(n.lencode=n.lendyn,n.lenbits=7,zt={bits:n.lenbits},xt=y(x,n.lens,0,19,n.lencode,0,n.work,zt),n.lenbits=zt.bits,xt){t.msg="invalid code lengths set",n.mode=ut;break}n.have=0,n.mode=et;case et:for(;n.have>>24,mt=Ct>>>16&255,bt=65535&Ct,!(pt<=c);){if(0===l)break t;l--,u+=r[s++]<>>=pt,c-=pt,n.lens[n.have++]=bt;else{if(16===bt){for(Bt=pt+2;c>>=pt,c-=pt,0===n.have){t.msg="invalid bit length repeat",n.mode=ut;break}yt=n.lens[n.have-1],p=3+(3&u),u>>>=2,c-=2}else if(17===bt){for(Bt=pt+3;c>>=pt,c-=pt,yt=0,p=3+(7&u),u>>>=3,c-=3}else{for(Bt=pt+7;c>>=pt,c-=pt,yt=0,p=11+(127&u),u>>>=7,c-=7}if(n.have+p>n.nlen+n.ndist){t.msg="invalid bit length repeat",n.mode=ut;break}for(;p--;)n.lens[n.have++]=yt}}if(n.mode===ut)break;if(0===n.lens[256]){t.msg="invalid code -- missing end-of-block",n.mode=ut;break}if(n.lenbits=9,zt={bits:n.lenbits},xt=y(z,n.lens,0,n.nlen,n.lencode,0,n.work,zt),n.lenbits=zt.bits,xt){t.msg="invalid literal/lengths set",n.mode=ut;break}if(n.distbits=6,n.distcode=n.distdyn,zt={bits:n.distbits},xt=y(B,n.lens,n.nlen,n.ndist,n.distcode,0,n.work,zt),n.distbits=zt.bits,xt){t.msg="invalid distances set",n.mode=ut;break}if(n.mode=nt,e===S)break t;case nt:n.mode=at;case at:if(l>=6&&h>=258){t.next_out=o,t.avail_out=h,t.next_in=s,t.avail_in=l,n.hold=u,n.bits=c,k(t,g),o=t.next_out,i=t.output,h=t.avail_out,s=t.next_in,r=t.input,l=t.avail_in,u=n.hold,c=n.bits,n.mode===q&&(n.back=-1);break}for(n.back=0;Ct=n.lencode[u&(1<>>24,mt=Ct>>>16&255,bt=65535&Ct,!(pt<=c);){if(0===l)break t;l--,u+=r[s++]<>wt)],pt=Ct>>>24,mt=Ct>>>16&255,bt=65535&Ct,!(wt+pt<=c);){if(0===l)break t;l--,u+=r[s++]<>>=wt,c-=wt,n.back+=wt}if(u>>>=pt,c-=pt,n.back+=pt,n.length=bt,0===mt){n.mode=lt;break}if(32&mt){n.back=-1,n.mode=q;break}if(64&mt){t.msg="invalid literal/length code",n.mode=ut;break}n.extra=15&mt,n.mode=rt;case rt:if(n.extra){for(Bt=n.extra;c>>=n.extra,c-=n.extra,n.back+=n.extra}n.was=n.length,n.mode=it;case it:for(;Ct=n.distcode[u&(1<>>24,mt=Ct>>>16&255,bt=65535&Ct,!(pt<=c);){if(0===l)break t;l--,u+=r[s++]<>wt)],pt=Ct>>>24,mt=Ct>>>16&255,bt=65535&Ct,!(wt+pt<=c);){if(0===l)break t;l--,u+=r[s++]<>>=wt,c-=wt,n.back+=wt}if(u>>>=pt,c-=pt,n.back+=pt,64&mt){t.msg="invalid distance code",n.mode=ut;break}n.offset=bt,n.extra=15&mt,n.mode=st;case st:if(n.extra){for(Bt=n.extra;c>>=n.extra,c-=n.extra,n.back+=n.extra}if(n.offset>n.dmax){t.msg="invalid distance too far back",n.mode=ut;break}n.mode=ot;case ot:if(0===h)break t;if(p=g-h,n.offset>p){if((p=n.offset-p)>n.whave&&n.sane){t.msg="invalid distance too far back",n.mode=ut;break}p>n.wnext?(p-=n.wnext,m=n.wsize-p):m=n.wnext-p,p>n.length&&(p=n.length),gt=n.window}else gt=i,m=o-n.offset,p=n.length;p>h&&(p=h),h-=p,n.length-=p;do{i[o++]=gt[m++]}while(--p);0===n.length&&(n.mode=at);break;case lt:if(0===h)break t;i[o++]=n.length,h--,n.mode=at;break;case ht:if(n.wrap){for(;c<32;){if(0===l)break t;l--,u|=r[s++]<=1&&0===I[C];C--);if(A>C&&(A=C),0===C)return h[d++]=20971520,h[d++]=20971520,u.bits=1,0;for(B=1;B0&&(0===t||1!==C))return-1;for(U[1]=0,x=1;x<15;x++)U[x+1]=U[x]+I[x];for(z=0;z852||2===t&&Z>592)return 1;for(;;){w=x-E,f[z]b?(v=T[j+f[z]],k=R[D+f[z]]):(v=96,k=0),c=1<>E)+_]=w<<24|v<<16|k|0}while(0!==_);for(c=1<>=1;if(0!==c?(N&=c-1,N+=c):N=0,z++,0==--I[x]){if(x===C)break;x=e[n+f[z]]}if(x>A&&(N&p)!==g){for(0===E&&(E=A),m+=B,S=x-E,O=1<852||2===t&&Z>592)return 1;g=N&p,h[g]=A<<24|S<<16|m-d|0}}return 0!==N&&(h[m+N]=x-E<<24|64<<16|0),u.bits=A,0}},{"../utils/common":5}],14:[function(t,e,n){"use strict";e.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"}},{}],15:[function(t,e,n){"use strict";function a(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}e.exports=a},{}]},{},[3])(3)})}]); \ No newline at end of file diff --git a/doc/lib/js/docsify@4.js b/doc/lib/js/docsify@4.js new file mode 100644 index 0000000..76fc755 --- /dev/null +++ b/doc/lib/js/docsify@4.js @@ -0,0 +1 @@ +!function(){function c(i){var o=Object.create(null);return function(e){var n=f(e)?e:JSON.stringify(e);return o[n]||(o[n]=i(e))}}var a=c(function(e){return e.replace(/([A-Z])/g,function(e){return"-"+e.toLowerCase()})}),u=Object.prototype.hasOwnProperty,m=Object.assign||function(e){for(var n=arguments,i=1;i=e||n.classList.contains("hidden")?S(h,"add","sticky"):S(h,"remove","sticky"))}function ee(e,n,o,i){var t=[];null!=(n=l(n))&&(t=k(n,"a"));var a,r=decodeURI(e.toURL(e.getCurrentPath()));return t.sort(function(e,n){return n.href.length-e.href.length}).forEach(function(e){var n=decodeURI(e.getAttribute("href")),i=o?e.parentNode:e;e.title=e.title||e.innerText,0!==r.indexOf(n)||a?S(i,"remove","active"):(a=e,S(i,"add","active"))}),i&&(v.title=a?a.title||a.innerText+" - "+J:J),a}function ne(e,n){for(var i=0;ithis.end&&e>=this.next}[this.direction]}},{key:"_defaultEase",value:function(e,n,i,o){return(e/=o/2)<1?i/2*e*e+n:-i/2*(--e*(e-2)-1)+n}}]),re);function re(){var e=0c){n=n||p;break}n=p}!n||(r=fe[ve(e,n.getAttribute("data-id"))])&&r!==a&&(a&&a.classList.remove("active"),r.classList.add("active"),a=r,!pe&&h.classList.contains("sticky")&&(e=i.clientHeight,r=a.offsetTop+a.clientHeight+40,a=a.offsetTop>=t.scrollTop&&r<=t.scrollTop+e,i.scrollTop=a?t.scrollTop:+r"']/),xe=/[&<>"']/g,Se=/[<>"']|&(?!#?\w+;)/,Ae=/[<>"']|&(?!#?\w+;)/g,$e={"&":"&","<":"<",">":">",'"':""","'":"'"};var ze=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function Fe(e){return e.replace(ze,function(e,n){return"colon"===(n=n.toLowerCase())?":":"#"===n.charAt(0)?"x"===n.charAt(1)?String.fromCharCode(parseInt(n.substring(2),16)):String.fromCharCode(+n.substring(1)):""})}var Ee=/(^|[^\[])\^/g;var Te=/[^\w:]/g,Ce=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;var Re={},je=/^[^:]+:\/*[^/]*$/,Oe=/^([^:]+:)[\s\S]*$/,Le=/^([^:]+:\/*[^/]*)[\s\S]*$/;function qe(e,n){Re[" "+e]||(je.test(e)?Re[" "+e]=e+"/":Re[" "+e]=Pe(e,"/",!0));var i=-1===(e=Re[" "+e]).indexOf(":");return"//"===n.substring(0,2)?i?n:e.replace(Oe,"$1")+n:"/"===n.charAt(0)?i?n:e.replace(Le,"$1")+n:e+n}function Pe(e,n,i){var o=e.length;if(0===o)return"";for(var t=0;tn)i.splice(n);else for(;i.length>=1,e+=e;return i+e},We=we.defaults,Xe=Be,Qe=Ze,Je=Me,Ke=Ve;function en(e,n,i){var o=n.href,t=n.title?Je(n.title):null,n=e[1].replace(/\\([\[\]])/g,"$1");return"!"!==e[0].charAt(0)?{type:"link",raw:i,href:o,title:t,text:n}:{type:"image",raw:i,href:o,title:t,text:Je(n)}}var nn=function(){function e(e){this.options=e||We}return e.prototype.space=function(e){e=this.rules.block.newline.exec(e);if(e)return 1=i.length?e.slice(i.length):e}).join("\n")}(i,n[3]||"");return{type:"code",raw:i,lang:n[2]&&n[2].trim(),text:e}}},e.prototype.heading=function(e){var n=this.rules.block.heading.exec(e);if(n){var i=n[2].trim();return/#$/.test(i)&&(e=Xe(i,"#"),!this.options.pedantic&&e&&!/ $/.test(e)||(i=e.trim())),{type:"heading",raw:n[0],depth:n[1].length,text:i}}},e.prototype.nptable=function(e){e=this.rules.block.nptable.exec(e);if(e){var n={type:"table",header:Qe(e[1].replace(/^ *| *\| *$/g,"")),align:e[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:e[3]?e[3].replace(/\n$/,"").split("\n"):[],raw:e[0]};if(n.header.length===n.align.length){for(var i=n.align.length,o=0;o ?/gm,"");return{type:"blockquote",raw:n[0],text:e}}},e.prototype.list=function(e){e=this.rules.block.list.exec(e);if(e){for(var n,i,o,t,a,r=e[0],c=e[2],u=1s[1].length:o[1].length>s[0].length||3/i.test(e[0])&&(n=!1),!i&&/^<(pre|code|kbd|script)(\s|>)/i.test(e[0])?i=!0:i&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(e[0])&&(i=!1),{type:this.options.sanitize?"text":"html",raw:e[0],inLink:n,inRawBlock:i,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):Je(e[0]):e[0]}},e.prototype.link=function(e){var n=this.rules.inline.link.exec(e);if(n){e=n[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;var i=Xe(e.slice(0,-1),"\\");if((e.length-i.length)%2==0)return}else{var o=Ke(n[2],"()");-1$/.test(e)?i.slice(1):i.slice(1,-1):i)&&i.replace(this.rules.inline._escapes,"$1"),title:o&&o.replace(this.rules.inline._escapes,"$1")},n[0])}},e.prototype.reflink=function(e,n){if((i=this.rules.inline.reflink.exec(e))||(i=this.rules.inline.nolink.exec(e))){var e=(i[2]||i[1]).replace(/\s+/g," ");if((e=n[e.toLowerCase()])&&e.href)return en(i,e,i[0]);var i=i[0].charAt(0);return{type:"text",raw:i,text:i}}},e.prototype.strong=function(e,n,i){void 0===i&&(i="");var o=this.rules.inline.strong.start.exec(e);if(o&&(!o[1]||o[1]&&(""===i||this.rules.inline.punctuation.exec(i)))){n=n.slice(-1*e.length);var t,a="**"===o[0]?this.rules.inline.strong.endAst:this.rules.inline.strong.endUnd;for(a.lastIndex=0;null!=(o=a.exec(n));)if(t=this.rules.inline.strong.middle.exec(n.slice(0,o.index+3)))return{type:"strong",raw:e.slice(0,t[0].length),text:e.slice(2,t[0].length-2)}}},e.prototype.em=function(e,n,i){void 0===i&&(i="");var o=this.rules.inline.em.start.exec(e);if(o&&(!o[1]||o[1]&&(""===i||this.rules.inline.punctuation.exec(i)))){n=n.slice(-1*e.length);var t,a="*"===o[0]?this.rules.inline.em.endAst:this.rules.inline.em.endUnd;for(a.lastIndex=0;null!=(o=a.exec(n));)if(t=this.rules.inline.em.middle.exec(n.slice(0,o.index+2)))return{type:"em",raw:e.slice(0,t[0].length),text:e.slice(1,t[0].length-1)}}},e.prototype.codespan=function(e){var n=this.rules.inline.code.exec(e);if(n){var i=n[2].replace(/\n/g," "),o=/[^ ]/.test(i),e=/^ /.test(i)&&/ $/.test(i);return o&&e&&(i=i.substring(1,i.length-1)),i=Je(i,!0),{type:"codespan",raw:n[0],text:i}}},e.prototype.br=function(e){e=this.rules.inline.br.exec(e);if(e)return{type:"br",raw:e[0]}},e.prototype.del=function(e){e=this.rules.inline.del.exec(e);if(e)return{type:"del",raw:e[0],text:e[2]}},e.prototype.autolink=function(e,n){e=this.rules.inline.autolink.exec(e);if(e){var i,n="@"===e[2]?"mailto:"+(i=Je(this.options.mangle?n(e[1]):e[1])):i=Je(e[1]);return{type:"link",raw:e[0],text:i,href:n,tokens:[{type:"text",raw:i,text:i}]}}},e.prototype.url=function(e,n){var i,o,t,a;if(i=this.rules.inline.url.exec(e)){if("@"===i[2])t="mailto:"+(o=Je(this.options.mangle?n(i[0]):i[0]));else{for(;a=i[0],i[0]=this.rules.inline._backpedal.exec(i[0])[0],a!==i[0];);o=Je(i[0]),t="www."===i[1]?"http://"+o:o}return{type:"link",raw:i[0],text:o,href:t,tokens:[{type:"text",raw:o,text:o}]}}},e.prototype.inlineText=function(e,n,i){e=this.rules.inline.text.exec(e);if(e){i=n?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):Je(e[0]):e[0]:Je(this.options.smartypants?i(e[0]):e[0]);return{type:"text",raw:e[0],text:i}}},e}(),Ze=De,Ve=Ne,De=Ue,Ne={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?! {0,3}bull )\n*|\s*$)/,html:"^ {0,3}(?:<(script|pre|style)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:\\n{2,}|$)|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$))",def:/^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,nptable:Ze,table:Ze,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\[\[\]]|[^\[\]])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/};Ne.def=Ve(Ne.def).replace("label",Ne._label).replace("title",Ne._title).getRegex(),Ne.bullet=/(?:[*+-]|\d{1,9}[.)])/,Ne.item=/^( *)(bull) ?[^\n]*(?:\n(?! *bull ?)[^\n]*)*/,Ne.item=Ve(Ne.item,"gm").replace(/bull/g,Ne.bullet).getRegex(),Ne.listItemStart=Ve(/^( *)(bull)/).replace("bull",Ne.bullet).getRegex(),Ne.list=Ve(Ne.list).replace(/bull/g,Ne.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+Ne.def.source+")").getRegex(),Ne._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",Ne._comment=/|$)/,Ne.html=Ve(Ne.html,"i").replace("comment",Ne._comment).replace("tag",Ne._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),Ne.paragraph=Ve(Ne._paragraph).replace("hr",Ne.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",Ne._tag).getRegex(),Ne.blockquote=Ve(Ne.blockquote).replace("paragraph",Ne.paragraph).getRegex(),Ne.normal=De({},Ne),Ne.gfm=De({},Ne.normal,{nptable:"^ *([^|\\n ].*\\|.*)\\n {0,3}([-:]+ *\\|[-| :]*)(?:\\n((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)",table:"^ *\\|(.+)\\n {0,3}\\|?( *[-:]+[-| :]*)(?:\\n *((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"}),Ne.gfm.nptable=Ve(Ne.gfm.nptable).replace("hr",Ne.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",Ne._tag).getRegex(),Ne.gfm.table=Ve(Ne.gfm.table).replace("hr",Ne.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",Ne._tag).getRegex(),Ne.pedantic=De({},Ne.normal,{html:Ve("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",Ne._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:Ze,paragraph:Ve(Ne.normal._paragraph).replace("hr",Ne.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",Ne.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()});Ze={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:Ze,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,nolink:/^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",strong:{start:/^(?:(\*\*(?=[*punctuation]))|\*\*)(?![\s])|__/,middle:/^\*\*(?:(?:(?!overlapSkip)(?:[^*]|\\\*)|overlapSkip)|\*(?:(?!overlapSkip)(?:[^*]|\\\*)|overlapSkip)*?\*)+?\*\*$|^__(?![\s])((?:(?:(?!overlapSkip)(?:[^_]|\\_)|overlapSkip)|_(?:(?!overlapSkip)(?:[^_]|\\_)|overlapSkip)*?_)+?)__$/,endAst:/[^punctuation\s]\*\*(?!\*)|[punctuation]\*\*(?!\*)(?:(?=[punctuation_\s]|$))/,endUnd:/[^\s]__(?!_)(?:(?=[punctuation*\s])|$)/},em:{start:/^(?:(\*(?=[punctuation]))|\*)(?![*\s])|_/,middle:/^\*(?:(?:(?!overlapSkip)(?:[^*]|\\\*)|overlapSkip)|\*(?:(?!overlapSkip)(?:[^*]|\\\*)|overlapSkip)*?\*)+?\*$|^_(?![_\s])(?:(?:(?!overlapSkip)(?:[^_]|\\_)|overlapSkip)|_(?:(?!overlapSkip)(?:[^_]|\\_)|overlapSkip)*?_)+?_$/,endAst:/[^punctuation\s]\*(?!\*)|[punctuation]\*(?!\*)(?:(?=[punctuation_\s]|$))/,endUnd:/[^\s]_(?!_)(?:(?=[punctuation*\s])|$)/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:Ze,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\?@\\[\\]`^{|}~"};Ze.punctuation=Ve(Ze.punctuation).replace(/punctuation/g,Ze._punctuation).getRegex(),Ze._blockSkip="\\[[^\\]]*?\\]\\([^\\)]*?\\)|`[^`]*?`|<[^>]*?>",Ze._overlapSkip="__[^_]*?__|\\*\\*\\[^\\*\\]*?\\*\\*",Ze._comment=Ve(Ne._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),Ze.em.start=Ve(Ze.em.start).replace(/punctuation/g,Ze._punctuation).getRegex(),Ze.em.middle=Ve(Ze.em.middle).replace(/punctuation/g,Ze._punctuation).replace(/overlapSkip/g,Ze._overlapSkip).getRegex(),Ze.em.endAst=Ve(Ze.em.endAst,"g").replace(/punctuation/g,Ze._punctuation).getRegex(),Ze.em.endUnd=Ve(Ze.em.endUnd,"g").replace(/punctuation/g,Ze._punctuation).getRegex(),Ze.strong.start=Ve(Ze.strong.start).replace(/punctuation/g,Ze._punctuation).getRegex(),Ze.strong.middle=Ve(Ze.strong.middle).replace(/punctuation/g,Ze._punctuation).replace(/overlapSkip/g,Ze._overlapSkip).getRegex(),Ze.strong.endAst=Ve(Ze.strong.endAst,"g").replace(/punctuation/g,Ze._punctuation).getRegex(),Ze.strong.endUnd=Ve(Ze.strong.endUnd,"g").replace(/punctuation/g,Ze._punctuation).getRegex(),Ze.blockSkip=Ve(Ze._blockSkip,"g").getRegex(),Ze.overlapSkip=Ve(Ze._overlapSkip,"g").getRegex(),Ze._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,Ze._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,Ze._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,Ze.autolink=Ve(Ze.autolink).replace("scheme",Ze._scheme).replace("email",Ze._email).getRegex(),Ze._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,Ze.tag=Ve(Ze.tag).replace("comment",Ze._comment).replace("attribute",Ze._attribute).getRegex(),Ze._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,Ze._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,Ze._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,Ze.link=Ve(Ze.link).replace("label",Ze._label).replace("href",Ze._href).replace("title",Ze._title).getRegex(),Ze.reflink=Ve(Ze.reflink).replace("label",Ze._label).getRegex(),Ze.reflinkSearch=Ve(Ze.reflinkSearch,"g").replace("reflink",Ze.reflink).replace("nolink",Ze.nolink).getRegex(),Ze.normal=De({},Ze),Ze.pedantic=De({},Ze.normal,{strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:Ve(/^!?\[(label)\]\((.*?)\)/).replace("label",Ze._label).getRegex(),reflink:Ve(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",Ze._label).getRegex()}),Ze.gfm=De({},Ze.normal,{escape:Ve(Ze.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\'+(i?e:gn(e,!0))+"\n":"
"+(i?e:gn(e,!0))+"
\n"},e.prototype.blockquote=function(e){return"
\n"+e+"
\n"},e.prototype.html=function(e){return e},e.prototype.heading=function(e,n,i,o){return this.options.headerIds?"'+e+"\n":""+e+"\n"},e.prototype.hr=function(){return this.options.xhtml?"
\n":"
\n"},e.prototype.list=function(e,n,i){var o=n?"ol":"ul";return"<"+o+(n&&1!==i?' start="'+i+'"':"")+">\n"+e+"\n"},e.prototype.listitem=function(e){return"
  • "+e+"
  • \n"},e.prototype.checkbox=function(e){return" "},e.prototype.paragraph=function(e){return"

    "+e+"

    \n"},e.prototype.table=function(e,n){return"\n\n"+e+"\n"+(n=n&&""+n+"")+"
    \n"},e.prototype.tablerow=function(e){return"\n"+e+"\n"},e.prototype.tablecell=function(e,n){var i=n.header?"th":"td";return(n.align?"<"+i+' align="'+n.align+'">':"<"+i+">")+e+"\n"},e.prototype.strong=function(e){return""+e+""},e.prototype.em=function(e){return""+e+""},e.prototype.codespan=function(e){return""+e+""},e.prototype.br=function(){return this.options.xhtml?"
    ":"
    "},e.prototype.del=function(e){return""+e+""},e.prototype.link=function(e,n,i){if(null===(e=dn(this.options.sanitize,this.options.baseUrl,e)))return i;e='"},e.prototype.image=function(e,n,i){if(null===(e=dn(this.options.sanitize,this.options.baseUrl,e)))return i;i=''+i+'":">"},e.prototype.text=function(e){return e},e}(),ln=function(){function e(){}return e.prototype.strong=function(e){return e},e.prototype.em=function(e){return e},e.prototype.codespan=function(e){return e},e.prototype.del=function(e){return e},e.prototype.html=function(e){return e},e.prototype.text=function(e){return e},e.prototype.link=function(e,n,i){return""+i},e.prototype.image=function(e,n,i){return""+i},e.prototype.br=function(){return""},e}(),vn=function(){function e(){this.seen={}}return e.prototype.serialize=function(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")},e.prototype.getNextSafeSlug=function(e,n){var i=e,o=0;if(this.seen.hasOwnProperty(i))for(o=this.seen[e];i=e+"-"+ ++o,this.seen.hasOwnProperty(i););return n||(this.seen[e]=o,this.seen[i]=0),i},e.prototype.slug=function(e,n){void 0===n&&(n={});e=this.serialize(e);return this.getNextSafeSlug(e,n.dryrun)},e}(),hn=we.defaults,_n=Ie,mn=function(){function i(e){this.options=e||hn,this.options.renderer=this.options.renderer||new sn,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new ln,this.slugger=new vn}return i.parse=function(e,n){return new i(n).parse(e)},i.parseInline=function(e,n){return new i(n).parseInline(e)},i.prototype.parse=function(e,n){void 0===n&&(n=!0);for(var i,o,t,a,r,c,u,f,p,d,g,s,l,v,h,_="",m=e.length,b=0;bAn error occurred:

    "+wn(e.message+"",!0)+"
    ";throw e}}xn.options=xn.setOptions=function(e){return bn(xn.defaults,e),yn(xn.defaults),xn},xn.getDefaults=Me,xn.defaults=we,xn.use=function(a){var n,e=bn({},a);if(a.renderer){var i,r=xn.defaults.renderer||new sn;for(i in a.renderer)!function(o){var t=r[o];r[o]=function(){for(var e=[],n=arguments.length;n--;)e[n]=arguments[n];var i=a.renderer[o].apply(r,e);return i=!1===i?t.apply(r,e):i}}(i);e.renderer=r}if(a.tokenizer){var t,c=xn.defaults.tokenizer||new nn;for(t in a.tokenizer)!function(){var o=c[t];c[t]=function(){for(var e=[],n=arguments.length;n--;)e[n]=arguments[n];var i=a.tokenizer[t].apply(c,e);return i=!1===i?o.apply(c,e):i}}();e.tokenizer=c}a.walkTokens&&(n=xn.defaults.walkTokens,e.walkTokens=function(e){a.walkTokens(e),n&&n(e)}),xn.setOptions(e)},xn.walkTokens=function(e,n){for(var i=0,o=e;iAn error occurred:

    "+wn(e.message+"",!0)+"
    ";throw e}},xn.Parser=mn,xn.parser=mn.parse,xn.Renderer=sn,xn.TextRenderer=ln,xn.Lexer=fn,xn.lexer=fn.lex,xn.Tokenizer=nn,xn.Slugger=vn;var Sn=xn.parse=xn;function An(e,i){if(void 0===i&&(i='
      {inner}
    '),!e||!e.length)return"";var o="";return e.forEach(function(e){var n=e.title.replace(/(<([^>]+)>)/g,"");o+='
  • '+e.title+"
  • ",e.children&&(o+=An(e.children,i))}),i.replace("{inner}",o)}function $n(e,n){return'

    '+n.slice(5).trim()+"

    "}function zn(e,o){var t=[],a={};return e.forEach(function(e){var n=e.level||1,i=n-1;o?@[\]^`{|}~]/g;function Tn(e){return e.toLowerCase()}function Cn(e){if("string"!=typeof e)return"";var n=e.trim().replace(/[A-Z]+/g,Tn).replace(/<[^>]+>/g,"").replace(En,"").replace(/\s/g,"-").replace(/-+/g,"-").replace(/^(\d)/,"_$1"),e=Fn[n],e=u.call(Fn,n)?e+1:0;return n=(Fn[n]=e)?n+"-"+e:n}Cn.clear=function(){Fn={}};var Rn={baseURL:"https://github.githubassets.com/images/icons/emoji/",data:{100:"unicode/1f4af.png?v8",1234:"unicode/1f522.png?v8","+1":"unicode/1f44d.png?v8","-1":"unicode/1f44e.png?v8","1st_place_medal":"unicode/1f947.png?v8","2nd_place_medal":"unicode/1f948.png?v8","3rd_place_medal":"unicode/1f949.png?v8","8ball":"unicode/1f3b1.png?v8",a:"unicode/1f170.png?v8",ab:"unicode/1f18e.png?v8",abacus:"unicode/1f9ee.png?v8",abc:"unicode/1f524.png?v8",abcd:"unicode/1f521.png?v8",accept:"unicode/1f251.png?v8",accordion:"unicode/1fa97.png?v8",adhesive_bandage:"unicode/1fa79.png?v8",adult:"unicode/1f9d1.png?v8",aerial_tramway:"unicode/1f6a1.png?v8",afghanistan:"unicode/1f1e6-1f1eb.png?v8",airplane:"unicode/2708.png?v8",aland_islands:"unicode/1f1e6-1f1fd.png?v8",alarm_clock:"unicode/23f0.png?v8",albania:"unicode/1f1e6-1f1f1.png?v8",alembic:"unicode/2697.png?v8",algeria:"unicode/1f1e9-1f1ff.png?v8",alien:"unicode/1f47d.png?v8",ambulance:"unicode/1f691.png?v8",american_samoa:"unicode/1f1e6-1f1f8.png?v8",amphora:"unicode/1f3fa.png?v8",anatomical_heart:"unicode/1fac0.png?v8",anchor:"unicode/2693.png?v8",andorra:"unicode/1f1e6-1f1e9.png?v8",angel:"unicode/1f47c.png?v8",anger:"unicode/1f4a2.png?v8",angola:"unicode/1f1e6-1f1f4.png?v8",angry:"unicode/1f620.png?v8",anguilla:"unicode/1f1e6-1f1ee.png?v8",anguished:"unicode/1f627.png?v8",ant:"unicode/1f41c.png?v8",antarctica:"unicode/1f1e6-1f1f6.png?v8",antigua_barbuda:"unicode/1f1e6-1f1ec.png?v8",apple:"unicode/1f34e.png?v8",aquarius:"unicode/2652.png?v8",argentina:"unicode/1f1e6-1f1f7.png?v8",aries:"unicode/2648.png?v8",armenia:"unicode/1f1e6-1f1f2.png?v8",arrow_backward:"unicode/25c0.png?v8",arrow_double_down:"unicode/23ec.png?v8",arrow_double_up:"unicode/23eb.png?v8",arrow_down:"unicode/2b07.png?v8",arrow_down_small:"unicode/1f53d.png?v8",arrow_forward:"unicode/25b6.png?v8",arrow_heading_down:"unicode/2935.png?v8",arrow_heading_up:"unicode/2934.png?v8",arrow_left:"unicode/2b05.png?v8",arrow_lower_left:"unicode/2199.png?v8",arrow_lower_right:"unicode/2198.png?v8",arrow_right:"unicode/27a1.png?v8",arrow_right_hook:"unicode/21aa.png?v8",arrow_up:"unicode/2b06.png?v8",arrow_up_down:"unicode/2195.png?v8",arrow_up_small:"unicode/1f53c.png?v8",arrow_upper_left:"unicode/2196.png?v8",arrow_upper_right:"unicode/2197.png?v8",arrows_clockwise:"unicode/1f503.png?v8",arrows_counterclockwise:"unicode/1f504.png?v8",art:"unicode/1f3a8.png?v8",articulated_lorry:"unicode/1f69b.png?v8",artificial_satellite:"unicode/1f6f0.png?v8",artist:"unicode/1f9d1-1f3a8.png?v8",aruba:"unicode/1f1e6-1f1fc.png?v8",ascension_island:"unicode/1f1e6-1f1e8.png?v8",asterisk:"unicode/002a-20e3.png?v8",astonished:"unicode/1f632.png?v8",astronaut:"unicode/1f9d1-1f680.png?v8",athletic_shoe:"unicode/1f45f.png?v8",atm:"unicode/1f3e7.png?v8",atom:"atom.png?v8",atom_symbol:"unicode/269b.png?v8",australia:"unicode/1f1e6-1f1fa.png?v8",austria:"unicode/1f1e6-1f1f9.png?v8",auto_rickshaw:"unicode/1f6fa.png?v8",avocado:"unicode/1f951.png?v8",axe:"unicode/1fa93.png?v8",azerbaijan:"unicode/1f1e6-1f1ff.png?v8",b:"unicode/1f171.png?v8",baby:"unicode/1f476.png?v8",baby_bottle:"unicode/1f37c.png?v8",baby_chick:"unicode/1f424.png?v8",baby_symbol:"unicode/1f6bc.png?v8",back:"unicode/1f519.png?v8",bacon:"unicode/1f953.png?v8",badger:"unicode/1f9a1.png?v8",badminton:"unicode/1f3f8.png?v8",bagel:"unicode/1f96f.png?v8",baggage_claim:"unicode/1f6c4.png?v8",baguette_bread:"unicode/1f956.png?v8",bahamas:"unicode/1f1e7-1f1f8.png?v8",bahrain:"unicode/1f1e7-1f1ed.png?v8",balance_scale:"unicode/2696.png?v8",bald_man:"unicode/1f468-1f9b2.png?v8",bald_woman:"unicode/1f469-1f9b2.png?v8",ballet_shoes:"unicode/1fa70.png?v8",balloon:"unicode/1f388.png?v8",ballot_box:"unicode/1f5f3.png?v8",ballot_box_with_check:"unicode/2611.png?v8",bamboo:"unicode/1f38d.png?v8",banana:"unicode/1f34c.png?v8",bangbang:"unicode/203c.png?v8",bangladesh:"unicode/1f1e7-1f1e9.png?v8",banjo:"unicode/1fa95.png?v8",bank:"unicode/1f3e6.png?v8",bar_chart:"unicode/1f4ca.png?v8",barbados:"unicode/1f1e7-1f1e7.png?v8",barber:"unicode/1f488.png?v8",baseball:"unicode/26be.png?v8",basecamp:"basecamp.png?v8",basecampy:"basecampy.png?v8",basket:"unicode/1f9fa.png?v8",basketball:"unicode/1f3c0.png?v8",basketball_man:"unicode/26f9-2642.png?v8",basketball_woman:"unicode/26f9-2640.png?v8",bat:"unicode/1f987.png?v8",bath:"unicode/1f6c0.png?v8",bathtub:"unicode/1f6c1.png?v8",battery:"unicode/1f50b.png?v8",beach_umbrella:"unicode/1f3d6.png?v8",bear:"unicode/1f43b.png?v8",bearded_person:"unicode/1f9d4.png?v8",beaver:"unicode/1f9ab.png?v8",bed:"unicode/1f6cf.png?v8",bee:"unicode/1f41d.png?v8",beer:"unicode/1f37a.png?v8",beers:"unicode/1f37b.png?v8",beetle:"unicode/1fab2.png?v8",beginner:"unicode/1f530.png?v8",belarus:"unicode/1f1e7-1f1fe.png?v8",belgium:"unicode/1f1e7-1f1ea.png?v8",belize:"unicode/1f1e7-1f1ff.png?v8",bell:"unicode/1f514.png?v8",bell_pepper:"unicode/1fad1.png?v8",bellhop_bell:"unicode/1f6ce.png?v8",benin:"unicode/1f1e7-1f1ef.png?v8",bento:"unicode/1f371.png?v8",bermuda:"unicode/1f1e7-1f1f2.png?v8",beverage_box:"unicode/1f9c3.png?v8",bhutan:"unicode/1f1e7-1f1f9.png?v8",bicyclist:"unicode/1f6b4.png?v8",bike:"unicode/1f6b2.png?v8",biking_man:"unicode/1f6b4-2642.png?v8",biking_woman:"unicode/1f6b4-2640.png?v8",bikini:"unicode/1f459.png?v8",billed_cap:"unicode/1f9e2.png?v8",biohazard:"unicode/2623.png?v8",bird:"unicode/1f426.png?v8",birthday:"unicode/1f382.png?v8",bison:"unicode/1f9ac.png?v8",black_cat:"unicode/1f408-2b1b.png?v8",black_circle:"unicode/26ab.png?v8",black_flag:"unicode/1f3f4.png?v8",black_heart:"unicode/1f5a4.png?v8",black_joker:"unicode/1f0cf.png?v8",black_large_square:"unicode/2b1b.png?v8",black_medium_small_square:"unicode/25fe.png?v8",black_medium_square:"unicode/25fc.png?v8",black_nib:"unicode/2712.png?v8",black_small_square:"unicode/25aa.png?v8",black_square_button:"unicode/1f532.png?v8",blond_haired_man:"unicode/1f471-2642.png?v8",blond_haired_person:"unicode/1f471.png?v8",blond_haired_woman:"unicode/1f471-2640.png?v8",blonde_woman:"unicode/1f471-2640.png?v8",blossom:"unicode/1f33c.png?v8",blowfish:"unicode/1f421.png?v8",blue_book:"unicode/1f4d8.png?v8",blue_car:"unicode/1f699.png?v8",blue_heart:"unicode/1f499.png?v8",blue_square:"unicode/1f7e6.png?v8",blueberries:"unicode/1fad0.png?v8",blush:"unicode/1f60a.png?v8",boar:"unicode/1f417.png?v8",boat:"unicode/26f5.png?v8",bolivia:"unicode/1f1e7-1f1f4.png?v8",bomb:"unicode/1f4a3.png?v8",bone:"unicode/1f9b4.png?v8",book:"unicode/1f4d6.png?v8",bookmark:"unicode/1f516.png?v8",bookmark_tabs:"unicode/1f4d1.png?v8",books:"unicode/1f4da.png?v8",boom:"unicode/1f4a5.png?v8",boomerang:"unicode/1fa83.png?v8",boot:"unicode/1f462.png?v8",bosnia_herzegovina:"unicode/1f1e7-1f1e6.png?v8",botswana:"unicode/1f1e7-1f1fc.png?v8",bouncing_ball_man:"unicode/26f9-2642.png?v8",bouncing_ball_person:"unicode/26f9.png?v8",bouncing_ball_woman:"unicode/26f9-2640.png?v8",bouquet:"unicode/1f490.png?v8",bouvet_island:"unicode/1f1e7-1f1fb.png?v8",bow:"unicode/1f647.png?v8",bow_and_arrow:"unicode/1f3f9.png?v8",bowing_man:"unicode/1f647-2642.png?v8",bowing_woman:"unicode/1f647-2640.png?v8",bowl_with_spoon:"unicode/1f963.png?v8",bowling:"unicode/1f3b3.png?v8",bowtie:"bowtie.png?v8",boxing_glove:"unicode/1f94a.png?v8",boy:"unicode/1f466.png?v8",brain:"unicode/1f9e0.png?v8",brazil:"unicode/1f1e7-1f1f7.png?v8",bread:"unicode/1f35e.png?v8",breast_feeding:"unicode/1f931.png?v8",bricks:"unicode/1f9f1.png?v8",bride_with_veil:"unicode/1f470-2640.png?v8",bridge_at_night:"unicode/1f309.png?v8",briefcase:"unicode/1f4bc.png?v8",british_indian_ocean_territory:"unicode/1f1ee-1f1f4.png?v8",british_virgin_islands:"unicode/1f1fb-1f1ec.png?v8",broccoli:"unicode/1f966.png?v8",broken_heart:"unicode/1f494.png?v8",broom:"unicode/1f9f9.png?v8",brown_circle:"unicode/1f7e4.png?v8",brown_heart:"unicode/1f90e.png?v8",brown_square:"unicode/1f7eb.png?v8",brunei:"unicode/1f1e7-1f1f3.png?v8",bubble_tea:"unicode/1f9cb.png?v8",bucket:"unicode/1faa3.png?v8",bug:"unicode/1f41b.png?v8",building_construction:"unicode/1f3d7.png?v8",bulb:"unicode/1f4a1.png?v8",bulgaria:"unicode/1f1e7-1f1ec.png?v8",bullettrain_front:"unicode/1f685.png?v8",bullettrain_side:"unicode/1f684.png?v8",burkina_faso:"unicode/1f1e7-1f1eb.png?v8",burrito:"unicode/1f32f.png?v8",burundi:"unicode/1f1e7-1f1ee.png?v8",bus:"unicode/1f68c.png?v8",business_suit_levitating:"unicode/1f574.png?v8",busstop:"unicode/1f68f.png?v8",bust_in_silhouette:"unicode/1f464.png?v8",busts_in_silhouette:"unicode/1f465.png?v8",butter:"unicode/1f9c8.png?v8",butterfly:"unicode/1f98b.png?v8",cactus:"unicode/1f335.png?v8",cake:"unicode/1f370.png?v8",calendar:"unicode/1f4c6.png?v8",call_me_hand:"unicode/1f919.png?v8",calling:"unicode/1f4f2.png?v8",cambodia:"unicode/1f1f0-1f1ed.png?v8",camel:"unicode/1f42b.png?v8",camera:"unicode/1f4f7.png?v8",camera_flash:"unicode/1f4f8.png?v8",cameroon:"unicode/1f1e8-1f1f2.png?v8",camping:"unicode/1f3d5.png?v8",canada:"unicode/1f1e8-1f1e6.png?v8",canary_islands:"unicode/1f1ee-1f1e8.png?v8",cancer:"unicode/264b.png?v8",candle:"unicode/1f56f.png?v8",candy:"unicode/1f36c.png?v8",canned_food:"unicode/1f96b.png?v8",canoe:"unicode/1f6f6.png?v8",cape_verde:"unicode/1f1e8-1f1fb.png?v8",capital_abcd:"unicode/1f520.png?v8",capricorn:"unicode/2651.png?v8",car:"unicode/1f697.png?v8",card_file_box:"unicode/1f5c3.png?v8",card_index:"unicode/1f4c7.png?v8",card_index_dividers:"unicode/1f5c2.png?v8",caribbean_netherlands:"unicode/1f1e7-1f1f6.png?v8",carousel_horse:"unicode/1f3a0.png?v8",carpentry_saw:"unicode/1fa9a.png?v8",carrot:"unicode/1f955.png?v8",cartwheeling:"unicode/1f938.png?v8",cat:"unicode/1f431.png?v8",cat2:"unicode/1f408.png?v8",cayman_islands:"unicode/1f1f0-1f1fe.png?v8",cd:"unicode/1f4bf.png?v8",central_african_republic:"unicode/1f1e8-1f1eb.png?v8",ceuta_melilla:"unicode/1f1ea-1f1e6.png?v8",chad:"unicode/1f1f9-1f1e9.png?v8",chains:"unicode/26d3.png?v8",chair:"unicode/1fa91.png?v8",champagne:"unicode/1f37e.png?v8",chart:"unicode/1f4b9.png?v8",chart_with_downwards_trend:"unicode/1f4c9.png?v8",chart_with_upwards_trend:"unicode/1f4c8.png?v8",checkered_flag:"unicode/1f3c1.png?v8",cheese:"unicode/1f9c0.png?v8",cherries:"unicode/1f352.png?v8",cherry_blossom:"unicode/1f338.png?v8",chess_pawn:"unicode/265f.png?v8",chestnut:"unicode/1f330.png?v8",chicken:"unicode/1f414.png?v8",child:"unicode/1f9d2.png?v8",children_crossing:"unicode/1f6b8.png?v8",chile:"unicode/1f1e8-1f1f1.png?v8",chipmunk:"unicode/1f43f.png?v8",chocolate_bar:"unicode/1f36b.png?v8",chopsticks:"unicode/1f962.png?v8",christmas_island:"unicode/1f1e8-1f1fd.png?v8",christmas_tree:"unicode/1f384.png?v8",church:"unicode/26ea.png?v8",cinema:"unicode/1f3a6.png?v8",circus_tent:"unicode/1f3aa.png?v8",city_sunrise:"unicode/1f307.png?v8",city_sunset:"unicode/1f306.png?v8",cityscape:"unicode/1f3d9.png?v8",cl:"unicode/1f191.png?v8",clamp:"unicode/1f5dc.png?v8",clap:"unicode/1f44f.png?v8",clapper:"unicode/1f3ac.png?v8",classical_building:"unicode/1f3db.png?v8",climbing:"unicode/1f9d7.png?v8",climbing_man:"unicode/1f9d7-2642.png?v8",climbing_woman:"unicode/1f9d7-2640.png?v8",clinking_glasses:"unicode/1f942.png?v8",clipboard:"unicode/1f4cb.png?v8",clipperton_island:"unicode/1f1e8-1f1f5.png?v8",clock1:"unicode/1f550.png?v8",clock10:"unicode/1f559.png?v8",clock1030:"unicode/1f565.png?v8",clock11:"unicode/1f55a.png?v8",clock1130:"unicode/1f566.png?v8",clock12:"unicode/1f55b.png?v8",clock1230:"unicode/1f567.png?v8",clock130:"unicode/1f55c.png?v8",clock2:"unicode/1f551.png?v8",clock230:"unicode/1f55d.png?v8",clock3:"unicode/1f552.png?v8",clock330:"unicode/1f55e.png?v8",clock4:"unicode/1f553.png?v8",clock430:"unicode/1f55f.png?v8",clock5:"unicode/1f554.png?v8",clock530:"unicode/1f560.png?v8",clock6:"unicode/1f555.png?v8",clock630:"unicode/1f561.png?v8",clock7:"unicode/1f556.png?v8",clock730:"unicode/1f562.png?v8",clock8:"unicode/1f557.png?v8",clock830:"unicode/1f563.png?v8",clock9:"unicode/1f558.png?v8",clock930:"unicode/1f564.png?v8",closed_book:"unicode/1f4d5.png?v8",closed_lock_with_key:"unicode/1f510.png?v8",closed_umbrella:"unicode/1f302.png?v8",cloud:"unicode/2601.png?v8",cloud_with_lightning:"unicode/1f329.png?v8",cloud_with_lightning_and_rain:"unicode/26c8.png?v8",cloud_with_rain:"unicode/1f327.png?v8",cloud_with_snow:"unicode/1f328.png?v8",clown_face:"unicode/1f921.png?v8",clubs:"unicode/2663.png?v8",cn:"unicode/1f1e8-1f1f3.png?v8",coat:"unicode/1f9e5.png?v8",cockroach:"unicode/1fab3.png?v8",cocktail:"unicode/1f378.png?v8",coconut:"unicode/1f965.png?v8",cocos_islands:"unicode/1f1e8-1f1e8.png?v8",coffee:"unicode/2615.png?v8",coffin:"unicode/26b0.png?v8",coin:"unicode/1fa99.png?v8",cold_face:"unicode/1f976.png?v8",cold_sweat:"unicode/1f630.png?v8",collision:"unicode/1f4a5.png?v8",colombia:"unicode/1f1e8-1f1f4.png?v8",comet:"unicode/2604.png?v8",comoros:"unicode/1f1f0-1f1f2.png?v8",compass:"unicode/1f9ed.png?v8",computer:"unicode/1f4bb.png?v8",computer_mouse:"unicode/1f5b1.png?v8",confetti_ball:"unicode/1f38a.png?v8",confounded:"unicode/1f616.png?v8",confused:"unicode/1f615.png?v8",congo_brazzaville:"unicode/1f1e8-1f1ec.png?v8",congo_kinshasa:"unicode/1f1e8-1f1e9.png?v8",congratulations:"unicode/3297.png?v8",construction:"unicode/1f6a7.png?v8",construction_worker:"unicode/1f477.png?v8",construction_worker_man:"unicode/1f477-2642.png?v8",construction_worker_woman:"unicode/1f477-2640.png?v8",control_knobs:"unicode/1f39b.png?v8",convenience_store:"unicode/1f3ea.png?v8",cook:"unicode/1f9d1-1f373.png?v8",cook_islands:"unicode/1f1e8-1f1f0.png?v8",cookie:"unicode/1f36a.png?v8",cool:"unicode/1f192.png?v8",cop:"unicode/1f46e.png?v8",copyright:"unicode/00a9.png?v8",corn:"unicode/1f33d.png?v8",costa_rica:"unicode/1f1e8-1f1f7.png?v8",cote_divoire:"unicode/1f1e8-1f1ee.png?v8",couch_and_lamp:"unicode/1f6cb.png?v8",couple:"unicode/1f46b.png?v8",couple_with_heart:"unicode/1f491.png?v8",couple_with_heart_man_man:"unicode/1f468-2764-1f468.png?v8",couple_with_heart_woman_man:"unicode/1f469-2764-1f468.png?v8",couple_with_heart_woman_woman:"unicode/1f469-2764-1f469.png?v8",couplekiss:"unicode/1f48f.png?v8",couplekiss_man_man:"unicode/1f468-2764-1f48b-1f468.png?v8",couplekiss_man_woman:"unicode/1f469-2764-1f48b-1f468.png?v8",couplekiss_woman_woman:"unicode/1f469-2764-1f48b-1f469.png?v8",cow:"unicode/1f42e.png?v8",cow2:"unicode/1f404.png?v8",cowboy_hat_face:"unicode/1f920.png?v8",crab:"unicode/1f980.png?v8",crayon:"unicode/1f58d.png?v8",credit_card:"unicode/1f4b3.png?v8",crescent_moon:"unicode/1f319.png?v8",cricket:"unicode/1f997.png?v8",cricket_game:"unicode/1f3cf.png?v8",croatia:"unicode/1f1ed-1f1f7.png?v8",crocodile:"unicode/1f40a.png?v8",croissant:"unicode/1f950.png?v8",crossed_fingers:"unicode/1f91e.png?v8",crossed_flags:"unicode/1f38c.png?v8",crossed_swords:"unicode/2694.png?v8",crown:"unicode/1f451.png?v8",cry:"unicode/1f622.png?v8",crying_cat_face:"unicode/1f63f.png?v8",crystal_ball:"unicode/1f52e.png?v8",cuba:"unicode/1f1e8-1f1fa.png?v8",cucumber:"unicode/1f952.png?v8",cup_with_straw:"unicode/1f964.png?v8",cupcake:"unicode/1f9c1.png?v8",cupid:"unicode/1f498.png?v8",curacao:"unicode/1f1e8-1f1fc.png?v8",curling_stone:"unicode/1f94c.png?v8",curly_haired_man:"unicode/1f468-1f9b1.png?v8",curly_haired_woman:"unicode/1f469-1f9b1.png?v8",curly_loop:"unicode/27b0.png?v8",currency_exchange:"unicode/1f4b1.png?v8",curry:"unicode/1f35b.png?v8",cursing_face:"unicode/1f92c.png?v8",custard:"unicode/1f36e.png?v8",customs:"unicode/1f6c3.png?v8",cut_of_meat:"unicode/1f969.png?v8",cyclone:"unicode/1f300.png?v8",cyprus:"unicode/1f1e8-1f1fe.png?v8",czech_republic:"unicode/1f1e8-1f1ff.png?v8",dagger:"unicode/1f5e1.png?v8",dancer:"unicode/1f483.png?v8",dancers:"unicode/1f46f.png?v8",dancing_men:"unicode/1f46f-2642.png?v8",dancing_women:"unicode/1f46f-2640.png?v8",dango:"unicode/1f361.png?v8",dark_sunglasses:"unicode/1f576.png?v8",dart:"unicode/1f3af.png?v8",dash:"unicode/1f4a8.png?v8",date:"unicode/1f4c5.png?v8",de:"unicode/1f1e9-1f1ea.png?v8",deaf_man:"unicode/1f9cf-2642.png?v8",deaf_person:"unicode/1f9cf.png?v8",deaf_woman:"unicode/1f9cf-2640.png?v8",deciduous_tree:"unicode/1f333.png?v8",deer:"unicode/1f98c.png?v8",denmark:"unicode/1f1e9-1f1f0.png?v8",department_store:"unicode/1f3ec.png?v8",derelict_house:"unicode/1f3da.png?v8",desert:"unicode/1f3dc.png?v8",desert_island:"unicode/1f3dd.png?v8",desktop_computer:"unicode/1f5a5.png?v8",detective:"unicode/1f575.png?v8",diamond_shape_with_a_dot_inside:"unicode/1f4a0.png?v8",diamonds:"unicode/2666.png?v8",diego_garcia:"unicode/1f1e9-1f1ec.png?v8",disappointed:"unicode/1f61e.png?v8",disappointed_relieved:"unicode/1f625.png?v8",disguised_face:"unicode/1f978.png?v8",diving_mask:"unicode/1f93f.png?v8",diya_lamp:"unicode/1fa94.png?v8",dizzy:"unicode/1f4ab.png?v8",dizzy_face:"unicode/1f635.png?v8",djibouti:"unicode/1f1e9-1f1ef.png?v8",dna:"unicode/1f9ec.png?v8",do_not_litter:"unicode/1f6af.png?v8",dodo:"unicode/1f9a4.png?v8",dog:"unicode/1f436.png?v8",dog2:"unicode/1f415.png?v8",dollar:"unicode/1f4b5.png?v8",dolls:"unicode/1f38e.png?v8",dolphin:"unicode/1f42c.png?v8",dominica:"unicode/1f1e9-1f1f2.png?v8",dominican_republic:"unicode/1f1e9-1f1f4.png?v8",door:"unicode/1f6aa.png?v8",doughnut:"unicode/1f369.png?v8",dove:"unicode/1f54a.png?v8",dragon:"unicode/1f409.png?v8",dragon_face:"unicode/1f432.png?v8",dress:"unicode/1f457.png?v8",dromedary_camel:"unicode/1f42a.png?v8",drooling_face:"unicode/1f924.png?v8",drop_of_blood:"unicode/1fa78.png?v8",droplet:"unicode/1f4a7.png?v8",drum:"unicode/1f941.png?v8",duck:"unicode/1f986.png?v8",dumpling:"unicode/1f95f.png?v8",dvd:"unicode/1f4c0.png?v8","e-mail":"unicode/1f4e7.png?v8",eagle:"unicode/1f985.png?v8",ear:"unicode/1f442.png?v8",ear_of_rice:"unicode/1f33e.png?v8",ear_with_hearing_aid:"unicode/1f9bb.png?v8",earth_africa:"unicode/1f30d.png?v8",earth_americas:"unicode/1f30e.png?v8",earth_asia:"unicode/1f30f.png?v8",ecuador:"unicode/1f1ea-1f1e8.png?v8",egg:"unicode/1f95a.png?v8",eggplant:"unicode/1f346.png?v8",egypt:"unicode/1f1ea-1f1ec.png?v8",eight:"unicode/0038-20e3.png?v8",eight_pointed_black_star:"unicode/2734.png?v8",eight_spoked_asterisk:"unicode/2733.png?v8",eject_button:"unicode/23cf.png?v8",el_salvador:"unicode/1f1f8-1f1fb.png?v8",electric_plug:"unicode/1f50c.png?v8",electron:"electron.png?v8",elephant:"unicode/1f418.png?v8",elevator:"unicode/1f6d7.png?v8",elf:"unicode/1f9dd.png?v8",elf_man:"unicode/1f9dd-2642.png?v8",elf_woman:"unicode/1f9dd-2640.png?v8",email:"unicode/1f4e7.png?v8",end:"unicode/1f51a.png?v8",england:"unicode/1f3f4-e0067-e0062-e0065-e006e-e0067-e007f.png?v8",envelope:"unicode/2709.png?v8",envelope_with_arrow:"unicode/1f4e9.png?v8",equatorial_guinea:"unicode/1f1ec-1f1f6.png?v8",eritrea:"unicode/1f1ea-1f1f7.png?v8",es:"unicode/1f1ea-1f1f8.png?v8",estonia:"unicode/1f1ea-1f1ea.png?v8",ethiopia:"unicode/1f1ea-1f1f9.png?v8",eu:"unicode/1f1ea-1f1fa.png?v8",euro:"unicode/1f4b6.png?v8",european_castle:"unicode/1f3f0.png?v8",european_post_office:"unicode/1f3e4.png?v8",european_union:"unicode/1f1ea-1f1fa.png?v8",evergreen_tree:"unicode/1f332.png?v8",exclamation:"unicode/2757.png?v8",exploding_head:"unicode/1f92f.png?v8",expressionless:"unicode/1f611.png?v8",eye:"unicode/1f441.png?v8",eye_speech_bubble:"unicode/1f441-1f5e8.png?v8",eyeglasses:"unicode/1f453.png?v8",eyes:"unicode/1f440.png?v8",face_exhaling:"unicode/1f62e-1f4a8.png?v8",face_in_clouds:"unicode/1f636-1f32b.png?v8",face_with_head_bandage:"unicode/1f915.png?v8",face_with_spiral_eyes:"unicode/1f635-1f4ab.png?v8",face_with_thermometer:"unicode/1f912.png?v8",facepalm:"unicode/1f926.png?v8",facepunch:"unicode/1f44a.png?v8",factory:"unicode/1f3ed.png?v8",factory_worker:"unicode/1f9d1-1f3ed.png?v8",fairy:"unicode/1f9da.png?v8",fairy_man:"unicode/1f9da-2642.png?v8",fairy_woman:"unicode/1f9da-2640.png?v8",falafel:"unicode/1f9c6.png?v8",falkland_islands:"unicode/1f1eb-1f1f0.png?v8",fallen_leaf:"unicode/1f342.png?v8",family:"unicode/1f46a.png?v8",family_man_boy:"unicode/1f468-1f466.png?v8",family_man_boy_boy:"unicode/1f468-1f466-1f466.png?v8",family_man_girl:"unicode/1f468-1f467.png?v8",family_man_girl_boy:"unicode/1f468-1f467-1f466.png?v8",family_man_girl_girl:"unicode/1f468-1f467-1f467.png?v8",family_man_man_boy:"unicode/1f468-1f468-1f466.png?v8",family_man_man_boy_boy:"unicode/1f468-1f468-1f466-1f466.png?v8",family_man_man_girl:"unicode/1f468-1f468-1f467.png?v8",family_man_man_girl_boy:"unicode/1f468-1f468-1f467-1f466.png?v8",family_man_man_girl_girl:"unicode/1f468-1f468-1f467-1f467.png?v8",family_man_woman_boy:"unicode/1f468-1f469-1f466.png?v8",family_man_woman_boy_boy:"unicode/1f468-1f469-1f466-1f466.png?v8",family_man_woman_girl:"unicode/1f468-1f469-1f467.png?v8",family_man_woman_girl_boy:"unicode/1f468-1f469-1f467-1f466.png?v8",family_man_woman_girl_girl:"unicode/1f468-1f469-1f467-1f467.png?v8",family_woman_boy:"unicode/1f469-1f466.png?v8",family_woman_boy_boy:"unicode/1f469-1f466-1f466.png?v8",family_woman_girl:"unicode/1f469-1f467.png?v8",family_woman_girl_boy:"unicode/1f469-1f467-1f466.png?v8",family_woman_girl_girl:"unicode/1f469-1f467-1f467.png?v8",family_woman_woman_boy:"unicode/1f469-1f469-1f466.png?v8",family_woman_woman_boy_boy:"unicode/1f469-1f469-1f466-1f466.png?v8",family_woman_woman_girl:"unicode/1f469-1f469-1f467.png?v8",family_woman_woman_girl_boy:"unicode/1f469-1f469-1f467-1f466.png?v8",family_woman_woman_girl_girl:"unicode/1f469-1f469-1f467-1f467.png?v8",farmer:"unicode/1f9d1-1f33e.png?v8",faroe_islands:"unicode/1f1eb-1f1f4.png?v8",fast_forward:"unicode/23e9.png?v8",fax:"unicode/1f4e0.png?v8",fearful:"unicode/1f628.png?v8",feather:"unicode/1fab6.png?v8",feelsgood:"feelsgood.png?v8",feet:"unicode/1f43e.png?v8",female_detective:"unicode/1f575-2640.png?v8",female_sign:"unicode/2640.png?v8",ferris_wheel:"unicode/1f3a1.png?v8",ferry:"unicode/26f4.png?v8",field_hockey:"unicode/1f3d1.png?v8",fiji:"unicode/1f1eb-1f1ef.png?v8",file_cabinet:"unicode/1f5c4.png?v8",file_folder:"unicode/1f4c1.png?v8",film_projector:"unicode/1f4fd.png?v8",film_strip:"unicode/1f39e.png?v8",finland:"unicode/1f1eb-1f1ee.png?v8",finnadie:"finnadie.png?v8",fire:"unicode/1f525.png?v8",fire_engine:"unicode/1f692.png?v8",fire_extinguisher:"unicode/1f9ef.png?v8",firecracker:"unicode/1f9e8.png?v8",firefighter:"unicode/1f9d1-1f692.png?v8",fireworks:"unicode/1f386.png?v8",first_quarter_moon:"unicode/1f313.png?v8",first_quarter_moon_with_face:"unicode/1f31b.png?v8",fish:"unicode/1f41f.png?v8",fish_cake:"unicode/1f365.png?v8",fishing_pole_and_fish:"unicode/1f3a3.png?v8",fist:"unicode/270a.png?v8",fist_left:"unicode/1f91b.png?v8",fist_oncoming:"unicode/1f44a.png?v8",fist_raised:"unicode/270a.png?v8",fist_right:"unicode/1f91c.png?v8",five:"unicode/0035-20e3.png?v8",flags:"unicode/1f38f.png?v8",flamingo:"unicode/1f9a9.png?v8",flashlight:"unicode/1f526.png?v8",flat_shoe:"unicode/1f97f.png?v8",flatbread:"unicode/1fad3.png?v8",fleur_de_lis:"unicode/269c.png?v8",flight_arrival:"unicode/1f6ec.png?v8",flight_departure:"unicode/1f6eb.png?v8",flipper:"unicode/1f42c.png?v8",floppy_disk:"unicode/1f4be.png?v8",flower_playing_cards:"unicode/1f3b4.png?v8",flushed:"unicode/1f633.png?v8",fly:"unicode/1fab0.png?v8",flying_disc:"unicode/1f94f.png?v8",flying_saucer:"unicode/1f6f8.png?v8",fog:"unicode/1f32b.png?v8",foggy:"unicode/1f301.png?v8",fondue:"unicode/1fad5.png?v8",foot:"unicode/1f9b6.png?v8",football:"unicode/1f3c8.png?v8",footprints:"unicode/1f463.png?v8",fork_and_knife:"unicode/1f374.png?v8",fortune_cookie:"unicode/1f960.png?v8",fountain:"unicode/26f2.png?v8",fountain_pen:"unicode/1f58b.png?v8",four:"unicode/0034-20e3.png?v8",four_leaf_clover:"unicode/1f340.png?v8",fox_face:"unicode/1f98a.png?v8",fr:"unicode/1f1eb-1f1f7.png?v8",framed_picture:"unicode/1f5bc.png?v8",free:"unicode/1f193.png?v8",french_guiana:"unicode/1f1ec-1f1eb.png?v8",french_polynesia:"unicode/1f1f5-1f1eb.png?v8",french_southern_territories:"unicode/1f1f9-1f1eb.png?v8",fried_egg:"unicode/1f373.png?v8",fried_shrimp:"unicode/1f364.png?v8",fries:"unicode/1f35f.png?v8",frog:"unicode/1f438.png?v8",frowning:"unicode/1f626.png?v8",frowning_face:"unicode/2639.png?v8",frowning_man:"unicode/1f64d-2642.png?v8",frowning_person:"unicode/1f64d.png?v8",frowning_woman:"unicode/1f64d-2640.png?v8",fu:"unicode/1f595.png?v8",fuelpump:"unicode/26fd.png?v8",full_moon:"unicode/1f315.png?v8",full_moon_with_face:"unicode/1f31d.png?v8",funeral_urn:"unicode/26b1.png?v8",gabon:"unicode/1f1ec-1f1e6.png?v8",gambia:"unicode/1f1ec-1f1f2.png?v8",game_die:"unicode/1f3b2.png?v8",garlic:"unicode/1f9c4.png?v8",gb:"unicode/1f1ec-1f1e7.png?v8",gear:"unicode/2699.png?v8",gem:"unicode/1f48e.png?v8",gemini:"unicode/264a.png?v8",genie:"unicode/1f9de.png?v8",genie_man:"unicode/1f9de-2642.png?v8",genie_woman:"unicode/1f9de-2640.png?v8",georgia:"unicode/1f1ec-1f1ea.png?v8",ghana:"unicode/1f1ec-1f1ed.png?v8",ghost:"unicode/1f47b.png?v8",gibraltar:"unicode/1f1ec-1f1ee.png?v8",gift:"unicode/1f381.png?v8",gift_heart:"unicode/1f49d.png?v8",giraffe:"unicode/1f992.png?v8",girl:"unicode/1f467.png?v8",globe_with_meridians:"unicode/1f310.png?v8",gloves:"unicode/1f9e4.png?v8",goal_net:"unicode/1f945.png?v8",goat:"unicode/1f410.png?v8",goberserk:"goberserk.png?v8",godmode:"godmode.png?v8",goggles:"unicode/1f97d.png?v8",golf:"unicode/26f3.png?v8",golfing:"unicode/1f3cc.png?v8",golfing_man:"unicode/1f3cc-2642.png?v8",golfing_woman:"unicode/1f3cc-2640.png?v8",gorilla:"unicode/1f98d.png?v8",grapes:"unicode/1f347.png?v8",greece:"unicode/1f1ec-1f1f7.png?v8",green_apple:"unicode/1f34f.png?v8",green_book:"unicode/1f4d7.png?v8",green_circle:"unicode/1f7e2.png?v8",green_heart:"unicode/1f49a.png?v8",green_salad:"unicode/1f957.png?v8",green_square:"unicode/1f7e9.png?v8",greenland:"unicode/1f1ec-1f1f1.png?v8",grenada:"unicode/1f1ec-1f1e9.png?v8",grey_exclamation:"unicode/2755.png?v8",grey_question:"unicode/2754.png?v8",grimacing:"unicode/1f62c.png?v8",grin:"unicode/1f601.png?v8",grinning:"unicode/1f600.png?v8",guadeloupe:"unicode/1f1ec-1f1f5.png?v8",guam:"unicode/1f1ec-1f1fa.png?v8",guard:"unicode/1f482.png?v8",guardsman:"unicode/1f482-2642.png?v8",guardswoman:"unicode/1f482-2640.png?v8",guatemala:"unicode/1f1ec-1f1f9.png?v8",guernsey:"unicode/1f1ec-1f1ec.png?v8",guide_dog:"unicode/1f9ae.png?v8",guinea:"unicode/1f1ec-1f1f3.png?v8",guinea_bissau:"unicode/1f1ec-1f1fc.png?v8",guitar:"unicode/1f3b8.png?v8",gun:"unicode/1f52b.png?v8",guyana:"unicode/1f1ec-1f1fe.png?v8",haircut:"unicode/1f487.png?v8",haircut_man:"unicode/1f487-2642.png?v8",haircut_woman:"unicode/1f487-2640.png?v8",haiti:"unicode/1f1ed-1f1f9.png?v8",hamburger:"unicode/1f354.png?v8",hammer:"unicode/1f528.png?v8",hammer_and_pick:"unicode/2692.png?v8",hammer_and_wrench:"unicode/1f6e0.png?v8",hamster:"unicode/1f439.png?v8",hand:"unicode/270b.png?v8",hand_over_mouth:"unicode/1f92d.png?v8",handbag:"unicode/1f45c.png?v8",handball_person:"unicode/1f93e.png?v8",handshake:"unicode/1f91d.png?v8",hankey:"unicode/1f4a9.png?v8",hash:"unicode/0023-20e3.png?v8",hatched_chick:"unicode/1f425.png?v8",hatching_chick:"unicode/1f423.png?v8",headphones:"unicode/1f3a7.png?v8",headstone:"unicode/1faa6.png?v8",health_worker:"unicode/1f9d1-2695.png?v8",hear_no_evil:"unicode/1f649.png?v8",heard_mcdonald_islands:"unicode/1f1ed-1f1f2.png?v8",heart:"unicode/2764.png?v8",heart_decoration:"unicode/1f49f.png?v8",heart_eyes:"unicode/1f60d.png?v8",heart_eyes_cat:"unicode/1f63b.png?v8",heart_on_fire:"unicode/2764-1f525.png?v8",heartbeat:"unicode/1f493.png?v8",heartpulse:"unicode/1f497.png?v8",hearts:"unicode/2665.png?v8",heavy_check_mark:"unicode/2714.png?v8",heavy_division_sign:"unicode/2797.png?v8",heavy_dollar_sign:"unicode/1f4b2.png?v8",heavy_exclamation_mark:"unicode/2757.png?v8",heavy_heart_exclamation:"unicode/2763.png?v8",heavy_minus_sign:"unicode/2796.png?v8",heavy_multiplication_x:"unicode/2716.png?v8",heavy_plus_sign:"unicode/2795.png?v8",hedgehog:"unicode/1f994.png?v8",helicopter:"unicode/1f681.png?v8",herb:"unicode/1f33f.png?v8",hibiscus:"unicode/1f33a.png?v8",high_brightness:"unicode/1f506.png?v8",high_heel:"unicode/1f460.png?v8",hiking_boot:"unicode/1f97e.png?v8",hindu_temple:"unicode/1f6d5.png?v8",hippopotamus:"unicode/1f99b.png?v8",hocho:"unicode/1f52a.png?v8",hole:"unicode/1f573.png?v8",honduras:"unicode/1f1ed-1f1f3.png?v8",honey_pot:"unicode/1f36f.png?v8",honeybee:"unicode/1f41d.png?v8",hong_kong:"unicode/1f1ed-1f1f0.png?v8",hook:"unicode/1fa9d.png?v8",horse:"unicode/1f434.png?v8",horse_racing:"unicode/1f3c7.png?v8",hospital:"unicode/1f3e5.png?v8",hot_face:"unicode/1f975.png?v8",hot_pepper:"unicode/1f336.png?v8",hotdog:"unicode/1f32d.png?v8",hotel:"unicode/1f3e8.png?v8",hotsprings:"unicode/2668.png?v8",hourglass:"unicode/231b.png?v8",hourglass_flowing_sand:"unicode/23f3.png?v8",house:"unicode/1f3e0.png?v8",house_with_garden:"unicode/1f3e1.png?v8",houses:"unicode/1f3d8.png?v8",hugs:"unicode/1f917.png?v8",hungary:"unicode/1f1ed-1f1fa.png?v8",hurtrealbad:"hurtrealbad.png?v8",hushed:"unicode/1f62f.png?v8",hut:"unicode/1f6d6.png?v8",ice_cream:"unicode/1f368.png?v8",ice_cube:"unicode/1f9ca.png?v8",ice_hockey:"unicode/1f3d2.png?v8",ice_skate:"unicode/26f8.png?v8",icecream:"unicode/1f366.png?v8",iceland:"unicode/1f1ee-1f1f8.png?v8",id:"unicode/1f194.png?v8",ideograph_advantage:"unicode/1f250.png?v8",imp:"unicode/1f47f.png?v8",inbox_tray:"unicode/1f4e5.png?v8",incoming_envelope:"unicode/1f4e8.png?v8",india:"unicode/1f1ee-1f1f3.png?v8",indonesia:"unicode/1f1ee-1f1e9.png?v8",infinity:"unicode/267e.png?v8",information_desk_person:"unicode/1f481.png?v8",information_source:"unicode/2139.png?v8",innocent:"unicode/1f607.png?v8",interrobang:"unicode/2049.png?v8",iphone:"unicode/1f4f1.png?v8",iran:"unicode/1f1ee-1f1f7.png?v8",iraq:"unicode/1f1ee-1f1f6.png?v8",ireland:"unicode/1f1ee-1f1ea.png?v8",isle_of_man:"unicode/1f1ee-1f1f2.png?v8",israel:"unicode/1f1ee-1f1f1.png?v8",it:"unicode/1f1ee-1f1f9.png?v8",izakaya_lantern:"unicode/1f3ee.png?v8",jack_o_lantern:"unicode/1f383.png?v8",jamaica:"unicode/1f1ef-1f1f2.png?v8",japan:"unicode/1f5fe.png?v8",japanese_castle:"unicode/1f3ef.png?v8",japanese_goblin:"unicode/1f47a.png?v8",japanese_ogre:"unicode/1f479.png?v8",jeans:"unicode/1f456.png?v8",jersey:"unicode/1f1ef-1f1ea.png?v8",jigsaw:"unicode/1f9e9.png?v8",jordan:"unicode/1f1ef-1f1f4.png?v8",joy:"unicode/1f602.png?v8",joy_cat:"unicode/1f639.png?v8",joystick:"unicode/1f579.png?v8",jp:"unicode/1f1ef-1f1f5.png?v8",judge:"unicode/1f9d1-2696.png?v8",juggling_person:"unicode/1f939.png?v8",kaaba:"unicode/1f54b.png?v8",kangaroo:"unicode/1f998.png?v8",kazakhstan:"unicode/1f1f0-1f1ff.png?v8",kenya:"unicode/1f1f0-1f1ea.png?v8",key:"unicode/1f511.png?v8",keyboard:"unicode/2328.png?v8",keycap_ten:"unicode/1f51f.png?v8",kick_scooter:"unicode/1f6f4.png?v8",kimono:"unicode/1f458.png?v8",kiribati:"unicode/1f1f0-1f1ee.png?v8",kiss:"unicode/1f48b.png?v8",kissing:"unicode/1f617.png?v8",kissing_cat:"unicode/1f63d.png?v8",kissing_closed_eyes:"unicode/1f61a.png?v8",kissing_heart:"unicode/1f618.png?v8",kissing_smiling_eyes:"unicode/1f619.png?v8",kite:"unicode/1fa81.png?v8",kiwi_fruit:"unicode/1f95d.png?v8",kneeling_man:"unicode/1f9ce-2642.png?v8",kneeling_person:"unicode/1f9ce.png?v8",kneeling_woman:"unicode/1f9ce-2640.png?v8",knife:"unicode/1f52a.png?v8",knot:"unicode/1faa2.png?v8",koala:"unicode/1f428.png?v8",koko:"unicode/1f201.png?v8",kosovo:"unicode/1f1fd-1f1f0.png?v8",kr:"unicode/1f1f0-1f1f7.png?v8",kuwait:"unicode/1f1f0-1f1fc.png?v8",kyrgyzstan:"unicode/1f1f0-1f1ec.png?v8",lab_coat:"unicode/1f97c.png?v8",label:"unicode/1f3f7.png?v8",lacrosse:"unicode/1f94d.png?v8",ladder:"unicode/1fa9c.png?v8",lady_beetle:"unicode/1f41e.png?v8",lantern:"unicode/1f3ee.png?v8",laos:"unicode/1f1f1-1f1e6.png?v8",large_blue_circle:"unicode/1f535.png?v8",large_blue_diamond:"unicode/1f537.png?v8",large_orange_diamond:"unicode/1f536.png?v8",last_quarter_moon:"unicode/1f317.png?v8",last_quarter_moon_with_face:"unicode/1f31c.png?v8",latin_cross:"unicode/271d.png?v8",latvia:"unicode/1f1f1-1f1fb.png?v8",laughing:"unicode/1f606.png?v8",leafy_green:"unicode/1f96c.png?v8",leaves:"unicode/1f343.png?v8",lebanon:"unicode/1f1f1-1f1e7.png?v8",ledger:"unicode/1f4d2.png?v8",left_luggage:"unicode/1f6c5.png?v8",left_right_arrow:"unicode/2194.png?v8",left_speech_bubble:"unicode/1f5e8.png?v8",leftwards_arrow_with_hook:"unicode/21a9.png?v8",leg:"unicode/1f9b5.png?v8",lemon:"unicode/1f34b.png?v8",leo:"unicode/264c.png?v8",leopard:"unicode/1f406.png?v8",lesotho:"unicode/1f1f1-1f1f8.png?v8",level_slider:"unicode/1f39a.png?v8",liberia:"unicode/1f1f1-1f1f7.png?v8",libra:"unicode/264e.png?v8",libya:"unicode/1f1f1-1f1fe.png?v8",liechtenstein:"unicode/1f1f1-1f1ee.png?v8",light_rail:"unicode/1f688.png?v8",link:"unicode/1f517.png?v8",lion:"unicode/1f981.png?v8",lips:"unicode/1f444.png?v8",lipstick:"unicode/1f484.png?v8",lithuania:"unicode/1f1f1-1f1f9.png?v8",lizard:"unicode/1f98e.png?v8",llama:"unicode/1f999.png?v8",lobster:"unicode/1f99e.png?v8",lock:"unicode/1f512.png?v8",lock_with_ink_pen:"unicode/1f50f.png?v8",lollipop:"unicode/1f36d.png?v8",long_drum:"unicode/1fa98.png?v8",loop:"unicode/27bf.png?v8",lotion_bottle:"unicode/1f9f4.png?v8",lotus_position:"unicode/1f9d8.png?v8",lotus_position_man:"unicode/1f9d8-2642.png?v8",lotus_position_woman:"unicode/1f9d8-2640.png?v8",loud_sound:"unicode/1f50a.png?v8",loudspeaker:"unicode/1f4e2.png?v8",love_hotel:"unicode/1f3e9.png?v8",love_letter:"unicode/1f48c.png?v8",love_you_gesture:"unicode/1f91f.png?v8",low_brightness:"unicode/1f505.png?v8",luggage:"unicode/1f9f3.png?v8",lungs:"unicode/1fac1.png?v8",luxembourg:"unicode/1f1f1-1f1fa.png?v8",lying_face:"unicode/1f925.png?v8",m:"unicode/24c2.png?v8",macau:"unicode/1f1f2-1f1f4.png?v8",macedonia:"unicode/1f1f2-1f1f0.png?v8",madagascar:"unicode/1f1f2-1f1ec.png?v8",mag:"unicode/1f50d.png?v8",mag_right:"unicode/1f50e.png?v8",mage:"unicode/1f9d9.png?v8",mage_man:"unicode/1f9d9-2642.png?v8",mage_woman:"unicode/1f9d9-2640.png?v8",magic_wand:"unicode/1fa84.png?v8",magnet:"unicode/1f9f2.png?v8",mahjong:"unicode/1f004.png?v8",mailbox:"unicode/1f4eb.png?v8",mailbox_closed:"unicode/1f4ea.png?v8",mailbox_with_mail:"unicode/1f4ec.png?v8",mailbox_with_no_mail:"unicode/1f4ed.png?v8",malawi:"unicode/1f1f2-1f1fc.png?v8",malaysia:"unicode/1f1f2-1f1fe.png?v8",maldives:"unicode/1f1f2-1f1fb.png?v8",male_detective:"unicode/1f575-2642.png?v8",male_sign:"unicode/2642.png?v8",mali:"unicode/1f1f2-1f1f1.png?v8",malta:"unicode/1f1f2-1f1f9.png?v8",mammoth:"unicode/1f9a3.png?v8",man:"unicode/1f468.png?v8",man_artist:"unicode/1f468-1f3a8.png?v8",man_astronaut:"unicode/1f468-1f680.png?v8",man_beard:"unicode/1f9d4-2642.png?v8",man_cartwheeling:"unicode/1f938-2642.png?v8",man_cook:"unicode/1f468-1f373.png?v8",man_dancing:"unicode/1f57a.png?v8",man_facepalming:"unicode/1f926-2642.png?v8",man_factory_worker:"unicode/1f468-1f3ed.png?v8",man_farmer:"unicode/1f468-1f33e.png?v8",man_feeding_baby:"unicode/1f468-1f37c.png?v8",man_firefighter:"unicode/1f468-1f692.png?v8",man_health_worker:"unicode/1f468-2695.png?v8",man_in_manual_wheelchair:"unicode/1f468-1f9bd.png?v8",man_in_motorized_wheelchair:"unicode/1f468-1f9bc.png?v8",man_in_tuxedo:"unicode/1f935-2642.png?v8",man_judge:"unicode/1f468-2696.png?v8",man_juggling:"unicode/1f939-2642.png?v8",man_mechanic:"unicode/1f468-1f527.png?v8",man_office_worker:"unicode/1f468-1f4bc.png?v8",man_pilot:"unicode/1f468-2708.png?v8",man_playing_handball:"unicode/1f93e-2642.png?v8",man_playing_water_polo:"unicode/1f93d-2642.png?v8",man_scientist:"unicode/1f468-1f52c.png?v8",man_shrugging:"unicode/1f937-2642.png?v8",man_singer:"unicode/1f468-1f3a4.png?v8",man_student:"unicode/1f468-1f393.png?v8",man_teacher:"unicode/1f468-1f3eb.png?v8",man_technologist:"unicode/1f468-1f4bb.png?v8",man_with_gua_pi_mao:"unicode/1f472.png?v8",man_with_probing_cane:"unicode/1f468-1f9af.png?v8",man_with_turban:"unicode/1f473-2642.png?v8",man_with_veil:"unicode/1f470-2642.png?v8",mandarin:"unicode/1f34a.png?v8",mango:"unicode/1f96d.png?v8",mans_shoe:"unicode/1f45e.png?v8",mantelpiece_clock:"unicode/1f570.png?v8",manual_wheelchair:"unicode/1f9bd.png?v8",maple_leaf:"unicode/1f341.png?v8",marshall_islands:"unicode/1f1f2-1f1ed.png?v8",martial_arts_uniform:"unicode/1f94b.png?v8",martinique:"unicode/1f1f2-1f1f6.png?v8",mask:"unicode/1f637.png?v8",massage:"unicode/1f486.png?v8",massage_man:"unicode/1f486-2642.png?v8",massage_woman:"unicode/1f486-2640.png?v8",mate:"unicode/1f9c9.png?v8",mauritania:"unicode/1f1f2-1f1f7.png?v8",mauritius:"unicode/1f1f2-1f1fa.png?v8",mayotte:"unicode/1f1fe-1f1f9.png?v8",meat_on_bone:"unicode/1f356.png?v8",mechanic:"unicode/1f9d1-1f527.png?v8",mechanical_arm:"unicode/1f9be.png?v8",mechanical_leg:"unicode/1f9bf.png?v8",medal_military:"unicode/1f396.png?v8",medal_sports:"unicode/1f3c5.png?v8",medical_symbol:"unicode/2695.png?v8",mega:"unicode/1f4e3.png?v8",melon:"unicode/1f348.png?v8",memo:"unicode/1f4dd.png?v8",men_wrestling:"unicode/1f93c-2642.png?v8",mending_heart:"unicode/2764-1fa79.png?v8",menorah:"unicode/1f54e.png?v8",mens:"unicode/1f6b9.png?v8",mermaid:"unicode/1f9dc-2640.png?v8",merman:"unicode/1f9dc-2642.png?v8",merperson:"unicode/1f9dc.png?v8",metal:"unicode/1f918.png?v8",metro:"unicode/1f687.png?v8",mexico:"unicode/1f1f2-1f1fd.png?v8",microbe:"unicode/1f9a0.png?v8",micronesia:"unicode/1f1eb-1f1f2.png?v8",microphone:"unicode/1f3a4.png?v8",microscope:"unicode/1f52c.png?v8",middle_finger:"unicode/1f595.png?v8",military_helmet:"unicode/1fa96.png?v8",milk_glass:"unicode/1f95b.png?v8",milky_way:"unicode/1f30c.png?v8",minibus:"unicode/1f690.png?v8",minidisc:"unicode/1f4bd.png?v8",mirror:"unicode/1fa9e.png?v8",mobile_phone_off:"unicode/1f4f4.png?v8",moldova:"unicode/1f1f2-1f1e9.png?v8",monaco:"unicode/1f1f2-1f1e8.png?v8",money_mouth_face:"unicode/1f911.png?v8",money_with_wings:"unicode/1f4b8.png?v8",moneybag:"unicode/1f4b0.png?v8",mongolia:"unicode/1f1f2-1f1f3.png?v8",monkey:"unicode/1f412.png?v8",monkey_face:"unicode/1f435.png?v8",monocle_face:"unicode/1f9d0.png?v8",monorail:"unicode/1f69d.png?v8",montenegro:"unicode/1f1f2-1f1ea.png?v8",montserrat:"unicode/1f1f2-1f1f8.png?v8",moon:"unicode/1f314.png?v8",moon_cake:"unicode/1f96e.png?v8",morocco:"unicode/1f1f2-1f1e6.png?v8",mortar_board:"unicode/1f393.png?v8",mosque:"unicode/1f54c.png?v8",mosquito:"unicode/1f99f.png?v8",motor_boat:"unicode/1f6e5.png?v8",motor_scooter:"unicode/1f6f5.png?v8",motorcycle:"unicode/1f3cd.png?v8",motorized_wheelchair:"unicode/1f9bc.png?v8",motorway:"unicode/1f6e3.png?v8",mount_fuji:"unicode/1f5fb.png?v8",mountain:"unicode/26f0.png?v8",mountain_bicyclist:"unicode/1f6b5.png?v8",mountain_biking_man:"unicode/1f6b5-2642.png?v8",mountain_biking_woman:"unicode/1f6b5-2640.png?v8",mountain_cableway:"unicode/1f6a0.png?v8",mountain_railway:"unicode/1f69e.png?v8",mountain_snow:"unicode/1f3d4.png?v8",mouse:"unicode/1f42d.png?v8",mouse2:"unicode/1f401.png?v8",mouse_trap:"unicode/1faa4.png?v8",movie_camera:"unicode/1f3a5.png?v8",moyai:"unicode/1f5ff.png?v8",mozambique:"unicode/1f1f2-1f1ff.png?v8",mrs_claus:"unicode/1f936.png?v8",muscle:"unicode/1f4aa.png?v8",mushroom:"unicode/1f344.png?v8",musical_keyboard:"unicode/1f3b9.png?v8",musical_note:"unicode/1f3b5.png?v8",musical_score:"unicode/1f3bc.png?v8",mute:"unicode/1f507.png?v8",mx_claus:"unicode/1f9d1-1f384.png?v8",myanmar:"unicode/1f1f2-1f1f2.png?v8",nail_care:"unicode/1f485.png?v8",name_badge:"unicode/1f4db.png?v8",namibia:"unicode/1f1f3-1f1e6.png?v8",national_park:"unicode/1f3de.png?v8",nauru:"unicode/1f1f3-1f1f7.png?v8",nauseated_face:"unicode/1f922.png?v8",nazar_amulet:"unicode/1f9ff.png?v8",neckbeard:"neckbeard.png?v8",necktie:"unicode/1f454.png?v8",negative_squared_cross_mark:"unicode/274e.png?v8",nepal:"unicode/1f1f3-1f1f5.png?v8",nerd_face:"unicode/1f913.png?v8",nesting_dolls:"unicode/1fa86.png?v8",netherlands:"unicode/1f1f3-1f1f1.png?v8",neutral_face:"unicode/1f610.png?v8",new:"unicode/1f195.png?v8",new_caledonia:"unicode/1f1f3-1f1e8.png?v8",new_moon:"unicode/1f311.png?v8",new_moon_with_face:"unicode/1f31a.png?v8",new_zealand:"unicode/1f1f3-1f1ff.png?v8",newspaper:"unicode/1f4f0.png?v8",newspaper_roll:"unicode/1f5de.png?v8",next_track_button:"unicode/23ed.png?v8",ng:"unicode/1f196.png?v8",ng_man:"unicode/1f645-2642.png?v8",ng_woman:"unicode/1f645-2640.png?v8",nicaragua:"unicode/1f1f3-1f1ee.png?v8",niger:"unicode/1f1f3-1f1ea.png?v8",nigeria:"unicode/1f1f3-1f1ec.png?v8",night_with_stars:"unicode/1f303.png?v8",nine:"unicode/0039-20e3.png?v8",ninja:"unicode/1f977.png?v8",niue:"unicode/1f1f3-1f1fa.png?v8",no_bell:"unicode/1f515.png?v8",no_bicycles:"unicode/1f6b3.png?v8",no_entry:"unicode/26d4.png?v8",no_entry_sign:"unicode/1f6ab.png?v8",no_good:"unicode/1f645.png?v8",no_good_man:"unicode/1f645-2642.png?v8",no_good_woman:"unicode/1f645-2640.png?v8",no_mobile_phones:"unicode/1f4f5.png?v8",no_mouth:"unicode/1f636.png?v8",no_pedestrians:"unicode/1f6b7.png?v8",no_smoking:"unicode/1f6ad.png?v8","non-potable_water":"unicode/1f6b1.png?v8",norfolk_island:"unicode/1f1f3-1f1eb.png?v8",north_korea:"unicode/1f1f0-1f1f5.png?v8",northern_mariana_islands:"unicode/1f1f2-1f1f5.png?v8",norway:"unicode/1f1f3-1f1f4.png?v8",nose:"unicode/1f443.png?v8",notebook:"unicode/1f4d3.png?v8",notebook_with_decorative_cover:"unicode/1f4d4.png?v8",notes:"unicode/1f3b6.png?v8",nut_and_bolt:"unicode/1f529.png?v8",o:"unicode/2b55.png?v8",o2:"unicode/1f17e.png?v8",ocean:"unicode/1f30a.png?v8",octocat:"octocat.png?v8",octopus:"unicode/1f419.png?v8",oden:"unicode/1f362.png?v8",office:"unicode/1f3e2.png?v8",office_worker:"unicode/1f9d1-1f4bc.png?v8",oil_drum:"unicode/1f6e2.png?v8",ok:"unicode/1f197.png?v8",ok_hand:"unicode/1f44c.png?v8",ok_man:"unicode/1f646-2642.png?v8",ok_person:"unicode/1f646.png?v8",ok_woman:"unicode/1f646-2640.png?v8",old_key:"unicode/1f5dd.png?v8",older_adult:"unicode/1f9d3.png?v8",older_man:"unicode/1f474.png?v8",older_woman:"unicode/1f475.png?v8",olive:"unicode/1fad2.png?v8",om:"unicode/1f549.png?v8",oman:"unicode/1f1f4-1f1f2.png?v8",on:"unicode/1f51b.png?v8",oncoming_automobile:"unicode/1f698.png?v8",oncoming_bus:"unicode/1f68d.png?v8",oncoming_police_car:"unicode/1f694.png?v8",oncoming_taxi:"unicode/1f696.png?v8",one:"unicode/0031-20e3.png?v8",one_piece_swimsuit:"unicode/1fa71.png?v8",onion:"unicode/1f9c5.png?v8",open_book:"unicode/1f4d6.png?v8",open_file_folder:"unicode/1f4c2.png?v8",open_hands:"unicode/1f450.png?v8",open_mouth:"unicode/1f62e.png?v8",open_umbrella:"unicode/2602.png?v8",ophiuchus:"unicode/26ce.png?v8",orange:"unicode/1f34a.png?v8",orange_book:"unicode/1f4d9.png?v8",orange_circle:"unicode/1f7e0.png?v8",orange_heart:"unicode/1f9e1.png?v8",orange_square:"unicode/1f7e7.png?v8",orangutan:"unicode/1f9a7.png?v8",orthodox_cross:"unicode/2626.png?v8",otter:"unicode/1f9a6.png?v8",outbox_tray:"unicode/1f4e4.png?v8",owl:"unicode/1f989.png?v8",ox:"unicode/1f402.png?v8",oyster:"unicode/1f9aa.png?v8",package:"unicode/1f4e6.png?v8",page_facing_up:"unicode/1f4c4.png?v8",page_with_curl:"unicode/1f4c3.png?v8",pager:"unicode/1f4df.png?v8",paintbrush:"unicode/1f58c.png?v8",pakistan:"unicode/1f1f5-1f1f0.png?v8",palau:"unicode/1f1f5-1f1fc.png?v8",palestinian_territories:"unicode/1f1f5-1f1f8.png?v8",palm_tree:"unicode/1f334.png?v8",palms_up_together:"unicode/1f932.png?v8",panama:"unicode/1f1f5-1f1e6.png?v8",pancakes:"unicode/1f95e.png?v8",panda_face:"unicode/1f43c.png?v8",paperclip:"unicode/1f4ce.png?v8",paperclips:"unicode/1f587.png?v8",papua_new_guinea:"unicode/1f1f5-1f1ec.png?v8",parachute:"unicode/1fa82.png?v8",paraguay:"unicode/1f1f5-1f1fe.png?v8",parasol_on_ground:"unicode/26f1.png?v8",parking:"unicode/1f17f.png?v8",parrot:"unicode/1f99c.png?v8",part_alternation_mark:"unicode/303d.png?v8",partly_sunny:"unicode/26c5.png?v8",partying_face:"unicode/1f973.png?v8",passenger_ship:"unicode/1f6f3.png?v8",passport_control:"unicode/1f6c2.png?v8",pause_button:"unicode/23f8.png?v8",paw_prints:"unicode/1f43e.png?v8",peace_symbol:"unicode/262e.png?v8",peach:"unicode/1f351.png?v8",peacock:"unicode/1f99a.png?v8",peanuts:"unicode/1f95c.png?v8",pear:"unicode/1f350.png?v8",pen:"unicode/1f58a.png?v8",pencil:"unicode/1f4dd.png?v8",pencil2:"unicode/270f.png?v8",penguin:"unicode/1f427.png?v8",pensive:"unicode/1f614.png?v8",people_holding_hands:"unicode/1f9d1-1f91d-1f9d1.png?v8",people_hugging:"unicode/1fac2.png?v8",performing_arts:"unicode/1f3ad.png?v8",persevere:"unicode/1f623.png?v8",person_bald:"unicode/1f9d1-1f9b2.png?v8",person_curly_hair:"unicode/1f9d1-1f9b1.png?v8",person_feeding_baby:"unicode/1f9d1-1f37c.png?v8",person_fencing:"unicode/1f93a.png?v8",person_in_manual_wheelchair:"unicode/1f9d1-1f9bd.png?v8",person_in_motorized_wheelchair:"unicode/1f9d1-1f9bc.png?v8",person_in_tuxedo:"unicode/1f935.png?v8",person_red_hair:"unicode/1f9d1-1f9b0.png?v8",person_white_hair:"unicode/1f9d1-1f9b3.png?v8",person_with_probing_cane:"unicode/1f9d1-1f9af.png?v8",person_with_turban:"unicode/1f473.png?v8",person_with_veil:"unicode/1f470.png?v8",peru:"unicode/1f1f5-1f1ea.png?v8",petri_dish:"unicode/1f9eb.png?v8",philippines:"unicode/1f1f5-1f1ed.png?v8",phone:"unicode/260e.png?v8",pick:"unicode/26cf.png?v8",pickup_truck:"unicode/1f6fb.png?v8",pie:"unicode/1f967.png?v8",pig:"unicode/1f437.png?v8",pig2:"unicode/1f416.png?v8",pig_nose:"unicode/1f43d.png?v8",pill:"unicode/1f48a.png?v8",pilot:"unicode/1f9d1-2708.png?v8",pinata:"unicode/1fa85.png?v8",pinched_fingers:"unicode/1f90c.png?v8",pinching_hand:"unicode/1f90f.png?v8",pineapple:"unicode/1f34d.png?v8",ping_pong:"unicode/1f3d3.png?v8",pirate_flag:"unicode/1f3f4-2620.png?v8",pisces:"unicode/2653.png?v8",pitcairn_islands:"unicode/1f1f5-1f1f3.png?v8",pizza:"unicode/1f355.png?v8",placard:"unicode/1faa7.png?v8",place_of_worship:"unicode/1f6d0.png?v8",plate_with_cutlery:"unicode/1f37d.png?v8",play_or_pause_button:"unicode/23ef.png?v8",pleading_face:"unicode/1f97a.png?v8",plunger:"unicode/1faa0.png?v8",point_down:"unicode/1f447.png?v8",point_left:"unicode/1f448.png?v8",point_right:"unicode/1f449.png?v8",point_up:"unicode/261d.png?v8",point_up_2:"unicode/1f446.png?v8",poland:"unicode/1f1f5-1f1f1.png?v8",polar_bear:"unicode/1f43b-2744.png?v8",police_car:"unicode/1f693.png?v8",police_officer:"unicode/1f46e.png?v8",policeman:"unicode/1f46e-2642.png?v8",policewoman:"unicode/1f46e-2640.png?v8",poodle:"unicode/1f429.png?v8",poop:"unicode/1f4a9.png?v8",popcorn:"unicode/1f37f.png?v8",portugal:"unicode/1f1f5-1f1f9.png?v8",post_office:"unicode/1f3e3.png?v8",postal_horn:"unicode/1f4ef.png?v8",postbox:"unicode/1f4ee.png?v8",potable_water:"unicode/1f6b0.png?v8",potato:"unicode/1f954.png?v8",potted_plant:"unicode/1fab4.png?v8",pouch:"unicode/1f45d.png?v8",poultry_leg:"unicode/1f357.png?v8",pound:"unicode/1f4b7.png?v8",pout:"unicode/1f621.png?v8",pouting_cat:"unicode/1f63e.png?v8",pouting_face:"unicode/1f64e.png?v8",pouting_man:"unicode/1f64e-2642.png?v8",pouting_woman:"unicode/1f64e-2640.png?v8",pray:"unicode/1f64f.png?v8",prayer_beads:"unicode/1f4ff.png?v8",pregnant_woman:"unicode/1f930.png?v8",pretzel:"unicode/1f968.png?v8",previous_track_button:"unicode/23ee.png?v8",prince:"unicode/1f934.png?v8",princess:"unicode/1f478.png?v8",printer:"unicode/1f5a8.png?v8",probing_cane:"unicode/1f9af.png?v8",puerto_rico:"unicode/1f1f5-1f1f7.png?v8",punch:"unicode/1f44a.png?v8",purple_circle:"unicode/1f7e3.png?v8",purple_heart:"unicode/1f49c.png?v8",purple_square:"unicode/1f7ea.png?v8",purse:"unicode/1f45b.png?v8",pushpin:"unicode/1f4cc.png?v8",put_litter_in_its_place:"unicode/1f6ae.png?v8",qatar:"unicode/1f1f6-1f1e6.png?v8",question:"unicode/2753.png?v8",rabbit:"unicode/1f430.png?v8",rabbit2:"unicode/1f407.png?v8",raccoon:"unicode/1f99d.png?v8",racehorse:"unicode/1f40e.png?v8",racing_car:"unicode/1f3ce.png?v8",radio:"unicode/1f4fb.png?v8",radio_button:"unicode/1f518.png?v8",radioactive:"unicode/2622.png?v8",rage:"unicode/1f621.png?v8",rage1:"rage1.png?v8",rage2:"rage2.png?v8",rage3:"rage3.png?v8",rage4:"rage4.png?v8",railway_car:"unicode/1f683.png?v8",railway_track:"unicode/1f6e4.png?v8",rainbow:"unicode/1f308.png?v8",rainbow_flag:"unicode/1f3f3-1f308.png?v8",raised_back_of_hand:"unicode/1f91a.png?v8",raised_eyebrow:"unicode/1f928.png?v8",raised_hand:"unicode/270b.png?v8",raised_hand_with_fingers_splayed:"unicode/1f590.png?v8",raised_hands:"unicode/1f64c.png?v8",raising_hand:"unicode/1f64b.png?v8",raising_hand_man:"unicode/1f64b-2642.png?v8",raising_hand_woman:"unicode/1f64b-2640.png?v8",ram:"unicode/1f40f.png?v8",ramen:"unicode/1f35c.png?v8",rat:"unicode/1f400.png?v8",razor:"unicode/1fa92.png?v8",receipt:"unicode/1f9fe.png?v8",record_button:"unicode/23fa.png?v8",recycle:"unicode/267b.png?v8",red_car:"unicode/1f697.png?v8",red_circle:"unicode/1f534.png?v8",red_envelope:"unicode/1f9e7.png?v8",red_haired_man:"unicode/1f468-1f9b0.png?v8",red_haired_woman:"unicode/1f469-1f9b0.png?v8",red_square:"unicode/1f7e5.png?v8",registered:"unicode/00ae.png?v8",relaxed:"unicode/263a.png?v8",relieved:"unicode/1f60c.png?v8",reminder_ribbon:"unicode/1f397.png?v8",repeat:"unicode/1f501.png?v8",repeat_one:"unicode/1f502.png?v8",rescue_worker_helmet:"unicode/26d1.png?v8",restroom:"unicode/1f6bb.png?v8",reunion:"unicode/1f1f7-1f1ea.png?v8",revolving_hearts:"unicode/1f49e.png?v8",rewind:"unicode/23ea.png?v8",rhinoceros:"unicode/1f98f.png?v8",ribbon:"unicode/1f380.png?v8",rice:"unicode/1f35a.png?v8",rice_ball:"unicode/1f359.png?v8",rice_cracker:"unicode/1f358.png?v8",rice_scene:"unicode/1f391.png?v8",right_anger_bubble:"unicode/1f5ef.png?v8",ring:"unicode/1f48d.png?v8",ringed_planet:"unicode/1fa90.png?v8",robot:"unicode/1f916.png?v8",rock:"unicode/1faa8.png?v8",rocket:"unicode/1f680.png?v8",rofl:"unicode/1f923.png?v8",roll_eyes:"unicode/1f644.png?v8",roll_of_paper:"unicode/1f9fb.png?v8",roller_coaster:"unicode/1f3a2.png?v8",roller_skate:"unicode/1f6fc.png?v8",romania:"unicode/1f1f7-1f1f4.png?v8",rooster:"unicode/1f413.png?v8",rose:"unicode/1f339.png?v8",rosette:"unicode/1f3f5.png?v8",rotating_light:"unicode/1f6a8.png?v8",round_pushpin:"unicode/1f4cd.png?v8",rowboat:"unicode/1f6a3.png?v8",rowing_man:"unicode/1f6a3-2642.png?v8",rowing_woman:"unicode/1f6a3-2640.png?v8",ru:"unicode/1f1f7-1f1fa.png?v8",rugby_football:"unicode/1f3c9.png?v8",runner:"unicode/1f3c3.png?v8",running:"unicode/1f3c3.png?v8",running_man:"unicode/1f3c3-2642.png?v8",running_shirt_with_sash:"unicode/1f3bd.png?v8",running_woman:"unicode/1f3c3-2640.png?v8",rwanda:"unicode/1f1f7-1f1fc.png?v8",sa:"unicode/1f202.png?v8",safety_pin:"unicode/1f9f7.png?v8",safety_vest:"unicode/1f9ba.png?v8",sagittarius:"unicode/2650.png?v8",sailboat:"unicode/26f5.png?v8",sake:"unicode/1f376.png?v8",salt:"unicode/1f9c2.png?v8",samoa:"unicode/1f1fc-1f1f8.png?v8",san_marino:"unicode/1f1f8-1f1f2.png?v8",sandal:"unicode/1f461.png?v8",sandwich:"unicode/1f96a.png?v8",santa:"unicode/1f385.png?v8",sao_tome_principe:"unicode/1f1f8-1f1f9.png?v8",sari:"unicode/1f97b.png?v8",sassy_man:"unicode/1f481-2642.png?v8",sassy_woman:"unicode/1f481-2640.png?v8",satellite:"unicode/1f4e1.png?v8",satisfied:"unicode/1f606.png?v8",saudi_arabia:"unicode/1f1f8-1f1e6.png?v8",sauna_man:"unicode/1f9d6-2642.png?v8",sauna_person:"unicode/1f9d6.png?v8",sauna_woman:"unicode/1f9d6-2640.png?v8",sauropod:"unicode/1f995.png?v8",saxophone:"unicode/1f3b7.png?v8",scarf:"unicode/1f9e3.png?v8",school:"unicode/1f3eb.png?v8",school_satchel:"unicode/1f392.png?v8",scientist:"unicode/1f9d1-1f52c.png?v8",scissors:"unicode/2702.png?v8",scorpion:"unicode/1f982.png?v8",scorpius:"unicode/264f.png?v8",scotland:"unicode/1f3f4-e0067-e0062-e0073-e0063-e0074-e007f.png?v8",scream:"unicode/1f631.png?v8",scream_cat:"unicode/1f640.png?v8",screwdriver:"unicode/1fa9b.png?v8",scroll:"unicode/1f4dc.png?v8",seal:"unicode/1f9ad.png?v8",seat:"unicode/1f4ba.png?v8",secret:"unicode/3299.png?v8",see_no_evil:"unicode/1f648.png?v8",seedling:"unicode/1f331.png?v8",selfie:"unicode/1f933.png?v8",senegal:"unicode/1f1f8-1f1f3.png?v8",serbia:"unicode/1f1f7-1f1f8.png?v8",service_dog:"unicode/1f415-1f9ba.png?v8",seven:"unicode/0037-20e3.png?v8",sewing_needle:"unicode/1faa1.png?v8",seychelles:"unicode/1f1f8-1f1e8.png?v8",shallow_pan_of_food:"unicode/1f958.png?v8",shamrock:"unicode/2618.png?v8",shark:"unicode/1f988.png?v8",shaved_ice:"unicode/1f367.png?v8",sheep:"unicode/1f411.png?v8",shell:"unicode/1f41a.png?v8",shield:"unicode/1f6e1.png?v8",shinto_shrine:"unicode/26e9.png?v8",ship:"unicode/1f6a2.png?v8",shipit:"shipit.png?v8",shirt:"unicode/1f455.png?v8",shit:"unicode/1f4a9.png?v8",shoe:"unicode/1f45e.png?v8",shopping:"unicode/1f6cd.png?v8",shopping_cart:"unicode/1f6d2.png?v8",shorts:"unicode/1fa73.png?v8",shower:"unicode/1f6bf.png?v8",shrimp:"unicode/1f990.png?v8",shrug:"unicode/1f937.png?v8",shushing_face:"unicode/1f92b.png?v8",sierra_leone:"unicode/1f1f8-1f1f1.png?v8",signal_strength:"unicode/1f4f6.png?v8",singapore:"unicode/1f1f8-1f1ec.png?v8",singer:"unicode/1f9d1-1f3a4.png?v8",sint_maarten:"unicode/1f1f8-1f1fd.png?v8",six:"unicode/0036-20e3.png?v8",six_pointed_star:"unicode/1f52f.png?v8",skateboard:"unicode/1f6f9.png?v8",ski:"unicode/1f3bf.png?v8",skier:"unicode/26f7.png?v8",skull:"unicode/1f480.png?v8",skull_and_crossbones:"unicode/2620.png?v8",skunk:"unicode/1f9a8.png?v8",sled:"unicode/1f6f7.png?v8",sleeping:"unicode/1f634.png?v8",sleeping_bed:"unicode/1f6cc.png?v8",sleepy:"unicode/1f62a.png?v8",slightly_frowning_face:"unicode/1f641.png?v8",slightly_smiling_face:"unicode/1f642.png?v8",slot_machine:"unicode/1f3b0.png?v8",sloth:"unicode/1f9a5.png?v8",slovakia:"unicode/1f1f8-1f1f0.png?v8",slovenia:"unicode/1f1f8-1f1ee.png?v8",small_airplane:"unicode/1f6e9.png?v8",small_blue_diamond:"unicode/1f539.png?v8",small_orange_diamond:"unicode/1f538.png?v8",small_red_triangle:"unicode/1f53a.png?v8",small_red_triangle_down:"unicode/1f53b.png?v8",smile:"unicode/1f604.png?v8",smile_cat:"unicode/1f638.png?v8",smiley:"unicode/1f603.png?v8",smiley_cat:"unicode/1f63a.png?v8",smiling_face_with_tear:"unicode/1f972.png?v8",smiling_face_with_three_hearts:"unicode/1f970.png?v8",smiling_imp:"unicode/1f608.png?v8",smirk:"unicode/1f60f.png?v8",smirk_cat:"unicode/1f63c.png?v8",smoking:"unicode/1f6ac.png?v8",snail:"unicode/1f40c.png?v8",snake:"unicode/1f40d.png?v8",sneezing_face:"unicode/1f927.png?v8",snowboarder:"unicode/1f3c2.png?v8",snowflake:"unicode/2744.png?v8",snowman:"unicode/26c4.png?v8",snowman_with_snow:"unicode/2603.png?v8",soap:"unicode/1f9fc.png?v8",sob:"unicode/1f62d.png?v8",soccer:"unicode/26bd.png?v8",socks:"unicode/1f9e6.png?v8",softball:"unicode/1f94e.png?v8",solomon_islands:"unicode/1f1f8-1f1e7.png?v8",somalia:"unicode/1f1f8-1f1f4.png?v8",soon:"unicode/1f51c.png?v8",sos:"unicode/1f198.png?v8",sound:"unicode/1f509.png?v8",south_africa:"unicode/1f1ff-1f1e6.png?v8",south_georgia_south_sandwich_islands:"unicode/1f1ec-1f1f8.png?v8",south_sudan:"unicode/1f1f8-1f1f8.png?v8",space_invader:"unicode/1f47e.png?v8",spades:"unicode/2660.png?v8",spaghetti:"unicode/1f35d.png?v8",sparkle:"unicode/2747.png?v8",sparkler:"unicode/1f387.png?v8",sparkles:"unicode/2728.png?v8",sparkling_heart:"unicode/1f496.png?v8",speak_no_evil:"unicode/1f64a.png?v8",speaker:"unicode/1f508.png?v8",speaking_head:"unicode/1f5e3.png?v8",speech_balloon:"unicode/1f4ac.png?v8",speedboat:"unicode/1f6a4.png?v8",spider:"unicode/1f577.png?v8",spider_web:"unicode/1f578.png?v8",spiral_calendar:"unicode/1f5d3.png?v8",spiral_notepad:"unicode/1f5d2.png?v8",sponge:"unicode/1f9fd.png?v8",spoon:"unicode/1f944.png?v8",squid:"unicode/1f991.png?v8",sri_lanka:"unicode/1f1f1-1f1f0.png?v8",st_barthelemy:"unicode/1f1e7-1f1f1.png?v8",st_helena:"unicode/1f1f8-1f1ed.png?v8",st_kitts_nevis:"unicode/1f1f0-1f1f3.png?v8",st_lucia:"unicode/1f1f1-1f1e8.png?v8",st_martin:"unicode/1f1f2-1f1eb.png?v8",st_pierre_miquelon:"unicode/1f1f5-1f1f2.png?v8",st_vincent_grenadines:"unicode/1f1fb-1f1e8.png?v8",stadium:"unicode/1f3df.png?v8",standing_man:"unicode/1f9cd-2642.png?v8",standing_person:"unicode/1f9cd.png?v8",standing_woman:"unicode/1f9cd-2640.png?v8",star:"unicode/2b50.png?v8",star2:"unicode/1f31f.png?v8",star_and_crescent:"unicode/262a.png?v8",star_of_david:"unicode/2721.png?v8",star_struck:"unicode/1f929.png?v8",stars:"unicode/1f320.png?v8",station:"unicode/1f689.png?v8",statue_of_liberty:"unicode/1f5fd.png?v8",steam_locomotive:"unicode/1f682.png?v8",stethoscope:"unicode/1fa7a.png?v8",stew:"unicode/1f372.png?v8",stop_button:"unicode/23f9.png?v8",stop_sign:"unicode/1f6d1.png?v8",stopwatch:"unicode/23f1.png?v8",straight_ruler:"unicode/1f4cf.png?v8",strawberry:"unicode/1f353.png?v8",stuck_out_tongue:"unicode/1f61b.png?v8",stuck_out_tongue_closed_eyes:"unicode/1f61d.png?v8",stuck_out_tongue_winking_eye:"unicode/1f61c.png?v8",student:"unicode/1f9d1-1f393.png?v8",studio_microphone:"unicode/1f399.png?v8",stuffed_flatbread:"unicode/1f959.png?v8",sudan:"unicode/1f1f8-1f1e9.png?v8",sun_behind_large_cloud:"unicode/1f325.png?v8",sun_behind_rain_cloud:"unicode/1f326.png?v8",sun_behind_small_cloud:"unicode/1f324.png?v8",sun_with_face:"unicode/1f31e.png?v8",sunflower:"unicode/1f33b.png?v8",sunglasses:"unicode/1f60e.png?v8",sunny:"unicode/2600.png?v8",sunrise:"unicode/1f305.png?v8",sunrise_over_mountains:"unicode/1f304.png?v8",superhero:"unicode/1f9b8.png?v8",superhero_man:"unicode/1f9b8-2642.png?v8",superhero_woman:"unicode/1f9b8-2640.png?v8",supervillain:"unicode/1f9b9.png?v8",supervillain_man:"unicode/1f9b9-2642.png?v8",supervillain_woman:"unicode/1f9b9-2640.png?v8",surfer:"unicode/1f3c4.png?v8",surfing_man:"unicode/1f3c4-2642.png?v8",surfing_woman:"unicode/1f3c4-2640.png?v8",suriname:"unicode/1f1f8-1f1f7.png?v8",sushi:"unicode/1f363.png?v8",suspect:"suspect.png?v8",suspension_railway:"unicode/1f69f.png?v8",svalbard_jan_mayen:"unicode/1f1f8-1f1ef.png?v8",swan:"unicode/1f9a2.png?v8",swaziland:"unicode/1f1f8-1f1ff.png?v8",sweat:"unicode/1f613.png?v8",sweat_drops:"unicode/1f4a6.png?v8",sweat_smile:"unicode/1f605.png?v8",sweden:"unicode/1f1f8-1f1ea.png?v8",sweet_potato:"unicode/1f360.png?v8",swim_brief:"unicode/1fa72.png?v8",swimmer:"unicode/1f3ca.png?v8",swimming_man:"unicode/1f3ca-2642.png?v8",swimming_woman:"unicode/1f3ca-2640.png?v8",switzerland:"unicode/1f1e8-1f1ed.png?v8",symbols:"unicode/1f523.png?v8",synagogue:"unicode/1f54d.png?v8",syria:"unicode/1f1f8-1f1fe.png?v8",syringe:"unicode/1f489.png?v8","t-rex":"unicode/1f996.png?v8",taco:"unicode/1f32e.png?v8",tada:"unicode/1f389.png?v8",taiwan:"unicode/1f1f9-1f1fc.png?v8",tajikistan:"unicode/1f1f9-1f1ef.png?v8",takeout_box:"unicode/1f961.png?v8",tamale:"unicode/1fad4.png?v8",tanabata_tree:"unicode/1f38b.png?v8",tangerine:"unicode/1f34a.png?v8",tanzania:"unicode/1f1f9-1f1ff.png?v8",taurus:"unicode/2649.png?v8",taxi:"unicode/1f695.png?v8",tea:"unicode/1f375.png?v8",teacher:"unicode/1f9d1-1f3eb.png?v8",teapot:"unicode/1fad6.png?v8",technologist:"unicode/1f9d1-1f4bb.png?v8",teddy_bear:"unicode/1f9f8.png?v8",telephone:"unicode/260e.png?v8",telephone_receiver:"unicode/1f4de.png?v8",telescope:"unicode/1f52d.png?v8",tennis:"unicode/1f3be.png?v8",tent:"unicode/26fa.png?v8",test_tube:"unicode/1f9ea.png?v8",thailand:"unicode/1f1f9-1f1ed.png?v8",thermometer:"unicode/1f321.png?v8",thinking:"unicode/1f914.png?v8",thong_sandal:"unicode/1fa74.png?v8",thought_balloon:"unicode/1f4ad.png?v8",thread:"unicode/1f9f5.png?v8",three:"unicode/0033-20e3.png?v8",thumbsdown:"unicode/1f44e.png?v8",thumbsup:"unicode/1f44d.png?v8",ticket:"unicode/1f3ab.png?v8",tickets:"unicode/1f39f.png?v8",tiger:"unicode/1f42f.png?v8",tiger2:"unicode/1f405.png?v8",timer_clock:"unicode/23f2.png?v8",timor_leste:"unicode/1f1f9-1f1f1.png?v8",tipping_hand_man:"unicode/1f481-2642.png?v8",tipping_hand_person:"unicode/1f481.png?v8",tipping_hand_woman:"unicode/1f481-2640.png?v8",tired_face:"unicode/1f62b.png?v8",tm:"unicode/2122.png?v8",togo:"unicode/1f1f9-1f1ec.png?v8",toilet:"unicode/1f6bd.png?v8",tokelau:"unicode/1f1f9-1f1f0.png?v8",tokyo_tower:"unicode/1f5fc.png?v8",tomato:"unicode/1f345.png?v8",tonga:"unicode/1f1f9-1f1f4.png?v8",tongue:"unicode/1f445.png?v8",toolbox:"unicode/1f9f0.png?v8",tooth:"unicode/1f9b7.png?v8",toothbrush:"unicode/1faa5.png?v8",top:"unicode/1f51d.png?v8",tophat:"unicode/1f3a9.png?v8",tornado:"unicode/1f32a.png?v8",tr:"unicode/1f1f9-1f1f7.png?v8",trackball:"unicode/1f5b2.png?v8",tractor:"unicode/1f69c.png?v8",traffic_light:"unicode/1f6a5.png?v8",train:"unicode/1f68b.png?v8",train2:"unicode/1f686.png?v8",tram:"unicode/1f68a.png?v8",transgender_flag:"unicode/1f3f3-26a7.png?v8",transgender_symbol:"unicode/26a7.png?v8",triangular_flag_on_post:"unicode/1f6a9.png?v8",triangular_ruler:"unicode/1f4d0.png?v8",trident:"unicode/1f531.png?v8",trinidad_tobago:"unicode/1f1f9-1f1f9.png?v8",tristan_da_cunha:"unicode/1f1f9-1f1e6.png?v8",triumph:"unicode/1f624.png?v8",trolleybus:"unicode/1f68e.png?v8",trollface:"trollface.png?v8",trophy:"unicode/1f3c6.png?v8",tropical_drink:"unicode/1f379.png?v8",tropical_fish:"unicode/1f420.png?v8",truck:"unicode/1f69a.png?v8",trumpet:"unicode/1f3ba.png?v8",tshirt:"unicode/1f455.png?v8",tulip:"unicode/1f337.png?v8",tumbler_glass:"unicode/1f943.png?v8",tunisia:"unicode/1f1f9-1f1f3.png?v8",turkey:"unicode/1f983.png?v8",turkmenistan:"unicode/1f1f9-1f1f2.png?v8",turks_caicos_islands:"unicode/1f1f9-1f1e8.png?v8",turtle:"unicode/1f422.png?v8",tuvalu:"unicode/1f1f9-1f1fb.png?v8",tv:"unicode/1f4fa.png?v8",twisted_rightwards_arrows:"unicode/1f500.png?v8",two:"unicode/0032-20e3.png?v8",two_hearts:"unicode/1f495.png?v8",two_men_holding_hands:"unicode/1f46c.png?v8",two_women_holding_hands:"unicode/1f46d.png?v8",u5272:"unicode/1f239.png?v8",u5408:"unicode/1f234.png?v8",u55b6:"unicode/1f23a.png?v8",u6307:"unicode/1f22f.png?v8",u6708:"unicode/1f237.png?v8",u6709:"unicode/1f236.png?v8",u6e80:"unicode/1f235.png?v8",u7121:"unicode/1f21a.png?v8",u7533:"unicode/1f238.png?v8",u7981:"unicode/1f232.png?v8",u7a7a:"unicode/1f233.png?v8",uganda:"unicode/1f1fa-1f1ec.png?v8",uk:"unicode/1f1ec-1f1e7.png?v8",ukraine:"unicode/1f1fa-1f1e6.png?v8",umbrella:"unicode/2614.png?v8",unamused:"unicode/1f612.png?v8",underage:"unicode/1f51e.png?v8",unicorn:"unicode/1f984.png?v8",united_arab_emirates:"unicode/1f1e6-1f1ea.png?v8",united_nations:"unicode/1f1fa-1f1f3.png?v8",unlock:"unicode/1f513.png?v8",up:"unicode/1f199.png?v8",upside_down_face:"unicode/1f643.png?v8",uruguay:"unicode/1f1fa-1f1fe.png?v8",us:"unicode/1f1fa-1f1f8.png?v8",us_outlying_islands:"unicode/1f1fa-1f1f2.png?v8",us_virgin_islands:"unicode/1f1fb-1f1ee.png?v8",uzbekistan:"unicode/1f1fa-1f1ff.png?v8",v:"unicode/270c.png?v8",vampire:"unicode/1f9db.png?v8",vampire_man:"unicode/1f9db-2642.png?v8",vampire_woman:"unicode/1f9db-2640.png?v8",vanuatu:"unicode/1f1fb-1f1fa.png?v8",vatican_city:"unicode/1f1fb-1f1e6.png?v8",venezuela:"unicode/1f1fb-1f1ea.png?v8",vertical_traffic_light:"unicode/1f6a6.png?v8",vhs:"unicode/1f4fc.png?v8",vibration_mode:"unicode/1f4f3.png?v8",video_camera:"unicode/1f4f9.png?v8",video_game:"unicode/1f3ae.png?v8",vietnam:"unicode/1f1fb-1f1f3.png?v8",violin:"unicode/1f3bb.png?v8",virgo:"unicode/264d.png?v8",volcano:"unicode/1f30b.png?v8",volleyball:"unicode/1f3d0.png?v8",vomiting_face:"unicode/1f92e.png?v8",vs:"unicode/1f19a.png?v8",vulcan_salute:"unicode/1f596.png?v8",waffle:"unicode/1f9c7.png?v8",wales:"unicode/1f3f4-e0067-e0062-e0077-e006c-e0073-e007f.png?v8",walking:"unicode/1f6b6.png?v8",walking_man:"unicode/1f6b6-2642.png?v8",walking_woman:"unicode/1f6b6-2640.png?v8",wallis_futuna:"unicode/1f1fc-1f1eb.png?v8",waning_crescent_moon:"unicode/1f318.png?v8",waning_gibbous_moon:"unicode/1f316.png?v8",warning:"unicode/26a0.png?v8",wastebasket:"unicode/1f5d1.png?v8",watch:"unicode/231a.png?v8",water_buffalo:"unicode/1f403.png?v8",water_polo:"unicode/1f93d.png?v8",watermelon:"unicode/1f349.png?v8",wave:"unicode/1f44b.png?v8",wavy_dash:"unicode/3030.png?v8",waxing_crescent_moon:"unicode/1f312.png?v8",waxing_gibbous_moon:"unicode/1f314.png?v8",wc:"unicode/1f6be.png?v8",weary:"unicode/1f629.png?v8",wedding:"unicode/1f492.png?v8",weight_lifting:"unicode/1f3cb.png?v8",weight_lifting_man:"unicode/1f3cb-2642.png?v8",weight_lifting_woman:"unicode/1f3cb-2640.png?v8",western_sahara:"unicode/1f1ea-1f1ed.png?v8",whale:"unicode/1f433.png?v8",whale2:"unicode/1f40b.png?v8",wheel_of_dharma:"unicode/2638.png?v8",wheelchair:"unicode/267f.png?v8",white_check_mark:"unicode/2705.png?v8",white_circle:"unicode/26aa.png?v8",white_flag:"unicode/1f3f3.png?v8",white_flower:"unicode/1f4ae.png?v8",white_haired_man:"unicode/1f468-1f9b3.png?v8",white_haired_woman:"unicode/1f469-1f9b3.png?v8",white_heart:"unicode/1f90d.png?v8",white_large_square:"unicode/2b1c.png?v8",white_medium_small_square:"unicode/25fd.png?v8",white_medium_square:"unicode/25fb.png?v8",white_small_square:"unicode/25ab.png?v8",white_square_button:"unicode/1f533.png?v8",wilted_flower:"unicode/1f940.png?v8",wind_chime:"unicode/1f390.png?v8",wind_face:"unicode/1f32c.png?v8",window:"unicode/1fa9f.png?v8",wine_glass:"unicode/1f377.png?v8",wink:"unicode/1f609.png?v8",wolf:"unicode/1f43a.png?v8",woman:"unicode/1f469.png?v8",woman_artist:"unicode/1f469-1f3a8.png?v8",woman_astronaut:"unicode/1f469-1f680.png?v8",woman_beard:"unicode/1f9d4-2640.png?v8",woman_cartwheeling:"unicode/1f938-2640.png?v8",woman_cook:"unicode/1f469-1f373.png?v8",woman_dancing:"unicode/1f483.png?v8",woman_facepalming:"unicode/1f926-2640.png?v8",woman_factory_worker:"unicode/1f469-1f3ed.png?v8",woman_farmer:"unicode/1f469-1f33e.png?v8",woman_feeding_baby:"unicode/1f469-1f37c.png?v8",woman_firefighter:"unicode/1f469-1f692.png?v8",woman_health_worker:"unicode/1f469-2695.png?v8",woman_in_manual_wheelchair:"unicode/1f469-1f9bd.png?v8",woman_in_motorized_wheelchair:"unicode/1f469-1f9bc.png?v8",woman_in_tuxedo:"unicode/1f935-2640.png?v8",woman_judge:"unicode/1f469-2696.png?v8",woman_juggling:"unicode/1f939-2640.png?v8",woman_mechanic:"unicode/1f469-1f527.png?v8",woman_office_worker:"unicode/1f469-1f4bc.png?v8",woman_pilot:"unicode/1f469-2708.png?v8",woman_playing_handball:"unicode/1f93e-2640.png?v8",woman_playing_water_polo:"unicode/1f93d-2640.png?v8",woman_scientist:"unicode/1f469-1f52c.png?v8",woman_shrugging:"unicode/1f937-2640.png?v8",woman_singer:"unicode/1f469-1f3a4.png?v8",woman_student:"unicode/1f469-1f393.png?v8",woman_teacher:"unicode/1f469-1f3eb.png?v8",woman_technologist:"unicode/1f469-1f4bb.png?v8",woman_with_headscarf:"unicode/1f9d5.png?v8",woman_with_probing_cane:"unicode/1f469-1f9af.png?v8",woman_with_turban:"unicode/1f473-2640.png?v8",woman_with_veil:"unicode/1f470-2640.png?v8",womans_clothes:"unicode/1f45a.png?v8",womans_hat:"unicode/1f452.png?v8",women_wrestling:"unicode/1f93c-2640.png?v8",womens:"unicode/1f6ba.png?v8",wood:"unicode/1fab5.png?v8",woozy_face:"unicode/1f974.png?v8",world_map:"unicode/1f5fa.png?v8",worm:"unicode/1fab1.png?v8",worried:"unicode/1f61f.png?v8",wrench:"unicode/1f527.png?v8",wrestling:"unicode/1f93c.png?v8",writing_hand:"unicode/270d.png?v8",x:"unicode/274c.png?v8",yarn:"unicode/1f9f6.png?v8",yawning_face:"unicode/1f971.png?v8",yellow_circle:"unicode/1f7e1.png?v8",yellow_heart:"unicode/1f49b.png?v8",yellow_square:"unicode/1f7e8.png?v8",yemen:"unicode/1f1fe-1f1ea.png?v8",yen:"unicode/1f4b4.png?v8",yin_yang:"unicode/262f.png?v8",yo_yo:"unicode/1fa80.png?v8",yum:"unicode/1f60b.png?v8",zambia:"unicode/1f1ff-1f1f2.png?v8",zany_face:"unicode/1f92a.png?v8",zap:"unicode/26a1.png?v8",zebra:"unicode/1f993.png?v8",zero:"unicode/0030-20e3.png?v8",zimbabwe:"unicode/1f1ff-1f1fc.png?v8",zipper_mouth_face:"unicode/1f910.png?v8",zombie:"unicode/1f9df.png?v8",zombie_man:"unicode/1f9df-2642.png?v8",zombie_woman:"unicode/1f9df-2640.png?v8",zzz:"unicode/1f4a4.png?v8"}};function jn(e,t){return e.replace(/<(code|pre|script|template)[^>]*?>[\s\S]+?<\/(code|pre|script|template)>/g,function(e){return e.replace(/:/g,"__colon__")}).replace(//g,function(e){return e.replace(/:/g,"__colon__")}).replace(/([a-z]{2,}:)?\/\/[^\s'">)]+/gi,function(e){return e.replace(/:/g,"__colon__")}).replace(/:([a-z0-9_\-+]+?):/g,function(e,n){return i=e,o=n,e=t,n=Rn.data[o],i,i=n?e&&/unicode/.test(n)?''+n.replace("unicode/","").replace(/\.png.*/,"").split("-").map(function(e){return"&#x"+e+";"}).join("‍").concat("︎")+"":''+o+'':i;var i,o}).replace(/__colon__/g,":")}function On(e){var o={};return{str:e=(e=void 0===e?"":e)&&e.replace(/^('|")/,"").replace(/('|")$/,"").replace(/(?:^|\s):([\w-]+:?)=?([\w-%]+)?/g,function(e,n,i){return-1===n.indexOf(":")?(o[n]=i&&i.replace(/"/g,"")||!0,""):e}).trim(),config:o}}function Ln(e){return(e=void 0===e?"":e).replace(/(<\/?a.*?>)/gi,"")}var qn,Pn=be(function(e){var u,f,p,d,n,g=function(u){var i=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,n=0,e={},T={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof C?new C(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=r.reach);m+=_.value.length,_=_.next){var b=_.value;if(i.length>n.length)return;if(!(b instanceof C)){var k,w=1;if(l){if(!(k=R(h,m,n,s))||k.index>=n.length)break;var y=k.index,x=k.index+k[0].length,S=m;for(S+=_.value.length;S<=y;)_=_.next,S+=_.value.length;if(S-=_.value.length,m=S,_.value instanceof C)continue;for(var A=_;A!==i.tail&&(Sr.reach&&(r.reach=E);b=_.prev;z&&(b=j(i,b,z),m+=z.length),O(i,b,w);$=new C(c,g?T.tokenize($,g):$,v,$);_=j(i,b,$),F&&j(i,_,F),1r.reach&&(r.reach=E.reach))}}}}}(e,t,n,t.head,0),function(e){var n=[],i=e.head.next;for(;i!==e.tail;)n.push(i.value),i=i.next;return n}(t)},hooks:{all:{},add:function(e,n){var i=T.hooks.all;i[e]=i[e]||[],i[e].push(n)},run:function(e,n){var i=T.hooks.all[e];if(i&&i.length)for(var o,t=0;o=i[t++];)o(n)}},Token:C};function C(e,n,i,o){this.type=e,this.content=n,this.alias=i,this.length=0|(o||"").length}function R(e,n,i,o){e.lastIndex=n;i=e.exec(i);return i&&o&&i[1]&&(o=i[1].length,i.index+=o,i[0]=i[0].slice(o)),i}function a(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function j(e,n,i){var o=n.next,i={value:i,prev:n,next:o};return n.next=i,o.prev=i,e.length++,i}function O(e,n,i){for(var o=n.next,t=0;t"+t.content+""},!u.document)return u.addEventListener&&(T.disableWorkerMessageHandler||u.addEventListener("message",function(e){var n=JSON.parse(e.data),i=n.language,e=n.code,n=n.immediateClose;u.postMessage(T.highlight(e,T.languages[i],i)),n&&u.close()},!1)),T;var o=T.util.currentScript();function t(){T.manual||T.highlightAll()}return o&&(T.filename=o.src,o.hasAttribute("data-manual")&&(T.manual=!0)),T.manual||("loading"===(e=document.readyState)||"interactive"===e&&o&&o.defer?document.addEventListener("DOMContentLoaded",t):window.requestAnimationFrame?window.requestAnimationFrame(t):window.setTimeout(t,16)),T}("undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{});e.exports&&(e.exports=g),void 0!==me&&(me.Prism=g),g.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},g.languages.markup.tag.inside["attr-value"].inside.entity=g.languages.markup.entity,g.languages.markup.doctype.inside["internal-subset"].inside=g.languages.markup,g.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),Object.defineProperty(g.languages.markup.tag,"addInlined",{value:function(e,n){var i={};i["language-"+n]={pattern:/(^$)/i,lookbehind:!0,inside:g.languages[n]},i.cdata=/^$/i;i={"included-cdata":{pattern://i,inside:i}};i["language-"+n]={pattern:/[\s\S]+/,inside:g.languages[n]};n={};n[e]={pattern:RegExp(/(<__[^>]*>)(?:))*\]\]>|(?!)/.source.replace(/__/g,function(){return e}),"i"),lookbehind:!0,greedy:!0,inside:i},g.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(g.languages.markup.tag,"addAttribute",{value:function(e,n){g.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+e+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[n,"language-"+n],inside:g.languages[n]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),g.languages.html=g.languages.markup,g.languages.mathml=g.languages.markup,g.languages.svg=g.languages.markup,g.languages.xml=g.languages.extend("markup",{}),g.languages.ssml=g.languages.xml,g.languages.atom=g.languages.xml,g.languages.rss=g.languages.xml,function(e){var n=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-](?:[^;{\s]|\s+(?![\s{]))*(?:;|(?=\s*\{))/,inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+n.source+"|"+/(?:[^\\\r\n()"']|\\[\s\S])*/.source+")\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+n.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+n.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:n,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;e=e.languages.markup;e&&(e.tag.addInlined("style","css"),e.tag.addAttribute("style","css"))}(g),g.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},g.languages.javascript=g.languages.extend("clike",{"class-name":[g.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp(/(^|[^\w$])/.source+"(?:"+/NaN|Infinity/.source+"|"+/0[bB][01]+(?:_[01]+)*n?/.source+"|"+/0[oO][0-7]+(?:_[0-7]+)*n?/.source+"|"+/0[xX][\dA-Fa-f]+(?:_[\dA-Fa-f]+)*n?/.source+"|"+/\d+(?:_\d+)*n/.source+"|"+/(?:\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\.\d+(?:_\d+)*)(?:[Ee][+-]?\d+(?:_\d+)*)?/.source+")"+/(?![\w$])/.source),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),g.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,g.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)\/(?:\[(?:[^\]\\\r\n]|\\.)*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/,lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:g.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:g.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:g.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:g.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:g.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),g.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:g.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),g.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),g.languages.markup&&(g.languages.markup.tag.addInlined("script","javascript"),g.languages.markup.tag.addAttribute(/on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)/.source,"javascript")),g.languages.js=g.languages.javascript,void 0!==g&&"undefined"!=typeof document&&(Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector),u={js:"javascript",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell",sh:"bash",bat:"batch",h:"c",tex:"latex"},d="pre[data-src]:not(["+(f="data-src-status")+'="loaded"]):not(['+f+'="'+(p="loading")+'"])',g.hooks.add("before-highlightall",function(e){e.selector+=", "+d}),g.hooks.add("before-sanity-check",function(e){var t,n,i,o,a,r,c=e.element;c.matches(d)&&(e.code="",c.setAttribute(f,p),(t=c.appendChild(document.createElement("CODE"))).textContent="Loading…",i=c.getAttribute("data-src"),"none"===(e=e.language)&&(n=(/\.(\w+)$/.exec(i)||[,"none"])[1],e=u[n]||n),g.util.setLanguage(t,e),g.util.setLanguage(c,e),(n=g.plugins.autoloader)&&n.loadLanguages(e),i=i,o=function(e){c.setAttribute(f,"loaded");var n,i,o=function(e){if(i=/^\s*(\d+)\s*(?:(,)\s*(?:(\d+)\s*)?)?$/.exec(e||"")){var n=Number(i[1]),e=i[2],i=i[3];return e?i?[n,Number(i)]:[n,void 0]:[n,n]}}(c.getAttribute("data-range"));o&&(n=e.split(/\r\n?|\n/g),i=o[0],o=null==o[1]?n.length:o[1],i<0&&(i+=n.length),i=Math.max(0,Math.min(i-1,n.length)),o<0&&(o+=n.length),o=Math.max(0,Math.min(o,n.length)),e=n.slice(i,o).join("\n"),c.hasAttribute("data-start")||c.setAttribute("data-start",String(i+1))),t.textContent=e,g.highlightElement(t)},a=function(e){c.setAttribute(f,"failed"),t.textContent=e},(r=new XMLHttpRequest).open("GET",i,!0),r.onreadystatechange=function(){4==r.readyState&&(r.status<400&&r.responseText?o(r.responseText):400<=r.status?a("✖ Error "+r.status+" while fetching file: "+r.statusText):a("✖ Error: File does not exist or is empty"))},r.send(null))}),n=!(g.plugins.fileHighlight={highlight:function(e){for(var n,i=(e||document).querySelectorAll(d),o=0;n=i[o++];)g.highlightElement(n)}}),g.fileHighlight=function(){n||(console.warn("Prism.fileHighlight is deprecated. Use `Prism.plugins.fileHighlight.highlight` instead."),n=!0),g.plugins.fileHighlight.highlight.apply(this,arguments)})});function Mn(e,n){return"___"+e.toUpperCase()+n+"___"}qn=Prism,Object.defineProperties(qn.languages["markup-templating"]={},{buildPlaceholders:{value:function(o,t,e,a){var r;o.language===t&&(r=o.tokenStack=[],o.code=o.code.replace(e,function(e){if("function"==typeof a&&!a(e))return e;for(var n,i=r.length;-1!==o.code.indexOf(n=Mn(t,i));)++i;return r[i]=e,n}),o.grammar=qn.languages.markup)}},tokenizePlaceholders:{value:function(f,p){var d,g;f.language===p&&f.tokenStack&&(f.grammar=qn.languages[p],d=0,g=Object.keys(f.tokenStack),function e(n){for(var i=0;i=g.length);i++){var o,t,a,r,c,u=n[i];"string"==typeof u||u.content&&"string"==typeof u.content?(t=g[d],a=f.tokenStack[t],o="string"==typeof u?u:u.content,c=Mn(p,t),-1<(r=o.indexOf(c))&&(++d,t=o.substring(0,r),a=new qn.Token(p,qn.tokenize(a,f.grammar),"language-"+p,a),r=o.substring(r+c.length),c=[],t&&c.push.apply(c,e([t])),c.push(a),r&&c.push.apply(c,e([r])),"string"==typeof u?n.splice.apply(n,[i,1].concat(c)):u.content=c)):u.content&&e(u.content)}return n}(f.tokens))}}});function In(t,e){var a=this;this.config=t,this.router=e,this.cacheTree={},this.toc=[],this.cacheTOC={},this.linkTarget=t.externalLinkTarget||"_blank",this.linkRel="_blank"===this.linkTarget?t.externalLinkRel||"noopener":"",this.contentBase=e.getBasePath();var n=this._initRenderer();this.heading=n.heading;var r=o(e=t.markdown||{})?e(Sn,n):(Sn.setOptions(m(e,{renderer:m(n,e.renderer)})),Sn);this._marked=r,this.compile=function(i){var o=!0,e=c(function(e){o=!1;var n="";return i&&(n=f(i)?r(i):r.parser(i),n=t.noEmoji?n:jn(n,t.nativeEmoji),Cn.clear(),n)})(i),n=a.router.parse().file;return o?a.toc=a.cacheTOC[n]:a.cacheTOC[n]=[].concat(a.toc),e}}var Nn={},Hn={markdown:function(e){return{url:e}},mermaid:function(e){return{url:e}},iframe:function(e,n){return{html:'"}},video:function(e,n){return{html:'"}},audio:function(e,n){return{html:'"}},code:function(e,n){var i=e.match(/\.(\w+)$/);return{url:e,lang:i="md"===(i=n||i&&i[1])?"markdown":i}}};In.prototype.compileEmbed=function(e,n){var i,o,t=On(n),a=t.str,t=t.config;if(n=a,t.include)return T(e)||(e=q(this.contentBase,R(this.router.getCurrentPath()),e)),t.type&&(o=Hn[t.type])?(i=o.call(this,e,n)).type=t.type:(o="code",/\.(md|markdown)/.test(e)?o="markdown":/\.mmd/.test(e)?o="mermaid":/\.html?/.test(e)?o="iframe":/\.(mp4|ogg)/.test(e)?o="video":/\.mp3/.test(e)&&(o="audio"),(i=Hn[o].call(this,e,n)).type=o),i.fragment=t.fragment,i},In.prototype._matchNotCompileLink=function(e){for(var n=this.config.noCompileLinks||[],i=0;i/g.test(o)&&(o=o.replace("\x3c!-- {docsify-ignore} --\x3e",""),e.title=Ln(o),e.ignoreSubHeading=!0),/{docsify-ignore}/g.test(o)&&(o=o.replace("{docsify-ignore}",""),e.title=Ln(o),e.ignoreSubHeading=!0),//g.test(o)&&(o=o.replace("\x3c!-- {docsify-ignore-all} --\x3e",""),e.title=Ln(o),e.ignoreAllSubs=!0),/{docsify-ignore-all}/g.test(o)&&(o=o.replace("{docsify-ignore-all}",""),e.title=Ln(o),e.ignoreAllSubs=!0);i=Cn(t.id||o),t=a.toURL(a.getCurrentPath(),{id:i});return e.slug=t,g.toc.push(e),"'+o+""},t.code={renderer:e}.renderer.code=function(e,n){var i=Pn.languages[n=void 0===n?"markup":n]||Pn.languages.markup;return'
    '+Pn.highlight(e.replace(/@DOCSIFY_QM@/g,"`"),i,n)+"
    "},t.link=(i=(n={renderer:e,router:a,linkTarget:n,linkRel:i,compilerClass:g}).renderer,c=n.router,u=n.linkTarget,n.linkRel,f=n.compilerClass,i.link=function(e,n,i){var o=[],t=On(n=void 0===n?"":n),a=t.str,t=t.config;return u=t.target||u,r="_blank"===u?f.config.externalLinkRel||"noopener":"",n=a,T(e)||f._matchNotCompileLink(e)||t.ignore?(T(e)||"./"!==e.slice(0,2)||(e=document.URL.replace(/\/(?!.*\/).*/,"/").replace("#/./","")+e),o.push(0===e.indexOf("mailto:")?"":'target="'+u+'"'),o.push(0!==e.indexOf("mailto:")&&""!==r?' rel="'+r+'"':"")):(e===f.config.homepage&&(e="README"),e=c.toURL(e,null,c.getCurrentPath())),t.crossorgin&&"_self"===u&&"history"===f.config.routerMode&&-1===f.config.crossOriginLinks.indexOf(e)&&f.config.crossOriginLinks.push(e),t.disabled&&(o.push("disabled"),e="javascript:void(0)"),t.class&&o.push('class="'+t.class+'"'),t.id&&o.push('id="'+t.id+'"'),n&&o.push('title="'+n+'"'),'"+i+""}),t.paragraph={renderer:e}.renderer.paragraph=function(e){e=/^!>/.test(e)?$n("tip",e):/^\?>/.test(e)?$n("warn",e):"

    "+e+"

    ";return e},t.image=(o=(i={renderer:e,contentBase:o,router:a}).renderer,p=i.contentBase,d=i.router,o.image=function(e,n,i){var o=e,t=[],a=On(n),r=a.str,a=a.config;return n=r,a["no-zoom"]&&t.push("data-no-zoom"),n&&t.push('title="'+n+'"'),a.size&&(n=(r=a.size.split("x"))[0],(r=r[1])?t.push('width="'+n+'" height="'+r+'"'):t.push('width="'+n+'"')),a.class&&t.push('class="'+a.class+'"'),a.id&&t.push('id="'+a.id+'"'),T(e)||(o=q(p,R(d.getCurrentPath()),e)),0":''+i+'"}),t.list={renderer:e}.renderer.list=function(e,n,i){n=n?"ol":"ul";return"<"+n+" "+[/
  • /.test(e.split('class="task-list"')[0])?'class="task-list"':"",i&&1"+e+""},t.listitem={renderer:e}.renderer.listitem=function(e){return/^(]*>)/.test(e)?'
  • ":"
  • "+e+"
  • "},e.origin=t,e},In.prototype.sidebar=function(e,n){var i=this.toc,o=this.router.getCurrentPath(),t="";if(e)t=this.compile(e);else{for(var a=0;a{inner}");this.cacheTree[o]=n}return t},In.prototype.subSidebar=function(e){if(e){var n=this.router.getCurrentPath(),i=this.cacheTree,o=this.toc;o[0]&&o[0].ignoreAllSubs&&o.splice(0),o[0]&&1===o[0].level&&o.shift();for(var t=0;t\n'+e+"\n"}]).links={}:(n=[{type:"html",text:e}]).links={}),a({token:t,embedToken:n}),++u>=c&&a({})}}(n);n.embed.url?X(n.embed.url).then(o):o(n.embed.html)}}({compile:i,embedTokens:c,fetch:n},function(e){var n,i=e.embedToken,e=e.token;e?(n=e.index,p.forEach(function(e){n>e.start&&(n+=e.length)}),m(f,i.links),r=r.slice(0,n).concat(i,r.slice(n+1)),p.push({start:n,length:i.length-1})):(Bn[t]=r.concat(),r.links=Bn[t].links=f,o(r))})}function Yn(e,n,i){var o,t,a,r;return n="function"==typeof i?i(n):"string"==typeof i?(a=[],r=0,(o=i).replace(V,function(n,e,i){a.push(o.substring(r,i-1)),r=i+=n.length+1,a.push(t&&t[n]||function(e){return("00"+("string"==typeof Y[n]?e[Y[n]]():Y[n](e))).slice(-n.length)})}),r!==o.length&&a.push(o.substring(r)),function(e){for(var n="",i=0,o=e||new Date;i404 - Not found","Vue"in window)for(var a=0,r=k(".markdown-section > *").filter(n);ascript").filter(function(e){return!/template/.test(e.type)})[0])||(e=e.innerText.trim())&&new Function(e)()),"Vue"in window){var u,f,p=[],d=Object.keys(i.vueComponents||{});2===t&&d.length&&d.forEach(function(e){window.Vue.options.components[e]||window.Vue.component(e,i.vueComponents[e])}),!Un&&i.vueGlobalOptions&&"function"==typeof i.vueGlobalOptions.data&&(Un=i.vueGlobalOptions.data()),p.push.apply(p,Object.keys(i.vueMounts||{}).map(function(e){return[b(o,e),i.vueMounts[e]]}).filter(function(e){var n=e[0];e[1];return n})),(i.vueGlobalOptions||d.length)&&(u=/{{2}[^{}]*}{2}/,f=/<[^>/]+\s([@:]|v-)[\w-:.[\]]+[=>\s]/,p.push.apply(p,k(".markdown-section > *").filter(function(i){return!p.some(function(e){var n=e[0];e[1];return n===i})}).filter(function(e){return e.tagName.toLowerCase()in(i.vueComponents||{})||e.querySelector(d.join(",")||null)||u.test(e.outerHTML)||f.test(e.outerHTML)}).map(function(e){var n=m({},i.vueGlobalOptions||{});return Un&&(n.data=function(){return Un}),[e,n]})));for(var g=0,s=p;g([^<]*?)

    $'))&&("color"===n[2]?o.style.background=n[1]+(n[3]||""):(e=n[1],S(o,"add","has-mask"),T(n[1])||(e=q(this.router.getBasePath(),n[1])),o.style.backgroundImage="url("+e+")",o.style.backgroundSize="cover",o.style.backgroundPosition="center center"),i=i.replace(n[0],"")),this._renderTo(".cover-main",i),K()):S(o,"remove","show")},n.prototype._updateRender=function(){var e,n,i,o;e=this,n=l(".app-name-link"),i=e.config.nameLink,o=e.route.path,n&&(f(e.config.nameLink)?n.setAttribute("href",i):"object"==typeof i&&(e=Object.keys(i).filter(function(e){return-1':"")),e.coverpage&&(f+=(o=", 100%, 85%",'
    \x3c!--cover--\x3e
    ')),e.logo&&(o=/^data:image/.test(e.logo),n=/(?:http[s]?:)?\/\//.test(e.logo),i=/^\./.test(e.logo),o||n||i||(e.logo=q(this.router.getBasePath(),e.logo))),f+=(i=(n=e).name||"","
    "+('')+'
    \x3c!--main--\x3e
    '),this._renderTo(u,f,!0)):this.rendered=!0,e.mergeNavbar&&s?p=b(".sidebar"):(c.classList.add("app-nav"),e.repo||c.classList.add("no-badge")),e.loadNavbar&&y(p,c),e.themeColor&&(v.head.appendChild(w("div","").firstElementChild),a=e.themeColor,window.CSS&&window.CSS.supports&&window.CSS.supports("(--v:red)")||(e=k("style:not(.inserted),link"),[].forEach.call(e,function(e){"STYLE"===e.nodeName?Q(e,a):"LINK"===e.nodeName&&(e=e.getAttribute("href"),/\.css$/.test(e)&&X(e).then(function(e){e=w("style",e);_.appendChild(e),Q(e,a)}))}))),this._updateRender(),S(h,"ready")},n}(function(e){function n(){e.apply(this,arguments)}return e&&(n.__proto__=e),((n.prototype=Object.create(e&&e.prototype)).constructor=n).prototype.routes=function(){return this.config.routes||{}},n.prototype.matchVirtualRoute=function(t){var a=this.routes(),r=Object.keys(a),c=function(){return null};function u(){var e=r.shift();if(!e)return c(null);var n=A(o=(i="^",0===(o=e).indexOf(i)?o:"^"+o),"$")?o:o+"$",i=t.match(n);if(!i)return u();var o=a[e];if("string"==typeof o)return c(o);if("function"!=typeof o)return u();n=o,e=Xn(),o=e[0];return(0,e[1])(function(e){return"string"==typeof e?c(e):!1===e?c(null):u()}),n.length<=2?o(n(t,i)):n(t,i,o)}return{then:function(e){c=e,u()}}},n}(function(i){function e(){for(var e=[],n=arguments.length;n--;)e[n]=arguments[n];i.apply(this,e),this.route={}}return i&&(e.__proto__=i),((e.prototype=Object.create(i&&i.prototype)).constructor=e).prototype.updateRender=function(){this.router.normalize(),this.route=this.router.parse(),h.setAttribute("data-page",this.route.file)},e.prototype.initRouter=function(){var n=this,e=this.config,e=new("history"===(e.routerMode||"hash")&&t?D:H)(e);this.router=e,this.updateRender(),U=this.route,e.onchange(function(e){n.updateRender(),n._updateRender(),U.path!==n.route.path?(n.$fetch(d,n.$resetEvents.bind(n,e.source)),U=n.route):n.$resetEvents(e.source)})},e}(function(e){function n(){e.apply(this,arguments)}return e&&(n.__proto__=e),((n.prototype=Object.create(e&&e.prototype)).constructor=n).prototype.initLifecycle=function(){var i=this;this._hooks={},this._lifecycle={},["init","mounted","beforeEach","afterEach","doneEach","ready"].forEach(function(e){var n=i._hooks[e]=[];i._lifecycle[e]=function(e){return n.push(e)}})},n.prototype.callHook=function(e,t,a){void 0===a&&(a=d);var r=this._hooks[e],c=this.config.catchPluginErrors,u=function(n){var e=r[n];if(n>=r.length)a(t);else if("function"==typeof e){var i="Docsify plugin error";if(2===e.length)try{e(t,function(e){t=e,u(n+1)})}catch(e){if(!c)throw e;console.error(i,e),u(n+1)}else try{var o=e(t);t=void 0===o?t:o,u(n+1)}catch(e){if(!c)throw e;console.error(i,e),u(n+1)}}else u(n+1)};u(0)},n}(we))))))));function Kn(e,n,i){return Qn&&Qn.abort&&Qn.abort(),Qn=X(e,!0,i)}window.Docsify={util:Me,dom:n,get:X,slugify:Cn,version:"4.13.0"},window.DocsifyCompiler=In,window.marked=Sn,window.Prism=Pn,e(function(e){return new Jn})}(); diff --git a/doc/lib/js/search.min.js b/doc/lib/js/search.min.js new file mode 100644 index 0000000..be7a9f5 --- /dev/null +++ b/doc/lib/js/search.min.js @@ -0,0 +1 @@ +!function(){function u(e){return e.replace(//,"").replace(/{docsify-ignore}/,"").replace(//,"").replace(/{docsify-ignore-all}/,"").trim()}var f={},m={EXPIRE_KEY:"docsify.search.expires",INDEX_KEY:"docsify.search.index"};function g(e){var n={"&":"&","<":"<",">":">",'"':""","'":"'"};return String(e).replace(/[&<>"']/g,function(e){return n[e]})}function y(e){return e.text||"table"!==e.type||(e.cells.unshift(e.header),e.text=e.cells.map(function(e){return e.join(" | ")}).join(" |\n ")),e.text}function v(e){return e.text||"list"!==e.type||(e.text=e.raw),e.text}function b(o,e,s,c){void 0===e&&(e="");var d,e=window.marked.lexer(e),l=window.Docsify.slugify,p={},h="";return e.forEach(function(e,n){var t,a,i,r;"heading"===e.type&&e.depth<=c?(t=(a=(i=e.text,r={},{str:i=(i=void 0===i?"":i)&&i.replace(/^('|")/,"").replace(/('|")$/,"").replace(/(?:^|\s):([\w-]+:?)=?([\w-%]+)?/g,function(e,n,t){return-1===n.indexOf(":")?(r[n]=t&&t.replace(/"/g,"")||!0,""):e}).trim(),config:r})).str,i=a.config,a=u(e.text),d=i.id?s.toURL(o,{id:l(i.id)}):s.toURL(o,{id:l(g(a))}),t&&(h=u(t)),p[d]={slug:d,title:h,body:""}):(0===n&&(d=s.toURL(o),p[d]={slug:d,title:"/"!==o?o.slice(1):"Home Page",body:e.text||""}),d&&(p[d]?p[d].body?(e.text=y(e),e.text=v(e),p[d].body+="\n"+(e.text||"")):(e.text=y(e),e.text=v(e),p[d].body=p[d].body?p[d].body+e.text:e.text):p[d]={slug:d,title:"",body:""}))}),l.clear(),p}function p(e){return e&&e.normalize?e.normalize("NFD").replace(/[\u0300-\u036f]/g,""):e}function o(e){var n=[],t=[];Object.keys(f).forEach(function(n){t=t.concat(Object.keys(f[n]).map(function(e){return f[n][e]}))});var a=(e=e.trim()).split(/[\s\-,\\/]+/);1!==a.length&&(a=[].concat(e,a));for(var i=0;il.length&&(t=l.length),a=c&&"..."+c.substring(n,t).replace(a,function(e){return''+e+""})+"...",o+=a)}),0\n\n

    '+e.title+"

    \n

    "+e.content+"

    \n
    \n"}),t.classList.add("show"),a.classList.add("show"),t.innerHTML=r||'

    '+c+"

    ",s.hideOtherSidebarContent&&(i&&i.classList.add("hide"),n&&n.classList.add("hide"))}function l(e){s=e}function h(e,n){var t,a,i=n.router.parse().query.s;l(e),Docsify.dom.style("\n.sidebar {\n padding-top: 0;\n}\n\n.search {\n margin-bottom: 20px;\n padding: 6px;\n border-bottom: 1px solid #eee;\n}\n\n.search .input-wrap {\n display: flex;\n align-items: center;\n}\n\n.search .results-panel {\n display: none;\n}\n\n.search .results-panel.show {\n display: block;\n}\n\n.search input {\n outline: none;\n border: none;\n width: 100%;\n padding: 0.6em 7px;\n font-size: inherit;\n border: 1px solid transparent;\n}\n\n.search input:focus {\n box-shadow: 0 0 5px var(--theme-color, #42b983);\n border: 1px solid var(--theme-color, #42b983);\n}\n\n.search input::-webkit-search-decoration,\n.search input::-webkit-search-cancel-button,\n.search input {\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n\n.search input::-ms-clear {\n display: none;\n height: 0;\n width: 0;\n}\n\n.search .clear-button {\n cursor: pointer;\n width: 36px;\n text-align: right;\n display: none;\n}\n\n.search .clear-button.show {\n display: block;\n}\n\n.search .clear-button svg {\n transform: scale(.5);\n}\n\n.search h2 {\n font-size: 17px;\n margin: 10px 0;\n}\n\n.search a {\n text-decoration: none;\n color: inherit;\n}\n\n.search .matching-post {\n border-bottom: 1px solid #eee;\n}\n\n.search .matching-post:last-child {\n border-bottom: 0;\n}\n\n.search p {\n font-size: 14px;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n\n.search p.empty {\n text-align: center;\n}\n\n.app-name.hide, .sidebar-nav.hide {\n display: none;\n}"),function(e){void 0===e&&(e="");var n=Docsify.dom.create("div",'
    \n \n
    \n \n \n \n \n \n
    \n
    \n
    \n '),e=Docsify.dom.find("aside");Docsify.dom.toggleClass(n,"search"),Docsify.dom.before(e,n)}(i),n=Docsify.dom.find("div.search"),a=Docsify.dom.find(n,"input"),e=Docsify.dom.find(n,".input-wrap"),Docsify.dom.on(n,"click",function(e){return-1===["A","H2","P","EM"].indexOf(e.target.tagName)&&e.stopPropagation()}),Docsify.dom.on(a,"input",function(n){clearTimeout(t),t=setTimeout(function(e){return d(n.target.value.trim())},100)}),Docsify.dom.on(e,"click",function(e){"INPUT"!==e.target.tagName&&(a.value="",d())}),i&&setTimeout(function(e){return d(i)},500)}function x(e,n){var t,a,i,r,o;l(e),t=e.placeholder,a=n.route.path,(r=Docsify.dom.getNode('.search input[type="search"]'))&&("string"==typeof t?r.placeholder=t:(i=Object.keys(t).filter(function(e){return-1u.scrollOffset&&setTimeout(a,150))}),window.addEventListener("resize",a);var f={open:i,close:a,toggle:o,update:function(){var e=0 /dev/tcp/127.0.0.1/3306" ] + interval: 15s + timeout: 5s + retries: 10 + start_period: 10s + networks: + - media-net + environment: + MYSQL_DATABASE: wvp + MYSQL_ROOT_PASSWORD: root + MYSQL_USER: wvp_user + MYSQL_PASSWORD: wvp_password + TZ: Asia/Shanghai + # ports: + # - 3306:3306 + volumes: + - ./mysql/conf:/etc/mysql/conf.d + - ./logs/mysql:/logs + - ./volumes/mysql/data:/var/lib/mysql + - ../数据库/2.7.4/初始化-mysql-2.7.4.sql:/docker-entrypoint-initdb.d/init.sql # 初始化SQL脚本目录 + command: [ + # '--default-authentication-plugin=mysql_native_password', + '--innodb-buffer-pool-size=80M', + '--character-set-server=utf8mb4', + '--collation-server=utf8mb4_general_ci', + '--default-time-zone=+8:00', + '--lower-case-table-names=1' + ] + + polaris-media: + image: zlmediakit/zlmediakit:master # 替换为官方镜像 + restart: always + networks: + - media-net + ports: + #- "6080:80/tcp" # [播流]HTTP 安全考虑-非测试阶段需要注释掉,改为由nginx代理播流地址 + #- "4443:443/tcp" # [播流]HTTPS 安全考虑-非测试阶段需要注释掉,改为由nginx代理播流地址 + - "${MediaRtmp:-10935}:${MediaRtmp:-10935}/tcp" # [收流]RTMP + - "${MediaRtmp:-10935}:${MediaRtmp:-10935}/udp" # [收流]RTMP + #- "41935:41935/tcp" # [收流]RTMPS 无效 + - "${MediaRtsp:-5540}:${MediaRtsp:-5540}/tcp" # [收流]RTSP + - "${MediaRtsp:-5540}:${MediaRtsp:-5540}/udp" # [收流]RTSP + #- "45540:45540/tcp" # [收流]RTSPS 无效 + - "${MediaRtp:-10000}:${MediaRtp:-10000}/tcp" # [收流]RTP + - "${MediaRtp:-10000}:${MediaRtp:-10000}/udp" # [收流]RTP + volumes: + - ./volumes/video:/opt/media/bin/www/record/ + - ./logs/media:/opt/media/log/ + - ./media/config.ini:/conf/config.ini + command: [ + 'MediaServer', + '-c', '/conf/config.ini', + '-l', '0' + ] + + polaris-wvp: + # 显式指定构建上下文和Dockerfile路径 + build: + context: .. # 构建上下文的根路径 + dockerfile: ./docker/wvp/Dockerfile # 相对于上下文路径的Dockerfile位置 + restart: always + networks: + - media-net + ports: + - "18978:18978" + - "${SIP_Port:-8116}:${SIP_Port:-8116}/udp" + - "${SIP_Port:-8116}:${SIP_Port:-8116}/tcp" + depends_on: + - polaris-redis + - polaris-mysql + - polaris-media + volumes: + - ./wvp/wvp/:/opt/ylcx/wvp/ + - ./logs/wvp:/opt/wvp/logs/ + environment: + TZ: "Asia/Shanghai" + # 流链接的IP + Stream_IP: ${Stream_IP} + # SDP里的IP + SDP_IP: ${SDP_IP} + # [可选] zlm服务器访问WVP所使用的IP, 默认使用127.0.0.1,zlm和wvp没有部署在同一台服务器时必须配置 + ZLM_HOOK_HOST: polaris-wvp + ZLM_HOST: polaris-media + ZLM_SERCERT: su6TiedN2rVAmBbIDX0aa0QTiBJLBdcf + + MediaHttp: ${WebHttp:-8080} + #MediaHttps: ${WebHttps:-8081} + MediaRtmp: ${MediaRtmp:-10935} + MediaRtsp: ${MediaRtsp:-5540} + MediaRtp: ${MediaRtp:-10000} + + REDIS_HOST: polaris-redis + REDIS_PORT: 6379 + + DATABASE_HOST: polaris-mysql + DATABASE_PORT: 3306 + DATABASE_USER: wvp_user + DATABASE_PASSWORD: wvp_password + + SIP_ShowIP: ${SIP_ShowIP} + SIP_Port: ${SIP_Port:-8116} + SIP_Domain: ${SIP_Domain} + SIP_Id: ${SIP_Id} + SIP_Password: ${SIP_Password} + + RecordSip: ${RecordSip} + RecordPushLive: ${RecordPushLive} + + polaris-nginx: + # 显式指定构建上下文和Dockerfile路径 + build: + context: .. # 构建上下文的根路径 + dockerfile: ./docker/nginx/Dockerfile # 相对于上下文路径的Dockerfile位置 + ports: + - "${WebHttp:-8080}:8080" + depends_on: + - polaris-wvp + volumes: + - ./nginx/templates/:/etc/nginx/templates + - ./logs/nginx:/var/log/nginx + environment: + # 流链接的IP + Stream_IP: ${Stream_IP} + networks: + - media-net + +networks: + media-net: + driver: bridge \ No newline at end of file diff --git a/docker/docker-upgrade.sh b/docker/docker-upgrade.sh new file mode 100755 index 0000000..9c2d75f --- /dev/null +++ b/docker/docker-upgrade.sh @@ -0,0 +1,5 @@ +#/bin/bash +set -e + +docker compose down +docker compose up -d --remove-orphans \ No newline at end of file diff --git a/docker/media/Dockerfile b/docker/media/Dockerfile new file mode 100644 index 0000000..a7b6c16 --- /dev/null +++ b/docker/media/Dockerfile @@ -0,0 +1,91 @@ +FROM ubuntu:20.04 AS build + +#shell,rtmp,rtsp,rtsps,http,rtp +EXPOSE 10935/tcp +EXPOSE 5540/tcp +EXPOSE 6080/tcp +EXPOSE 10000/udp +EXPOSE 10000/tcp +EXPOSE 8000/udp +EXPOSE 8000/tcp +EXPOSE 9000/udp + +# ADD sources.list /etc/apt/sources.list + +RUN apt-get update && \ + DEBIAN_FRONTEND="noninteractive" \ + apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + git \ + curl \ + vim \ + wget \ + ca-certificates \ + tzdata \ + libssl-dev \ + gcc \ + g++ \ + gdb && \ + apt-get autoremove -y && \ + apt-get clean -y && \ + rm -rf /var/lib/apt/lists/* + +RUN mkdir -p /opt/media +WORKDIR /opt/media +RUN git clone --depth 1 https://gitee.com/xia-chu/ZLMediaKit && \ + cd ZLMediaKit && git submodule update --init + +# 3rdpart init +WORKDIR /opt/media/ZLMediaKit/3rdpart +RUN wget https://polaris-tian-generic.pkg.coding.net/qt/dependencies/openssl-1.1.1k.tar.gz?version=latest -O openssl-1.1.1k.tar.gz && \ + tar -xvzf openssl-1.1.1k.tar.gz && \ + cd openssl-1.1.1k && ./config shared --openssldir=/usr/local/openssl --prefix=/usr/local/openssl && \ + make && make install && \ + echo "/usr/local/lib64/" >> /etc/ld.so.conf && \ + echo "/usr/local/openssl/lib" >> /etc/ld.so.conf && \ + ldconfig && \ + ln -s /usr/local/openssl/bin/openssl /usr/local/bin/openssl + +WORKDIR /opt/media/ZLMediaKit/3rdpart +RUN wget https://github.com/cisco/libsrtp/archive/v2.3.0.tar.gz -O libsrtp-2.3.0.tar.gz && \ + tar xfv libsrtp-2.3.0.tar.gz && \ + mv libsrtp-2.3.0 libsrtp && \ + cd libsrtp && ./configure --enable-openssl --with-openssl-dir=/usr/local/openssl && make -j $(nproc) && make install + + +WORKDIR /opt/media/ZLMediaKit/build +RUN cmake .. -DENABLE_WEBRTC=true -DOPENSSL_ROOT_DIR=/usr/local/openssl -DOPENSSL_LIBRARIES=/usr/local/openssl/lib && \ + cmake --build . --target MediaServer +COPY config.ini /opt/media/ZLMediaKit/release/linux/Debug/ + +FROM ubuntu:20.04 + +RUN apt-get update && \ + DEBIAN_FRONTEND="noninteractive" \ + apt-get install -y --no-install-recommends \ + vim \ + wget \ + ca-certificates \ + tzdata \ + curl \ + libssl-dev \ + ffmpeg \ + gcc \ + g++ \ + gdb && \ + apt-get autoremove -y && \ + apt-get clean -y && \ + rm -rf /var/lib/apt/lists/* + +ENV TZ=Asia/Shanghai +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ + && echo $TZ > /etc/timezone && \ + mkdir -p /opt/media/bin/www + +WORKDIR /opt/media/bin/ +COPY --from=build /opt/media/ZLMediaKit/release/linux/Debug/MediaServer /opt/media/ZLMediaKit/default.pem /opt/media/bin/ +COPY --from=build /opt/media/ZLMediaKit/release/linux/Debug/config.ini /opt/media/conf/ +COPY --from=build /opt/media/ZLMediaKit/www/ /opt/media/bin/www/ +ENV PATH /opt/media/bin:$PATH +CMD ["./MediaServer","-s", "default.pem", "-c", "../conf/config.ini", "-l","0"] \ No newline at end of file diff --git a/docker/media/build.sh b/docker/media/build.sh new file mode 100755 index 0000000..edcc16c --- /dev/null +++ b/docker/media/build.sh @@ -0,0 +1,8 @@ +#/bin/bash +set -e + +version=2.7.3 + +docker build -t polaris-media:${version} . +docker tag polaris-media:${version} polaris-tian-docker.pkg.coding.net/qt/polaris/polaris-media:${version} +docker tag polaris-media:${version} polaris-tian-docker.pkg.coding.net/qt/polaris/polaris-media:latest \ No newline at end of file diff --git a/docker/media/config.ini b/docker/media/config.ini new file mode 100644 index 0000000..cc74281 --- /dev/null +++ b/docker/media/config.ini @@ -0,0 +1,199 @@ +; auto-generated by mINI class { + +[api] +apiDebug=1 +defaultSnap=./www/logo.png +downloadRoot=./www; +secret=su6TiedN2rVAmBbIDX0aa0QTiBJLBdcf +snapRoot=./www/snap/ + +[cluster] +origin_url= +retry_count=3 +timeout_sec=15 + +[ffmpeg] +bin=/usr/bin/ffmpeg +cmd=%s -re -i %s -c:a aac -strict -2 -ar 44100 -ab 48k -c:v libx264 -f flv %s +log=./ffmpeg/ffmpeg.log +restart_sec=0 +snap=%s -rtsp_transport tcp -i %s -y -f mjpeg -frames:v 1 %s + +[general] +broadcast_player_count_changed=0 +check_nvidia_dev=1 +enableVhost=0 +enable_ffmpeg_log=0 +flowThreshold=1024 +listen_ip=:: +maxStreamWaitMS=15000 +mediaServerId=polaris +mergeWriteMS=0 +resetWhenRePlay=1 +streamNoneReaderDelayMS=20000 +unready_frame_cache=100 +wait_add_track_ms=3000 +wait_audio_track_data_ms=1000 +wait_track_ready_ms=10000 + +[hls] +broadcastRecordTs=0 +deleteDelaySec=10 +fastRegister=0 +fileBufSize=65536 +segDelay=0 +segDur=2 +segKeep=0 +segNum=3 +segRetain=5 + +[hook] +alive_interval=10.0 +enable=1 +on_flow_report= +on_http_access= +on_play=http://polaris-wvp:18978/index/hook/on_play +on_publish=http://polaris-wvp:18978/index/hook/on_publish +on_record_mp4=http://polaris-wvp:18978/index/hook/on_record_mp4 +on_record_ts= +on_rtp_server_timeout=http://polaris-wvp:18978/index/hook/on_rtp_server_timeout +on_rtsp_auth= +on_rtsp_realm= +on_send_rtp_stopped=http://polaris-wvp:18978/index/hook/on_send_rtp_stopped +on_server_exited= +on_server_keepalive=http://polaris-wvp:18978/index/hook/on_server_keepalive +on_server_started=http://polaris-wvp:18978/index/hook/on_server_started +on_shell_login= +on_stream_changed=http://polaris-wvp:18978/index/hook/on_stream_changed +on_stream_none_reader=http://polaris-wvp:18978/index/hook/on_stream_none_reader +on_stream_not_found=http://polaris-wvp:18978/index/hook/on_stream_not_found +retry=1 +retry_delay=3.0 +stream_changed_schemas=rtsp/rtmp/fmp4/ts/hls/hls.fmp4 +timeoutSec=30 + +[http] +allow_cross_domains=1 +allow_ip_range= +charSet=utf-8 +dirMenu=1 +forbidCacheSuffix= +forwarded_ip_header= +keepAliveSecond=30 +maxReqSize=40960 +notFound=404 Not Found

    您访问的资源不存在!


    ZLMediaKit(git hash:8ccb4e9/%aI,branch:master,build time:2024-11-07T10:34:19)
    +port=80 +rootPath=./www +sendBufSize=65536 +sslport=443 +virtualPath= + +[multicast] +addrMax=239.255.255.255 +addrMin=239.0.0.0 +udpTTL=64 + +[protocol] +add_mute_audio=1 +auto_close=0 +continue_push_ms=3000 +enable_audio=1 +enable_fmp4=1 +enable_hls=0 +enable_hls_fmp4=0 +enable_mp4=0 +enable_rtmp=1 +enable_rtsp=1 +enable_ts=1 +fmp4_demand=0 +hls_demand=0 +hls_save_path=./www +modify_stamp=2 +mp4_as_player=0 +mp4_max_second=3600 +mp4_save_path=/opt/media/bin/www +paced_sender_ms=0 +rtmp_demand=0 +rtsp_demand=0 +ts_demand=0 + +[record] +appName=record +enableFmp4=1 +fastStart=0 +fileBufSize=65536 +fileRepeat=0 +sampleMS=500 + +[rtc] +bfilter=0 +datachannel_echo=0 +externIP= +maxRtpCacheMS=5000 +maxRtpCacheSize=2048 +max_bitrate=0 +min_bitrate=0 +nackIntervalRatio=1.0 +nackMaxCount=15 +nackMaxMS=3000 +nackMaxSize=2048 +nackRtpSize=8 +port=8000 +preferredCodecA=PCMA,PCMU,opus,mpeg4-generic +preferredCodecV=H264,H265,AV1,VP9,VP8 +rembBitRate=0 +start_bitrate=0 +tcpPort=8000 +timeoutSec=30 + +[rtmp] +directProxy=1 +enhanced=0 +handshakeSecond=15 +keepAliveSecond=15 +port=10001 +sslport=0 + +[rtp] +audioMtuSize=600 +h264_stap_a=1 +lowLatency=0 +rtpMaxSize=10 +videoMtuSize=1400 + +[rtp_proxy] +dumpDir= +gop_cache=1 +h264_pt=98 +h265_pt=99 +merge_frame=1 +opus_pt=100 +port=10003 +port_range=30000-30500 +ps_pt=96 +rtp_g711_dur_ms=100 +timeoutSec=15 +udp_recv_socket_buffer=4194304 + +[rtsp] +authBasic=0 +directProxy=1 +handshakeSecond=15 +keepAliveSecond=15 +lowLatency=0 +port=10002 +rtpTransportType=-1 +sslport=0 + +[shell] +maxReqSize=1024 +port=0 + +[srt] +latencyMul=4 +passPhrase= +pktBufSize=8192 +port=9000 +timeoutSec=5 + +; } --- diff --git a/docker/mysql/Dockerfile b/docker/mysql/Dockerfile new file mode 100644 index 0000000..ffdda52 --- /dev/null +++ b/docker/mysql/Dockerfile @@ -0,0 +1,3 @@ +FROM mysql:8.0.32 + +ADD ./db/*.sql /docker-entrypoint-initdb.d/ \ No newline at end of file diff --git a/docker/mysql/build.sh b/docker/mysql/build.sh new file mode 100755 index 0000000..133ab44 --- /dev/null +++ b/docker/mysql/build.sh @@ -0,0 +1,8 @@ +#/bin/bash +set -e + +version=2.7.3 + +docker build -t polaris-mysql:${version} . +docker tag polaris-mysql:${version} polaris-tian-docker.pkg.coding.net/qt/polaris/polaris-mysql:${version} +docker tag polaris-mysql:${version} polaris-tian-docker.pkg.coding.net/qt/polaris/polaris-mysql:latest \ No newline at end of file diff --git a/docker/mysql/db/privileges.sql b/docker/mysql/db/privileges.sql new file mode 100644 index 0000000..3aa6ce8 --- /dev/null +++ b/docker/mysql/db/privileges.sql @@ -0,0 +1,3 @@ +use mysql; +grant all privileges on wvp.* to 'ylcx'@'%'; +flush privileges; \ No newline at end of file diff --git a/docker/mysql/db/wvp.sql b/docker/mysql/db/wvp.sql new file mode 100644 index 0000000..d65a05d --- /dev/null +++ b/docker/mysql/db/wvp.sql @@ -0,0 +1,769 @@ +/*建库*/ +DROP DATABASE IF EXISTS `wvp`; + +CREATE DATABASE `wvp` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +USE `wvp`; + +/*建表*/ +drop table IF EXISTS wvp_device; +create table IF NOT EXISTS wvp_device +( + id serial primary key, + device_id character varying(50) not null, + name character varying(255), + manufacturer character varying(255), + model character varying(255), + firmware character varying(255), + transport character varying(50), + stream_mode character varying(50), + on_line bool default false, + register_time character varying(50), + keepalive_time character varying(50), + ip character varying(50), + create_time character varying(50), + update_time character varying(50), + port integer, + expires integer, + subscribe_cycle_for_catalog integer DEFAULT 0, + subscribe_cycle_for_mobile_position integer DEFAULT 0, + mobile_position_submission_interval integer DEFAULT 5, + subscribe_cycle_for_alarm integer DEFAULT 0, + host_address character varying(50), + charset character varying(50), + ssrc_check bool default false, + geo_coord_sys character varying(50), + media_server_id character varying(50) default 'auto', + custom_name character varying(255), + sdp_ip character varying(50), + local_ip character varying(50), + password character varying(255), + as_message_channel bool default false, + heart_beat_interval integer, + heart_beat_count integer, + position_capability integer, + broadcast_push_after_ack bool default false, + server_id character varying(50), + constraint uk_device_device unique (device_id) +); + +drop table IF EXISTS wvp_device_alarm; +create table IF NOT EXISTS wvp_device_alarm +( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + alarm_priority character varying(50), + alarm_method character varying(50), + alarm_time character varying(50), + alarm_description character varying(255), + longitude double precision, + latitude double precision, + alarm_type character varying(50), + create_time character varying(50) not null +); + +drop table IF EXISTS wvp_device_mobile_position; +create table IF NOT EXISTS wvp_device_mobile_position +( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + device_name character varying(255), + time character varying(50), + longitude double precision, + latitude double precision, + altitude double precision, + speed double precision, + direction double precision, + report_source character varying(50), + create_time character varying(50) +); + +drop table IF EXISTS wvp_device_channel; +create table IF NOT EXISTS wvp_device_channel +( + id serial primary key, + device_id character varying(50), + name character varying(255), + manufacturer character varying(50), + model character varying(50), + owner character varying(50), + civil_code character varying(50), + block character varying(50), + address character varying(50), + parental integer, + parent_id character varying(50), + safety_way integer, + register_way integer, + cert_num character varying(50), + certifiable integer, + err_code integer, + end_time character varying(50), + secrecy integer, + ip_address character varying(50), + port integer, + password character varying(255), + status character varying(50), + longitude double precision, + latitude double precision, + ptz_type integer, + position_type integer, + room_type integer, + use_type integer, + supply_light_type integer, + direction_type integer, + resolution character varying(255), + business_group_id character varying(255), + download_speed character varying(255), + svc_space_support_mod integer, + svc_time_support_mode integer, + create_time character varying(50) not null, + update_time character varying(50) not null, + sub_count integer, + stream_id character varying(255), + has_audio bool default false, + gps_time character varying(50), + stream_identification character varying(50), + channel_type int default 0 not null, + gb_device_id character varying(50), + gb_name character varying(255), + gb_manufacturer character varying(255), + gb_model character varying(255), + gb_owner character varying(255), + gb_civil_code character varying(255), + gb_block character varying(255), + gb_address character varying(255), + gb_parental integer, + gb_parent_id character varying(255), + gb_safety_way integer, + gb_register_way integer, + gb_cert_num character varying(50), + gb_certifiable integer, + gb_err_code integer, + gb_end_time character varying(50), + gb_secrecy integer, + gb_ip_address character varying(50), + gb_port integer, + gb_password character varying(50), + gb_status character varying(50), + gb_longitude double, + gb_latitude double, + gb_business_group_id character varying(50), + gb_ptz_type integer, + gb_position_type integer, + gb_room_type integer, + gb_use_type integer, + gb_supply_light_type integer, + gb_direction_type integer, + gb_resolution character varying(255), + gb_download_speed character varying(255), + gb_svc_space_support_mod integer, + gb_svc_time_support_mode integer, + record_plan_id integer, + data_type integer not null, + data_device_id integer not null, + gps_speed double precision, + gps_altitude double precision, + gps_direction double precision, + index (data_type), + index (data_device_id), + constraint uk_wvp_unique_channel unique (gb_device_id) +); + +drop table IF EXISTS wvp_media_server; +create table IF NOT EXISTS wvp_media_server +( + id character varying(255) primary key, + ip character varying(50), + hook_ip character varying(50), + sdp_ip character varying(50), + stream_ip character varying(50), + http_port integer, + http_ssl_port integer, + rtmp_port integer, + rtmp_ssl_port integer, + rtp_proxy_port integer, + rtsp_port integer, + rtsp_ssl_port integer, + flv_port integer, + flv_ssl_port integer, + ws_flv_port integer, + ws_flv_ssl_port integer, + auto_config bool default false, + secret character varying(50), + type character varying(50) default 'zlm', + rtp_enable bool default false, + rtp_port_range character varying(50), + send_rtp_port_range character varying(50), + record_assist_port integer, + default_server bool default false, + create_time character varying(50), + update_time character varying(50), + hook_alive_interval integer, + record_path character varying(255), + record_day integer default 7, + transcode_suffix character varying(255), + server_id character varying(50), + constraint uk_media_server_unique_ip_http_port unique (ip, http_port, server_id) +); + +drop table IF EXISTS wvp_platform; +create table IF NOT EXISTS wvp_platform +( + id serial primary key, + enable bool default false, + name character varying(255), + server_gb_id character varying(50), + server_gb_domain character varying(50), + server_ip character varying(50), + server_port integer, + device_gb_id character varying(50), + device_ip character varying(50), + device_port character varying(50), + username character varying(255), + password character varying(50), + expires character varying(50), + keep_timeout character varying(50), + transport character varying(50), + civil_code character varying(50), + manufacturer character varying(255), + model character varying(255), + address character varying(255), + character_set character varying(50), + ptz bool default false, + rtcp bool default false, + status bool default false, + catalog_group integer, + register_way integer, + secrecy integer, + create_time character varying(50), + update_time character varying(50), + as_message_channel bool default false, + catalog_with_platform integer default 1, + catalog_with_group integer default 1, + catalog_with_region integer default 1, + auto_push_channel bool default true, + send_stream_ip character varying(50), + server_id character varying(50), + constraint uk_platform_unique_server_gb_id unique (server_gb_id) +); + +drop table IF EXISTS wvp_platform_channel; +create table IF NOT EXISTS wvp_platform_channel +( + id serial primary key, + platform_id integer, + device_channel_id integer, + custom_device_id character varying(50), + custom_name character varying(255), + custom_manufacturer character varying(50), + custom_model character varying(50), + custom_owner character varying(50), + custom_civil_code character varying(50), + custom_block character varying(50), + custom_address character varying(50), + custom_parental integer, + custom_parent_id character varying(50), + custom_safety_way integer, + custom_register_way integer, + custom_cert_num character varying(50), + custom_certifiable integer, + custom_err_code integer, + custom_end_time character varying(50), + custom_secrecy integer, + custom_ip_address character varying(50), + custom_port integer, + custom_password character varying(255), + custom_status character varying(50), + custom_longitude double precision, + custom_latitude double precision, + custom_ptz_type integer, + custom_position_type integer, + custom_room_type integer, + custom_use_type integer, + custom_supply_light_type integer, + custom_direction_type integer, + custom_resolution character varying(255), + custom_business_group_id character varying(255), + custom_download_speed character varying(255), + custom_svc_space_support_mod integer, + custom_svc_time_support_mode integer, + constraint uk_platform_gb_channel_platform_id_catalog_id_device_channel_id unique (platform_id, device_channel_id), + constraint uk_platform_gb_channel_device_id unique (custom_device_id) +); + +drop table IF EXISTS wvp_platform_group; +create table IF NOT EXISTS wvp_platform_group +( + id serial primary key, + platform_id integer, + group_id integer, + constraint uk_wvp_platform_group_platform_id_group_id unique (platform_id, group_id) +); + +drop table IF EXISTS wvp_platform_region; +create table IF NOT EXISTS wvp_platform_region +( + id serial primary key, + platform_id integer, + region_id integer, + constraint uk_wvp_platform_region_platform_id_group_id unique (platform_id, region_id) +); + +drop table IF EXISTS wvp_stream_proxy; +create table IF NOT EXISTS wvp_stream_proxy +( + id serial primary key, + type character varying(50), + app character varying(255), + stream character varying(255), + src_url character varying(255), + timeout integer, + ffmpeg_cmd_key character varying(255), + rtsp_type character varying(50), + media_server_id character varying(50), + enable_audio bool default false, + enable_mp4 bool default false, + pulling bool default false, + enable bool default false, + enable_remove_none_reader bool default false, + create_time character varying(50), + name character varying(255), + update_time character varying(50), + stream_key character varying(255), + server_id character varying(50), + enable_disable_none_reader bool default false, + relates_media_server_id character varying(50), + constraint uk_stream_proxy_app_stream unique (app, stream) +); + +drop table IF EXISTS wvp_stream_push; +create table IF NOT EXISTS wvp_stream_push +( + id serial primary key, + app character varying(255), + stream character varying(255), + create_time character varying(50), + media_server_id character varying(50), + server_id character varying(50), + push_time character varying(50), + status bool default false, + update_time character varying(50), + pushing bool default false, + self bool default false, + start_offline_push bool default true, + constraint uk_stream_push_app_stream unique (app, stream) +); + +drop table IF EXISTS wvp_cloud_record; +create table IF NOT EXISTS wvp_cloud_record +( + id serial primary key, + app character varying(255), + stream character varying(255), + call_id character varying(255), + start_time bigint, + end_time bigint, + media_server_id character varying(50), + server_id character varying(50), + file_name character varying(255), + folder character varying(500), + file_path character varying(500), + collect bool default false, + file_size bigint, + time_len bigint +); + +drop table IF EXISTS wvp_user; +create table IF NOT EXISTS wvp_user +( + id serial primary key, + username character varying(255), + password character varying(255), + role_id integer, + create_time character varying(50), + update_time character varying(50), + push_key character varying(50), + constraint uk_user_username unique (username) +); + +drop table IF EXISTS wvp_user_role; +create table IF NOT EXISTS wvp_user_role +( + id serial primary key, + name character varying(50), + authority character varying(50), + create_time character varying(50), + update_time character varying(50) +); + + +drop table IF EXISTS wvp_user_api_key; +create table IF NOT EXISTS wvp_user_api_key +( + id serial primary key, + user_id bigint, + app character varying(255), + api_key text, + expired_at bigint, + remark character varying(255), + enable bool default true, + create_time character varying(50), + update_time character varying(50) +); + + +/*初始数据*/ +INSERT INTO wvp_user +VALUES (1, 'admin', '21232f297a57a5a743894a0e4a801fc3', 1, '2021-04-13 14:14:57', '2021-04-13 14:14:57', + '3e80d1762a324d5b0ff636e0bd16f1e3'); +INSERT INTO wvp_user_role +VALUES (1, 'admin', '0', '2021-04-13 14:14:57', '2021-04-13 14:14:57'); + +drop table IF EXISTS wvp_common_group; +create table IF NOT EXISTS wvp_common_group +( + id serial primary key, + device_id varchar(50) NOT NULL, + name varchar(255) NOT NULL, + parent_id int, + parent_device_id varchar(50) DEFAULT NULL, + business_group varchar(50) NOT NULL, + create_time varchar(50) NOT NULL, + update_time varchar(50) NOT NULL, + civil_code varchar(50) default null, + constraint uk_common_group_device_platform unique (device_id) +); + +drop table IF EXISTS wvp_common_region; +create table IF NOT EXISTS wvp_common_region +( + id serial primary key, + device_id varchar(50) NOT NULL, + name varchar(255) NOT NULL, + parent_id int, + parent_device_id varchar(50) DEFAULT NULL, + create_time varchar(50) NOT NULL, + update_time varchar(50) NOT NULL, + constraint uk_common_region_device_id unique (device_id) +); + +drop table IF EXISTS wvp_record_plan; +create table IF NOT EXISTS wvp_record_plan +( + id serial primary key, + snap bool default false, + name varchar(255) NOT NULL, + create_time character varying(50), + update_time character varying(50) +); + +drop table IF EXISTS wvp_record_plan_item; +create table IF NOT EXISTS wvp_record_plan_item +( + id serial primary key, + start int, + stop int, + week_day int, + plan_id int, + create_time character varying(50), + update_time character varying(50) +); + +/* +* 20240528 +*/ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20240528`() +BEGIN + IF NOT EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'transcode_suffix') + THEN + ALTER TABLE wvp_media_server ADD transcode_suffix character varying(255); + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'type') + THEN + alter table wvp_media_server + add type character varying(50) default 'zlm'; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'flv_port') + THEN + alter table wvp_media_server add flv_port integer; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'flv_ssl_port') + THEN + alter table wvp_media_server add flv_ssl_port integer; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'ws_flv_port') + THEN + alter table wvp_media_server add ws_flv_port integer; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'ws_flv_ssl_port') + THEN + alter table wvp_media_server add ws_flv_ssl_port integer; + END IF; +END; // +call wvp_20240528(); +DROP PROCEDURE wvp_20240528; +DELIMITER ; + +create table IF NOT EXISTS wvp_user_api_key ( + id serial primary key , + user_id bigint, + app character varying(255) , + api_key text, + expired_at bigint, + remark character varying(255), + enable bool default true, + create_time character varying(50), + update_time character varying(50) +); + +/* +* 20241222 +*/ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20241222`() +BEGIN + IF EXISTS (SELECT column_name FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and INDEX_NAME = 'uk_wvp_device_channel_unique_device_channel') + THEN + alter table wvp_device_channel drop index uk_wvp_device_channel_unique_device_channel; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and INDEX_NAME = 'uk_wvp_unique_stream_push_id') + THEN + alter table wvp_device_channel drop index uk_wvp_unique_stream_push_id; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and INDEX_NAME = 'uk_wvp_unique_stream_proxy_id') + THEN + alter table wvp_device_channel drop index uk_wvp_unique_stream_proxy_id; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'data_type') + THEN + alter table wvp_device_channel add data_type integer not null; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'data_device_id') + THEN + alter table wvp_device_channel add data_device_id integer not null; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'device_db_id') + THEN + update wvp_device_channel wdc INNER JOIN + (SELECT id, device_db_id from wvp_device_channel where device_db_id is not null ) ct on ct.id = wdc.id + set wdc.data_type = 1, wdc.data_device_id = ct.device_db_id where wdc.device_db_id is not null; + alter table wvp_device_channel drop device_db_id; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'stream_push_id') + THEN + update wvp_device_channel wdc INNER JOIN + (SELECT id, stream_push_id from wvp_device_channel where stream_push_id is not null ) ct on ct.id = wdc.id + set wdc.data_type = 2, wdc.data_device_id = ct.stream_push_id where wdc.stream_push_id is not null; + alter table wvp_device_channel drop stream_push_id; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'stream_proxy_id') + THEN + update wvp_device_channel wdc INNER JOIN + (SELECT id, stream_proxy_id from wvp_device_channel where stream_proxy_id is not null ) ct on ct.id = wdc.id + set wdc.data_type = 3, wdc.data_device_id = ct.stream_proxy_id where wdc.stream_proxy_id is not null; + alter table wvp_device_channel drop stream_proxy_id; + END IF; +END; // +call wvp_20241222(); +DROP PROCEDURE wvp_20241222; +DELIMITER ; +/* +* 20241231 +*/ +DELIMITER // +CREATE PROCEDURE `wvp_20241231`() +BEGIN + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_stream_proxy' and column_name = 'relates_media_server_id') + THEN + alter table wvp_stream_proxy add relates_media_server_id character varying(50); + END IF; +END; // +call wvp_20241231(); +DROP PROCEDURE wvp_20241231; +DELIMITER ; +/* +* 20250111 +*/ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20250111`() +BEGIN + IF EXISTS (SELECT column_name FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_cloud_record' and INDEX_NAME = 'uk_stream_push_app_stream_path') + THEN + alter table wvp_cloud_record drop index uk_stream_push_app_stream_path ; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_cloud_record' and column_name = 'folder') + THEN + alter table wvp_cloud_record modify folder varchar(500) null; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_cloud_record' and column_name = 'file_path') + THEN + alter table wvp_cloud_record modify file_path varchar(500) null; + END IF; +END; // +call wvp_20250111(); +DROP PROCEDURE wvp_20250111; +DELIMITER ; + +/* +* 20250211 +*/ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20250211`() +BEGIN + IF EXISTS (SELECT column_name FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device' and column_name = 'keepalive_interval_time') + THEN + alter table wvp_device change keepalive_interval_time heart_beat_interval integer after as_message_channel; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device' and column_name = 'heart_beat_count') + THEN + alter table wvp_device add heart_beat_count integer; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device' and column_name = 'position_capability') + THEN + alter table wvp_device add position_capability integer; + END IF; +END; // +call wvp_20250211(); +DROP PROCEDURE wvp_20250211; +DELIMITER ; + +/** + * 20250312 + */ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20250312`() +BEGIN + DECLARE serverId VARCHAR(32) DEFAULT '你的服务ID'; + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device' and column_name = 'server_id') + THEN + alter table wvp_device add server_id character varying(50); + update wvp_device set server_id = serverId; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'server_id') + THEN + alter table wvp_media_server add server_id character varying(50); + update wvp_media_server set server_id = serverId; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_stream_proxy' and column_name = 'server_id') + THEN + alter table wvp_stream_proxy add server_id character varying(50); + update wvp_stream_proxy set server_id = serverId; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_cloud_record' and column_name = 'server_id') + THEN + alter table wvp_cloud_record add server_id character varying(50); + update wvp_cloud_record set server_id = serverId; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_platform' and column_name = 'server_id') + THEN + alter table wvp_platform add server_id character varying(50); + END IF; +END; // +call wvp_20250312(); +DROP PROCEDURE wvp_20250312; +DELIMITER ; + +/* +* 20250319 +*/ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20250319`() +BEGIN + IF NOT EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'gps_speed') + THEN + alter table wvp_device_channel add gps_speed double precision; + END IF; + + IF NOT EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'gps_altitude') + THEN + alter table wvp_device_channel add gps_altitude double precision; + END IF; + + IF NOT EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'gps_direction') + THEN + alter table wvp_device_channel add gps_direction double precision; + END IF; +END; // +call wvp_20250319(); +DROP PROCEDURE wvp_20250319; +DELIMITER ; + +/* +* 20250402 +*/ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20250402`() +BEGIN + IF NOT EXISTS (SELECT column_name FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and INDEX_NAME = 'data_type') + THEN + create index data_type on wvp_device_channel (data_type); + END IF; + IF NOT EXISTS (SELECT column_name FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and INDEX_NAME = 'data_device_id') + THEN + create index data_device_id on wvp_device_channel (data_device_id); + END IF; + +END; // +call wvp_20250402(); +DROP PROCEDURE wvp_20250402; +DELIMITER ; + + + diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile new file mode 100644 index 0000000..5e57aab --- /dev/null +++ b/docker/nginx/Dockerfile @@ -0,0 +1,28 @@ +FROM ubuntu:24.04 AS builder + + +RUN apt-get update && \ + apt-get install -y nodejs npm && \ + rm -rf /var/lib/apt/lists/* + +COPY ./web /build +WORKDIR /build + +RUN npm --registry=https://registry.npmmirror.com install +RUN npm run build:prod + +WORKDIR /src/main/resources +RUN ls + +WORKDIR /src/main/resources/static +RUN ls + +FROM nginx:alpine + +ARG TZ=Asia/Shanghai + + +COPY --from=builder /src/main/resources/static /opt/dist + +CMD ["nginx","-g","daemon off;"] + diff --git a/docker/nginx/build.sh b/docker/nginx/build.sh new file mode 100755 index 0000000..717d546 --- /dev/null +++ b/docker/nginx/build.sh @@ -0,0 +1,11 @@ +#/bin/bash +set -e + +version=2.7.3 + +rm ./dist/static/js/config.js +cp ./config.js ./dist/static/js/ + +docker build -t polaris-nginx:${version} . +docker tag polaris-nginx:${version} polaris-tian-docker.pkg.coding.net/qt/polaris/polaris-nginx:${version} +docker tag polaris-nginx:${version} polaris-tian-docker.pkg.coding.net/qt/polaris/polaris-nginx:latest \ No newline at end of file diff --git a/docker/nginx/config.js b/docker/nginx/config.js new file mode 100644 index 0000000..f63449d --- /dev/null +++ b/docker/nginx/config.js @@ -0,0 +1,22 @@ + +window.baseUrl = "http://10.10.1.124:18978" + +// map组件全局参数, 注释此内容可以关闭地图功能 +window.mapParam = { + // 开启/关闭地图功能 + enable: true, + // 坐标系 GCJ-02 WGS-84, + coordinateSystem: "GCJ-02", + // 地图瓦片地址 + tilesUrl: "http://webrd0{1-4}.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8", + // 瓦片大小 + tileSize: 256, + // 默认层级 + zoom:10, + // 默认地图中心点 + center:[116.41020, 39.915119], + // 地图最大层级 + maxZoom:18, + // 地图最小层级 + minZoom: 3 +} diff --git a/docker/nginx/templates/nginx.conf.template b/docker/nginx/templates/nginx.conf.template new file mode 100644 index 0000000..cf0de13 --- /dev/null +++ b/docker/nginx/templates/nginx.conf.template @@ -0,0 +1,110 @@ +server { + listen 8080; + server_name localhost; + + location / { + root /opt/dist; + index index.html index.htm; + } + location /record_proxy/{ + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header REMOTE-HOST $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://polaris-wvp:18978/; + } + location /api/ { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header REMOTE-HOST $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://polaris-wvp:18978; + + + # 从环境变量获取原始主机地址(x.x.x.x) + set $original_host ${Stream_IP}; + + # 执行字符串替换 + # 将媒体资源文件替换为Nginx输出的相对地址 + sub_filter "http://$original_host/index/api/downloadFile" "mediaserver/api/downloadFile"; + sub_filter "http://$original_host:80/index/api/downloadFile" "mediaserver/api/downloadFile"; + sub_filter "https://$original_host/index/api/downloadFile" "mediaserver/api/downloadFile"; + sub_filter "https://$original_host:443/index/api/downloadFile" "mediaserver/api/downloadFile"; + sub_filter "http://$original_host/mp4_record" "mp4_record"; + sub_filter "http://$original_host:80/mp4_record" "mp4_record"; + sub_filter "https://$original_host/mp4_record" "mp4_record"; + sub_filter "https://$original_host:443/mp4_record" "mp4_record"; + + # 设置为off表示替换所有匹配项,而不仅仅是第一个 + sub_filter_once off; + + # 确保响应被正确处理 + sub_filter_types application/json; # 只对JSON响应进行处理 + } + + # 将mediaserver/record转发到目标地址 + location /mediaserver/api/downloadFile { + # 目标服务器地址 + proxy_pass http://polaris-media:80/index/api/downloadFile; + + # 以下是常用的反向代理设置 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 超时设置,根据需要调整 + proxy_connect_timeout 300s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + } + + # 仅允许代理/rtp/开头的路径 + location ^~ /rtp/ { + # 代理到ZLMediakit服务 + proxy_pass http://polaris-media:80; + + # 基础HTTP代理配置 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket支持配置 + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # 超时设置,根据实际需求调整 + proxy_connect_timeout 60s; + proxy_read_timeout 3600s; + proxy_send_timeout 60s; + } + + # 仅允许代理/rtp/开头的路径 + location ^~ /mp4_record/ { + # 代理到ZLMediakit服务 + proxy_pass http://polaris-media:80; + + # 基础HTTP代理配置 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket支持配置 + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # 超时设置,根据实际需求调整 + proxy_connect_timeout 60s; + proxy_read_timeout 3600s; + proxy_send_timeout 60s; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root html; + } +} diff --git a/docker/push.sh b/docker/push.sh new file mode 100755 index 0000000..468a957 --- /dev/null +++ b/docker/push.sh @@ -0,0 +1,15 @@ +#/bin/bash +set -e + +version=2.7.3 + +docker push polaris-tian-docker.pkg.coding.net/qt/polaris/ylcx-media:latest +docker push polaris-tian-docker.pkg.coding.net/qt/polaris/ylcx-mysql:latest +docker push polaris-tian-docker.pkg.coding.net/qt/polaris/ylcx-redis:latest +docker push polaris-tian-docker.pkg.coding.net/qt/polaris/ylcx-wvp:latest +docker push polaris-tian-docker.pkg.coding.net/qt/polaris/ylcx-nginx:latest +docker push polaris-tian-docker.pkg.coding.net/qt/polaris/ylcx-media:${version} +docker push polaris-tian-docker.pkg.coding.net/qt/polaris/ylcx-mysql:${version} +docker push polaris-tian-docker.pkg.coding.net/qt/polaris/ylcx-redis:${version} +docker push polaris-tian-docker.pkg.coding.net/qt/polaris/ylcx-wvp:${version} +docker push polaris-tian-docker.pkg.coding.net/qt/polaris/ylcx-nginx:${version} \ No newline at end of file diff --git a/docker/redis/Dockerfile b/docker/redis/Dockerfile new file mode 100644 index 0000000..d92f809 --- /dev/null +++ b/docker/redis/Dockerfile @@ -0,0 +1,5 @@ +FROM redis + +RUN mkdir -p /opt/polaris/redis +WORKDIR /opt/polaris/redis +COPY ./conf/redis.conf /opt/polaris/redis/redis.conf \ No newline at end of file diff --git a/docker/redis/build.sh b/docker/redis/build.sh new file mode 100755 index 0000000..a990f8f --- /dev/null +++ b/docker/redis/build.sh @@ -0,0 +1,8 @@ +#/bin/bash +set -e + +version=2.7.3 + +docker build -t polaris-redis:${version} . +docker tag polaris-redis:${version} polaris-tian-docker.pkg.coding.net/qt/polaris/polaris-redis:${version} +docker tag polaris-redis:${version} polaris-tian-docker.pkg.coding.net/qt/polaris/polaris-redis:latest \ No newline at end of file diff --git a/docker/redis/conf/redis.conf b/docker/redis/conf/redis.conf new file mode 100644 index 0000000..a151bd4 --- /dev/null +++ b/docker/redis/conf/redis.conf @@ -0,0 +1,2 @@ +#requirepass root +bind 0.0.0.0 \ No newline at end of file diff --git a/docker/wvp/Dockerfile b/docker/wvp/Dockerfile new file mode 100644 index 0000000..e66b4cd --- /dev/null +++ b/docker/wvp/Dockerfile @@ -0,0 +1,84 @@ +FROM ringcentral/jdk:11 AS builder + +EXPOSE 18978/tcp +EXPOSE 8116/tcp +EXPOSE 8116/udp +EXPOSE 8080/tcp + +#RUN apt-get update && \ + #DEBIAN_FRONTEND="noninteractive" \ + #apt-get install -y --no-install-recommends \ + #wget \ + #cmake \ + #maven \ + #git \ + #ca-certificates \ + #tzdata \ + #curl \ + #libpcre3 \ + #libpcre3-dev \ + #zlib1g-dev \ + #openssl \ + #libssl-dev \ + #gdb && \ + #apt-get autoremove -y && \ + #apt-get clean -y && \ + #rm -rf /var/lib/apt/lists/* + +## install jdk1.8 +#RUN mkdir -p /opt/download +#WORKDIR /opt/download +#RUN if [ "$Platfrom" = "arm64" ]; \ + #then \ + #wget https://polaris-tian-generic.pkg.coding.net/qt/autopliot/jdk-8u411-linux-aarch64.tar.gz?version=latest --no-check-certificate -O jdk-8.tar.gz && \ + #tar -zxvf /opt/download/jdk-8.tar.gz -C /usr/local/ --transform 's/jdk1.8.0_411/java/' && \ + #rm /opt/download/jdk-8.tar.gz; \ + #else \ + #wget https://polaris-tian-generic.pkg.coding.net/qt/autopliot/jdk-8u202-linux-x64.tar.gz?version=latest --no-check-certificate -O jdk-8.tar.gz && \ + #tar -zxvf /opt/download/jdk-8.tar.gz -C /usr/local/ --transform 's/jdk1.8.0_202/java/' && \ + #rm /opt/download/jdk-8.tar.gz; \ + #fi + +#ENV JAVA_HOME /usr/local/java/ +#ENV JRE_HOME ${JAVA_HOME}/jre +#ENV CLASSPATH .:${JAVA_HOME}/lib:${JRE_HOME}/lib +#ENV PATH ${JAVA_HOME}/bin:$PATH + +RUN java -version && javac -version + +#RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources && \ +RUN apt-get update && \ + apt-get install -y maven && \ + rm -rf /var/lib/apt/lists/* + + +COPY . /build +WORKDIR /build +RUN ls && mvn clean package -Dmaven.test.skip=true +WORKDIR /build/target +RUN mv wvp-pro-*.jar wvp.jar + + +FROM ringcentral/jdk:11 +RUN mkdir -p /opt/wvp +WORKDIR /opt/wvp +COPY --from=builder /build/target /opt/wvp +COPY ./docker/wvp/wvp /opt/wvp +ENTRYPOINT ["java", "-Xms512m", "-Xmx1024m", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/opt/ylcx/", "-jar", "wvp.jar", "--spring.config.location=/opt/ylcx/wvp/application.yml"] + + + +#RUN mkdir -p /opt/wvp +#WORKDIR /opt/wvp +#COPY ./wvp /opt/wvp +# +#WORKDIR /home +#RUN cd /home && \ + #git clone https://gitee.com/pan648540858/wvp-GB28181-pro.git +# +#RUN cd /home/wvp-GB28181-pro && \ + #mvn clean package -Dmaven.test.skip=true && \ + #cp /home/wvp-GB28181-pro/target/*.jar /opt/wvp/wvp.jar +# +#WORKDIR /opt/wvp +#ENTRYPOINT ["java", "-Xms512m", "-Xmx1024m", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/opt/ylcx/", "-jar", "wvp.jar", "--spring.config.location=/opt/ylcx/wvp/application.yml"] \ No newline at end of file diff --git a/docker/wvp/build.sh b/docker/wvp/build.sh new file mode 100755 index 0000000..f45df29 --- /dev/null +++ b/docker/wvp/build.sh @@ -0,0 +1,8 @@ +#/bin/bash +set -e + +version=2.7.3 + +docker build -t polaris-wvp:${version} . +docker tag polaris-wvp:${version} polaris-tian-docker.pkg.coding.net/qt/polaris/polaris-wvp:${version} +docker tag polaris-wvp:${version} polaris-tian-docker.pkg.coding.net/qt/polaris/polaris-wvp:latest \ No newline at end of file diff --git a/docker/wvp/wvp/application-base.yml b/docker/wvp/wvp/application-base.yml new file mode 100755 index 0000000..c15c6e5 --- /dev/null +++ b/docker/wvp/wvp/application-base.yml @@ -0,0 +1,105 @@ +spring: + # 设置接口超时时间 + mvc: + async: + request-timeout: 20000 + thymeleaf: + cache: false + # [可选]上传文件大小限制 + servlet: + multipart: + max-file-size: 10MB + max-request-size: 100MB + # REDIS数据库配置 + redis: + # [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1 + host: 127.0.0.1 + # [必须修改] 端口号 + port: 6379 + # [可选] 数据库 DB + database: 1 + # [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接 + password: + # [可选] 超时时间 + timeout: 30000 + # mysql数据源 + datasource: + dynamic: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://127.0.0.1:3306/wvp?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true + username: root + password: root +#[可选] 监听的HTTP端口, 网页和接口调用都是这个端口 +server: + port: 18978 + ssl: + # [可选] 是否开启HTTPS访问 + enabled: false +# 作为28181服务器的配置 +sip: + # [必须修改] 本机的IP + ip: 127.0.0.1 + # [可选] + port: 8116 + # [可选] + domain: 3402000000 + # [可选] + id: 34020000002000000001 + password: + alarm: true + +# 默认服务器配置 +media: + id: polaris + # [必须修改]内网IP + ip: 127.0.0.1 + http-port: 6080 + # [可选] 返回流地址时的ip,置空使用 media.ip + stream-ip: 127.0.0.1 + # [可选] wvp在国标信令中使用的ip,此ip为摄像机可以访问到的ip, 置空使用 media.ip + sdp-ip: 127.0.0.1 + # [可选] Hook IP, 默认使用sip.ip + hook-ip: 127.0.0.1 + # [可选] sslport + http-ssl-port: 4443 + rtp-proxy-port: 10000 + rtmp-port: 10935 + rtmp-ssl-port: 41935 + rtsp-port: 5540 + rtsp-ssl-port: 45540 + # [可选] + secret: su6TiedN2rVAmBbIDX0aa0QTiBJLBdcf + # 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试 + rtp: + # [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输 + enable: false + # [可选] + port-range: 30000,30500 + # [可选] + send-port-range: 50502,50506 + + record-path: /opt/media/record + record-day: 7 + record-assist-port: 0 +user-settings: + auto-apply-play: true + play-timeout: 30000 + wait-track: false + record-push-live: false + record-sip: false + stream-on-demand: true + interface-authentication: false + broadcast-for-platform: TCP-PASSIVE + push-stream-after-ack: true + send-to-platforms-when-id-lost: true + interface-authentication-excludes: + - /api/** + push-authority: false + allowed-origins: + - http://localhost:8080 + - http://127.0.0.1:8080 + - http://0.0.0.0:8080 +logging: + config: classpath:logback-spring.xml + diff --git a/docker/wvp/wvp/application-docker.yml b/docker/wvp/wvp/application-docker.yml new file mode 100644 index 0000000..04eeff8 --- /dev/null +++ b/docker/wvp/wvp/application-docker.yml @@ -0,0 +1,140 @@ +spring: + cache: + type: redis + thymeleaf: + cache: false + # 设置接口超时时间 + mvc: + async: + request-timeout: 20000 + # [可选]上传文件大小限制 + servlet: + multipart: + max-file-size: 10MB + max-request-size: 100MB + # REDIS数据库配置 + redis: + # [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1 + host: ${REDIS_HOST:127.0.0.1} + # [必须修改] 端口号 + port: ${REDIS_PORT:6379} + # [可选] 数据库 DB + database: 1 + # [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接 + password: + # [可选] 超时时间 + timeout: 10000 + ## [可选] 一个pool最多可分配多少个jedis实例 + #poolMaxTotal: 1000 + ## [可选] 一个pool最多有多少个状态为idle(空闲)的jedis实例 + #poolMaxIdle: 500 + ## [可选] 最大的等待时间(秒) + #poolMaxWait: 5 + # [必选] jdbc数据库配置 + datasource: + # mysql数据源 + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${DATABASE_HOST:127.0.0.1}:${DATABASE_PORT:3306}/wvp?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true&allowPublicKeyRetrieval=true + username: ${DATABASE_USER:root} + password: ${DATABASE_PASSWORD:root} + +#[可选] 监听的HTTP端口, 网页和接口调用都是这个端口 +server: + port: 18978 + ssl: + # [可选] 是否开启HTTPS访问 + # docker里运行,内部不需要HTTPS + enabled: false +# 作为28181服务器的配置 +sip: + # [必须修改] 本机的IP,对应你的网卡,监听什么ip就是使用什么网卡, + # 如果要监听多张网卡,可以使用逗号分隔多个IP, 例如: 192.168.1.4,10.0.0.4 + # 如果不明白,就使用0.0.0.0,大部分情况都是可以的 + # 请不要使用127.0.0.1,任何包括localhost在内的域名都是不可以的。 + ip: 0.0.0.0 + # [可选] 没有任何业务需求,仅仅是在前端展示的时候用 + show-ip: ${SIP_ShowIP} + # [可选] + port: ${SIP_Port:8116} + # 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007) + # 后两位为行业编码,定义参照附录D.3 + # 3701020049标识山东济南历下区 信息行业接入 + # [可选] + domain: ${SIP_Domain:3402000000} + # [可选] + id: ${SIP_Id:34020000002000000001} + # [可选] 默认设备认证密码,后续扩展使用设备单独密码, 移除密码将不进行校验 + password: ${SIP_Password} + # [可选] 国标级联注册失败,再次发起注册的时间间隔。 默认60秒 + register-time-interval: 60 + # [可选] 云台控制速度 + ptz-speed: 50 + # TODO [可选] 收到心跳后自动上线, 重启服务后会将所有设备置为离线,默认false,等待注册后上线。设置为true则收到心跳设置为上线。 + # keepalliveToOnline: false + # 是否存储alarm信息 + alarm: true + # 命令发送等待回复的超时时间, 单位:毫秒 + timeout: 1000 + +# 默认服务器配置 +media: + id: polaris + # [必须修改] ZLM 内网IP与端口 + ip: ${ZLM_HOST:127.0.0.1} + http-port: 80 + # [可选] 返回流地址时的ip,置空使用 media.ip + stream-ip: ${Stream_IP} + # [可选] wvp在国标信令中使用的ip,此ip为摄像机可以访问到的ip, 置空使用 media.ip + sdp-ip: ${SDP_IP} + # [可选] zlm服务器访问WVP所使用的IP, 默认使用127.0.0.1,zlm和wvp没有部署在同一台服务器时必须配置 + hook-ip: ${ZLM_HOOK_HOST} + # [可选] sslport + http-ssl-port: 0 + flv-port: ${MediaHttp:} + flv-ssl-port: ${MediaHttps:} + ws-flv-port: ${MediaHttp:} + ws-flv-ssl-port: ${MediaHttps:} + rtp-proxy-port: ${MediaRtp:} + rtmp-port: ${MediaRtmp:} + rtmp-ssl-port: 0 + rtsp-port: ${MediaRtsp:} + rtsp-ssl-port: 0 + # [可选] 是否自动配置ZLM, 如果希望手动配置ZLM, 可以设为false, 不建议新接触的用户修改 + auto-config: true + # [可选] + secret: ${ZLM_SERCERT} + # 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试 + rtp: + # [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输 + enable: false + # [可选] + port-range: 30000,30500 + # [可选] + send-port-range: 50502,50506 + + record-path: /opt/media/bin/www/record/ + record-day: 7 + record-assist-port: 0 +user-settings: + auto-apply-play: true + play-timeout: 30000 + wait-track: false + record-push-live: ${RecordPushLive:false} + record-sip: ${RecordSip:false} + stream-on-demand: true + interface-authentication: true + broadcast-for-platform: TCP-PASSIVE + push-stream-after-ack: true + send-to-platforms-when-id-lost: true + interface-authentication-excludes: + # - /api/** + push-authority: true + # allowed-origins: + # - http://localhost:8080 + # - http://127.0.0.1:8080 + # - http://0.0.0.0:8080 + # - ${NGINX_HOST} +logging: + config: classpath:logback-spring.xml + diff --git a/docker/wvp/wvp/application.yml b/docker/wvp/wvp/application.yml new file mode 100755 index 0000000..4496fb6 --- /dev/null +++ b/docker/wvp/wvp/application.yml @@ -0,0 +1,5 @@ +spring: + application: + name: wvp + profiles: + active: docker \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..3d76cf1 --- /dev/null +++ b/install.sh @@ -0,0 +1,57 @@ +#! /bin/sh + +WORD_DIR=$(cd $(dirname $0); pwd) +SERVICE_NAME="wvp" + +# 检查是否为 root 用户 +if [ "$(id -u)" -ne 0 ]; then + echo "提示: 建议使用 root 用户执行此脚本,否则可能权限不足!" + read -p "继续?(y/n) " -n 1 -r + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi + echo +fi + +# 当前目录直接搜索(不含子目录) +jar_files=(*.jar) + +if [ ${#jar_files[@]} -eq 0 ]; then + echo "当前目录无 JAR 文件!" + exit 1 +fi + +# 遍历结果 +for jar in "${jar_files[@]}"; do + echo "找到 JAR 文件: $jar" +done + +# 写文件 +# 生成 Systemd 服务文件内容 +SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" +cat << EOF | sudo tee "$SERVICE_FILE" > /dev/null +[Unit] +Description=${SERVICE_NAME} +After=syslog.target + +[Service] +User=$USER +WorkingDirectory=${WORD_DIR} +ExecStart=java -jar ${jar_files} +SuccessExitStatus=143 +Restart=on-failure +RestartSec=10s +Environment=SPRING_PROFILES_ACTIVE=prod + +[Install] +WantedBy=multi-user.target +EOF + +# 重载 Systemd 并启动服务 +sudo systemctl daemon-reload +sudo systemctl enable "$SERVICE_NAME" +sudo systemctl start "$SERVICE_NAME" + +# 验证服务状态 +echo "服务已安装!执行以下命令查看状态:" +echo "sudo systemctl status $SERVICE_NAME" diff --git a/libs/jdbc-aarch/kingbase8-8.6.0.jar b/libs/jdbc-aarch/kingbase8-8.6.0.jar new file mode 100644 index 0000000..489bf53 Binary files /dev/null and b/libs/jdbc-aarch/kingbase8-8.6.0.jar differ diff --git a/libs/jdbc-aarch/kingbase8-8.6.0.jre7.jar b/libs/jdbc-aarch/kingbase8-8.6.0.jre7.jar new file mode 100644 index 0000000..28102eb Binary files /dev/null and b/libs/jdbc-aarch/kingbase8-8.6.0.jre7.jar differ diff --git a/libs/jdbc-aarch/postgresql-42.2.9.jar b/libs/jdbc-aarch/postgresql-42.2.9.jar new file mode 100644 index 0000000..62d0b6d Binary files /dev/null and b/libs/jdbc-aarch/postgresql-42.2.9.jar differ diff --git a/libs/jdbc-aarch/postgresql-42.2.9.jre7.jar b/libs/jdbc-aarch/postgresql-42.2.9.jre7.jar new file mode 100644 index 0000000..f6077b6 Binary files /dev/null and b/libs/jdbc-aarch/postgresql-42.2.9.jre7.jar differ diff --git a/libs/jdbc-x86/bcprov-jdk15on-1.70.jar b/libs/jdbc-x86/bcprov-jdk15on-1.70.jar new file mode 100644 index 0000000..0e4198e Binary files /dev/null and b/libs/jdbc-x86/bcprov-jdk15on-1.70.jar differ diff --git a/libs/jdbc-x86/kingbase8-8.6.0.jar b/libs/jdbc-x86/kingbase8-8.6.0.jar new file mode 100644 index 0000000..ff4664e Binary files /dev/null and b/libs/jdbc-x86/kingbase8-8.6.0.jar differ diff --git a/libs/jdbc-x86/kingbase8-8.6.0.jre6.jar b/libs/jdbc-x86/kingbase8-8.6.0.jre6.jar new file mode 100644 index 0000000..fcdf628 Binary files /dev/null and b/libs/jdbc-x86/kingbase8-8.6.0.jre6.jar differ diff --git a/libs/jdbc-x86/kingbase8-8.6.0.jre7.jar b/libs/jdbc-x86/kingbase8-8.6.0.jre7.jar new file mode 100644 index 0000000..a039358 Binary files /dev/null and b/libs/jdbc-x86/kingbase8-8.6.0.jre7.jar differ diff --git a/libs/jdbc-x86/postgresql-42.2.9.jar b/libs/jdbc-x86/postgresql-42.2.9.jar new file mode 100644 index 0000000..487cfc1 Binary files /dev/null and b/libs/jdbc-x86/postgresql-42.2.9.jar differ diff --git a/libs/jdbc-x86/postgresql-42.2.9.jre6.jar b/libs/jdbc-x86/postgresql-42.2.9.jre6.jar new file mode 100644 index 0000000..11e9f99 Binary files /dev/null and b/libs/jdbc-x86/postgresql-42.2.9.jre6.jar differ diff --git a/libs/jdbc-x86/postgresql-42.2.9.jre7.jar b/libs/jdbc-x86/postgresql-42.2.9.jre7.jar new file mode 100644 index 0000000..522738d Binary files /dev/null and b/libs/jdbc-x86/postgresql-42.2.9.jre7.jar differ diff --git a/libs/smiley-http-proxy-servlet-2.0.jar b/libs/smiley-http-proxy-servlet-2.0.jar new file mode 100644 index 0000000..876b56c Binary files /dev/null and b/libs/smiley-http-proxy-servlet-2.0.jar differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e6405c8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,522 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.4 + + + com.genersoft + wvp-pro + 2.7.4 + web video platform + 国标28181视频平台 + ${project.packaging} + + + + nexus-aliyun + Nexus aliyun + https://maven.aliyun.com/repository/public + default + + false + + + true + + + + ECC + https://maven.ecc.no/releases + + + + + + nexus-aliyun + Nexus aliyun + https://maven.aliyun.com/repository/public + + false + + + true + + + + + + UTF-8 + MMddHHmm + + + ${project.build.directory}/generated-snippets + ${project.basedir}/docs/asciidoc + ${project.build.directory}/asciidoc + ${project.build.directory}/asciidoc/html + ${project.build.directory}/asciidoc/pdf + + 21 + 21 + + + + + jar + + true + + + jar + + + + war + + war + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-jetty + + + + + javax.servlet + javax.servlet-api + 3.1.0 + provided + + + + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + org.springframework.boot + spring-boot-starter-aop + + + + org.springframework.session + spring-session-core + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 3.0.4 + + + com.zaxxer + HikariCP + + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + + com.h2database + h2 + 2.3.232 + + + + + com.mysql + mysql-connector-j + 8.2.0 + + + + + org.postgresql + postgresql + 42.5.1 + + + + + + + com.kingbase + kingbase8 + 8.6.0 + system + ${basedir}/libs/jdbc-aarch/kingbase8-8.6.0.jar + + + com.kingbase + kingbase8 + 8.6.0 + system + ${basedir}/libs/jdbc-x86/kingbase8-8.6.0.jar + + + + + + + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + 2.1.1 + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.6 + + + org.springdoc + springdoc-openapi-starter-webmvc-api + 2.8.6 + + + org.springdoc + springdoc-openapi-security + 1.8.0 + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + 4.5.0 + + + + + + + + + + + + javax.sip + jain-sip-ri + 1.3.0-91 + + + + + org.slf4j + log4j-over-slf4j + 2.0.17 + + + + + org.dom4j + dom4j + 2.1.4 + + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.57 + + + com.alibaba.fastjson2 + fastjson2-extension + 2.0.57 + + + com.alibaba.fastjson2 + fastjson2-extension-spring5 + 2.0.57 + + + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + + + com.squareup.okhttp3 + logging-interceptor + 4.12.0 + + + + + io.github.rburgst + okhttp-digest + 3.1.1 + + + + + + + + + + + + org.bitbucket.b_c + jose4j + 0.9.6 + + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + + + com.alibaba + easyexcel + 4.0.3 + + + org.apache.commons + commons-compress + + + + + org.apache.commons + commons-compress + 1.27.1 + + + + + com.github.oshi + oshi-core + 6.6.5 + + + + + com.google.guava + guava + 33.4.8-jre + + + + + org.apache.ftpserver + ftpserver-core + 1.2.1 + + + + org.apache.ftpserver + ftplet-api + 1.2.1 + + + + + org.projectlombok + lombok + 1.18.38 + provided + + + + + + + + + + + io.github.sevdokimov.logviewer + log-viewer-spring-boot + 1.0.10 + + + + cn.hutool + hutool-all + 5.8.38 + + + + org.bouncycastle + bcpkix-jdk18on + 1.78.1 + + + + + no.ecc.vectortile + java-vector-tile + 1.4.1 + + + + + org.locationtech.jts + jts-core + 1.18.2 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + ${project.artifactId}-${project.version}-${maven.build.timestamp} + + + org.springframework.boot + spring-boot-maven-plugin + 3.4.10 + + true + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + 21 + 21 + + + org.projectlombok + lombok + 1.18.30 + + + + + + + pl.project13.maven + git-commit-id-plugin + 4.9.10 + + true + false + yyyyMMdd + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + true + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + **/配置详情.yml + **/application.yml + **/application-*.yml + **/local.jks + **/install.sh + + + + + maven-resources-plugin + + + copy-resources + package + + copy-resources + + + + + src/main/resources + + application.yml + application-*.yml + install.sh + + + + ${project.build.directory} + + + + + + + + src/main/resources + + + src/main/java + + **/*.xml + + + + + diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..f17040b --- /dev/null +++ b/run.sh @@ -0,0 +1,122 @@ +#!/bin/bash +# JDK路径 +export JAVA_HOME=/usr/local/java/jdk1.8.0_202 +export JRE_HOME=${JAVA_HOME}/jre +export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib +export PATH=${JAVA_HOME}/bin:$PATH + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +function log() { + message="[Polaris Log]: $1 " + case "$1" in + *"失败"* | *"错误"* | *"请使用 root 或 sudo 权限运行此脚本"*) + echo -e "${RED}${message}${NC}" 2>&1 | tee -a + ;; + *"成功"*) + echo -e "${GREEN}${message}${NC}" 2>&1 | tee -a + ;; + *"忽略"* | *"跳过"*) + echo -e "${YELLOW}${message}${NC}" 2>&1 | tee -a + ;; + *) + echo -e "${BLUE}${message}${NC}" 2>&1 | tee -a + ;; + esac +} + +echo +cat < /dev/null 2>&1 & + fi + log "流媒体服务开启成功" +} + +function stop() { + log "======================= 停止流媒体服务 =======================" + + PID="" + query() { + PID=$(ps -ef | grep java | grep $AppName | grep -v grep | awk '{print $2}') + } + query + if [ x"$PID" != x"" ]; then + log "进程PID: $PID" + kill -TERM $PID + log "$AppName (pid:$PID) exiting..." + while [ x"$PID" != x"" ]; do + sleep 1 + query + done + log "成功:$AppName exited." + else + log "忽略:进程不存在" + fi +} + +function status() { + log "======================= 运行状态 =======================" + log "" + + PID=`ps -ef |grep java|grep $AppName|grep -v grep|awk '{print $2}'` + if [ $PID != 0 ]; then + log "进程PID: $PID" + log "$AppName is running..." + else + log "$AppName is not running..." + fi + log "" + log "========================================================" +} + +function restart() { + stop + sleep 3 + start +} + +case $1 in +start) + start + ;; +stop) + stop + ;; +restart) + restart + ;; +status) + status + ;; +*) ;; + +esac + diff --git a/src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java b/src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java new file mode 100644 index 0000000..cabb8b2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java @@ -0,0 +1,68 @@ +package com.genersoft.iot.vmp; + +import com.genersoft.iot.vmp.jt1078.util.ClassUtil; +import com.genersoft.iot.vmp.utils.GitUtil; +import com.genersoft.iot.vmp.utils.SpringBeanFactory; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.ServletComponentScan; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.scheduling.annotation.EnableScheduling; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.SessionCookieConfig; +import jakarta.servlet.SessionTrackingMode; +import java.util.Collections; + +/** + * 启动类 + */ +@ServletComponentScan("com.genersoft.iot.vmp.conf") +@SpringBootApplication +@EnableScheduling +@EnableCaching +@Slf4j +public class VManageBootstrap extends SpringBootServletInitializer { + + private static String[] args; + private static ConfigurableApplicationContext context; + public static void main(String[] args) { + VManageBootstrap.args = args; + VManageBootstrap.context = SpringApplication.run(VManageBootstrap.class, args); + ClassUtil.context = VManageBootstrap.context; + GitUtil gitUtil = SpringBeanFactory.getBean("gitUtil"); + if (gitUtil == null) { + log.info("获取版本信息失败"); + }else { + log.info("构建版本: {}", gitUtil.getBuildVersion()); + log.info("构建时间: {}", gitUtil.getBuildDate()); + log.info("GIT信息: 分支: {}, ID: {}, 时间: {}", gitUtil.getBranch(), gitUtil.getCommitIdShort(), gitUtil.getCommitTime()); + } + } + // 项目重启 + public static void restart() { + context.close(); + VManageBootstrap.context = SpringApplication.run(VManageBootstrap.class, args); + } + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(VManageBootstrap.class); + } + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + super.onStartup(servletContext); + + servletContext.setSessionTrackingModes( + Collections.singleton(SessionTrackingMode.COOKIE) + ); + SessionCookieConfig sessionCookieConfig = servletContext.getSessionCookieConfig(); + sessionCookieConfig.setHttpOnly(true); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/CivilCodePo.java b/src/main/java/com/genersoft/iot/vmp/common/CivilCodePo.java new file mode 100644 index 0000000..3885197 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/CivilCodePo.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.common; + +import org.springframework.util.ObjectUtils; + +public class CivilCodePo { + + private String code; + + private String name; + + private String parentCode; + + public static CivilCodePo getInstance(String[] infoArray) { + CivilCodePo civilCodePo = new CivilCodePo(); + civilCodePo.setCode(infoArray[0]); + civilCodePo.setName(infoArray[1]); + if (!ObjectUtils.isEmpty(infoArray[2])) { + civilCodePo.setParentCode(infoArray[2]); + } + return civilCodePo; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getParentCode() { + return parentCode; + } + + public void setParentCode(String parentCode) { + this.parentCode = parentCode; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/CommonCallback.java b/src/main/java/com/genersoft/iot/vmp/common/CommonCallback.java new file mode 100644 index 0000000..819fe0d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/CommonCallback.java @@ -0,0 +1,5 @@ +package com.genersoft.iot.vmp.common; + +public interface CommonCallback{ + public void run(T t); +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/DeviceStatusCallback.java b/src/main/java/com/genersoft/iot/vmp/common/DeviceStatusCallback.java new file mode 100644 index 0000000..9ee9b9e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/DeviceStatusCallback.java @@ -0,0 +1,7 @@ +package com.genersoft.iot.vmp.common; + +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; + +public interface DeviceStatusCallback { + public void run(String deviceId, SipTransactionInfo transactionInfo); +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java b/src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java new file mode 100644 index 0000000..0c12796 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java @@ -0,0 +1,63 @@ +package com.genersoft.iot.vmp.common; + +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import lombok.Data; + +/** + * 记录每次发送invite消息的状态 + */ +@Data +public class InviteInfo { + + private String deviceId; + + private Integer channelId; + + private String stream; + + private SSRCInfo ssrcInfo; + + private String receiveIp; + + private Integer receivePort; + + private String streamMode; + + private InviteSessionType type; + + private InviteSessionStatus status; + + private StreamInfo streamInfo; + + private String mediaServerId; + + private Long expirationTime; + + private Long createTime; + + private Boolean record; + + private String startTime; + + private String endTime; + + + public static InviteInfo getInviteInfo(String deviceId, Integer channelId, String stream, SSRCInfo ssrcInfo, String mediaServerId, + String receiveIp, Integer receivePort, String streamMode, + InviteSessionType type, InviteSessionStatus status, Boolean record) { + InviteInfo inviteInfo = new InviteInfo(); + inviteInfo.setDeviceId(deviceId); + inviteInfo.setChannelId(channelId); + inviteInfo.setStream(stream); + inviteInfo.setSsrcInfo(ssrcInfo); + inviteInfo.setReceiveIp(receiveIp); + inviteInfo.setReceivePort(receivePort); + inviteInfo.setStreamMode(streamMode); + inviteInfo.setType(type); + inviteInfo.setStatus(status); + inviteInfo.setMediaServerId(mediaServerId); + inviteInfo.setRecord(record); + return inviteInfo; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/InviteSessionStatus.java b/src/main/java/com/genersoft/iot/vmp/common/InviteSessionStatus.java new file mode 100644 index 0000000..04cc7c9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/InviteSessionStatus.java @@ -0,0 +1,11 @@ +package com.genersoft.iot.vmp.common; + +/** + * 标识invite消息发出后的各个状态, + * 收到ok钱停止invite发送cancel, + * 收到200ok后发送BYE停止invite + */ +public enum InviteSessionStatus { + ready, + ok, +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/InviteSessionType.java b/src/main/java/com/genersoft/iot/vmp/common/InviteSessionType.java new file mode 100644 index 0000000..9241305 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/InviteSessionType.java @@ -0,0 +1,9 @@ +package com.genersoft.iot.vmp.common; + +public enum InviteSessionType { + PLAY, + PLAYBACK, + DOWNLOAD, + BROADCAST, + TALK +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/RemoteAddressInfo.java b/src/main/java/com/genersoft/iot/vmp/common/RemoteAddressInfo.java new file mode 100755 index 0000000..39478d8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/RemoteAddressInfo.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.common; + +public class RemoteAddressInfo { + private String ip; + private int port; + + public RemoteAddressInfo(String ip, int port) { + this.ip = ip; + this.port = port; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/ServerInfo.java b/src/main/java/com/genersoft/iot/vmp/common/ServerInfo.java new file mode 100644 index 0000000..fb1941a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/ServerInfo.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.common; + +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.Data; + +@Data +public class ServerInfo { + + private String ip; + private int port; + /** + * 现在使用的线程数 + */ + private String createTime; + + public static ServerInfo create(String ip, int port) { + ServerInfo serverInfo = new ServerInfo(); + serverInfo.setIp(ip); + serverInfo.setPort(port); + serverInfo.setCreateTime(DateUtil.getNow()); + return serverInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/StatisticsInfo.java b/src/main/java/com/genersoft/iot/vmp/common/StatisticsInfo.java new file mode 100644 index 0000000..8d7ab28 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/StatisticsInfo.java @@ -0,0 +1,97 @@ +package com.genersoft.iot.vmp.common; + +import lombok.Data; + +/** + * 统计信息 + */ +@Data +public class StatisticsInfo { + + private long id; + + /** + * ID + */ + private String deviceId; + + /** + * 分支 + */ + private String branch; + + /** + * git提交版本ID + */ + private String gitCommitId; + + /** + * git地址 + */ + private String gitUrl; + + /** + * 构建版本 + */ + private String version; + + /** + * 操作系统名称 + */ + private String osName; + + /** + * 是否是docker环境 + */ + private Boolean docker; + + /** + * 架构 + */ + private String arch; + + /** + * jdk版本 + */ + private String jdkVersion; + + /** + * redis版本 + */ + private String redisVersion; + + /** + * sql数据库版本 + */ + private String sqlVersion; + + /** + * sql数据库类型, mysql/postgresql/金仓等 + */ + private String sqlType; + + /** + * 创建时间 + */ + private String time; + + @Override + public String toString() { + return "StatisticsInfo{" + + "id=" + id + + ", deviceId='" + deviceId + '\'' + + ", branch='" + branch + '\'' + + ", gitCommitId='" + gitCommitId + '\'' + + ", gitUrl='" + gitUrl + '\'' + + ", version='" + version + '\'' + + ", osName='" + osName + '\'' + + ", docker=" + docker + + ", arch='" + arch + '\'' + + ", jdkVersion='" + jdkVersion + '\'' + + ", redisVersion='" + redisVersion + '\'' + + ", sqlVersion='" + sqlVersion + '\'' + + ", sqlType='" + sqlType + '\'' + + ", time='" + time + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java b/src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java new file mode 100644 index 0000000..78c93e5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java @@ -0,0 +1,372 @@ +package com.genersoft.iot.vmp.common; + +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.Objects; + +@Data +@Schema(description = "流信息") +public class StreamInfo implements Serializable, Cloneable{ + + @Schema(description = "应用名") + private String app; + @Schema(description = "流ID") + private String stream; + @Schema(description = "设备编号") + private String deviceId; + @Schema(description = "通道ID") + private Integer channelId; + + @Schema(description = "IP") + private String ip; + + @Schema(description = "HTTP-FLV流地址") + private StreamURL flv; + + @Schema(description = "HTTPS-FLV流地址") + private StreamURL https_flv; + @Schema(description = "Websocket-FLV流地址") + private StreamURL ws_flv; + @Schema(description = "Websockets-FLV流地址") + private StreamURL wss_flv; + @Schema(description = "HTTP-FMP4流地址") + private StreamURL fmp4; + @Schema(description = "HTTPS-FMP4流地址") + private StreamURL https_fmp4; + @Schema(description = "Websocket-FMP4流地址") + private StreamURL ws_fmp4; + @Schema(description = "Websockets-FMP4流地址") + private StreamURL wss_fmp4; + @Schema(description = "HLS流地址") + private StreamURL hls; + @Schema(description = "HTTPS-HLS流地址") + private StreamURL https_hls; + @Schema(description = "Websocket-HLS流地址") + private StreamURL ws_hls; + @Schema(description = "Websockets-HLS流地址") + private StreamURL wss_hls; + @Schema(description = "HTTP-TS流地址") + private StreamURL ts; + @Schema(description = "HTTPS-TS流地址") + private StreamURL https_ts; + @Schema(description = "Websocket-TS流地址") + private StreamURL ws_ts; + @Schema(description = "Websockets-TS流地址") + private StreamURL wss_ts; + @Schema(description = "RTMP流地址") + private StreamURL rtmp; + @Schema(description = "RTMPS流地址") + private StreamURL rtmps; + @Schema(description = "RTSP流地址") + private StreamURL rtsp; + @Schema(description = "RTSPS流地址") + private StreamURL rtsps; + @Schema(description = "RTC流地址") + private StreamURL rtc; + + @Schema(description = "RTCS流地址") + private StreamURL rtcs; + @Schema(description = "流媒体节点") + private MediaServer mediaServer; + @Schema(description = "流编码信息") + private MediaInfo mediaInfo; + @Schema(description = "开始时间") + private String startTime; + @Schema(description = "结束时间") + private String endTime; + @Schema(description = "时长(回放时使用)") + private Double duration; + @Schema(description = "进度(录像下载使用)") + private double progress; + @Schema(description = "文件下载地址(录像下载使用)") + private DownloadFileInfo downLoadFilePath; + @Schema(description = "点播请求的callId") + private String callId; + + @Schema(description = "是否暂停(录像回放使用)") + private boolean pause; + + @Schema(description = "产生源类型,包括 unknown = 0,rtmp_push=1,rtsp_push=2,rtp_push=3,pull=4,ffmpeg_pull=5,mp4_vod=6,device_chn=7") + private int originType; + + @Schema(description = "originType的文本描述") + private String originTypeStr; + + @Schema(description = "转码后的视频流") + private StreamInfo transcodeStream; + + @Schema(description = "使用的WVP ID") + private String serverId; + + @Schema(description = "流绑定的流媒体操作key") + private String key; + + public void setRtmp(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) { + String file = String.format("%s/%s%s", app, stream, callIdParam); + if (port != null && port > 0) { + this.rtmp = new StreamURL("rtmp", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.rtmps = new StreamURL("rtmps", host, sslPort, file); + } + } + + public void setRtsp(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) { + String file = String.format("%s/%s%s", app, stream, callIdParam); + if (port != null && port > 0) { + this.rtsp = new StreamURL("rtsp", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.rtsps = new StreamURL("rtsps", host, sslPort, file); + } + } + + public void setFlv(String host, Integer port, Integer sslPort, String file) { + if (port != null && port > 0) { + this.flv = new StreamURL("http", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.https_flv = new StreamURL("https", host, sslPort, file); + } + } + + public void setWsFlv(String host, Integer port, Integer sslPort, String file) { + if (port != null && port > 0) { + this.ws_flv = new StreamURL("ws", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.wss_flv = new StreamURL("wss", host, sslPort, file); + } + } + + public void setFmp4(String host, Integer port, Integer sslPort, String file) { + if (port != null && port > 0) { + this.fmp4 = new StreamURL("http", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.https_fmp4 = new StreamURL("https", host, sslPort, file); + } + } + + public void setWsMp4(String host, Integer port, Integer sslPort, String file) { + if (port != null && port > 0) { + this.ws_fmp4 = new StreamURL("ws", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.wss_fmp4 = new StreamURL("wss", host, sslPort, file); + } + } + + public void setHls(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) { + String file = String.format("%s/%s/hls.m3u8%s", app, stream, callIdParam); + if (port != null && port > 0) { + this.hls = new StreamURL("http", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.https_hls = new StreamURL("https", host, sslPort, file); + } + } + + public void setWsHls(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) { + String file = String.format("%s/%s/hls.m3u8%s", app, stream, callIdParam); + if (port != null && port > 0) { + this.ws_hls = new StreamURL("ws", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.wss_hls = new StreamURL("wss", host, sslPort, file); + } + } + + public void setTs(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) { + String file = String.format("%s/%s.live.ts%s", app, stream, callIdParam); + + if (port != null && port > 0) { + this.ts = new StreamURL("http", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.https_ts = new StreamURL("https", host, sslPort, file); + } + } + + public void setWsTs(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) { + String file = String.format("%s/%s.live.ts%s", app, stream, callIdParam); + + if (port != null && port > 0) { + this.ws_ts = new StreamURL("ws", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.wss_ts = new StreamURL("wss", host, sslPort, file); + } + } + + public void setRtc(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam, boolean isPlay) { + if (callIdParam != null) { + callIdParam = Objects.equals(callIdParam, "") ? callIdParam : callIdParam.replace("?", "&"); + } +// String file = String.format("%s/%s?type=%s%s", app, stream, isPlay?"play":"push", callIdParam); + String file = String.format("index/api/webrtc?app=%s&stream=%s&type=%s%s", app, stream, isPlay?"play":"push", callIdParam); + if (port > 0) { + this.rtc = new StreamURL("http", host, port, file); + } + if (sslPort > 0) { + this.rtcs = new StreamURL("https", host, sslPort, file); + } + } + + public void changeStreamIp(String localAddr) { + if (this.flv != null) { + this.flv.setHost(localAddr); + } + if (this.ws_flv != null ){ + this.ws_flv.setHost(localAddr); + } + if (this.hls != null ) { + this.hls.setHost(localAddr); + } + if (this.ws_hls != null ) { + this.ws_hls.setHost(localAddr); + } + if (this.ts != null ) { + this.ts.setHost(localAddr); + } + if (this.ws_ts != null ) { + this.ws_ts.setHost(localAddr); + } + if (this.fmp4 != null ) { + this.fmp4.setHost(localAddr); + } + if (this.ws_fmp4 != null ) { + this.ws_fmp4.setHost(localAddr); + } + if (this.rtc != null ) { + this.rtc.setHost(localAddr); + } + if (this.https_flv != null) { + this.https_flv.setHost(localAddr); + } + if (this.wss_flv != null) { + this.wss_flv.setHost(localAddr); + } + if (this.https_hls != null) { + this.https_hls.setHost(localAddr); + } + if (this.wss_hls != null) { + this.wss_hls.setHost(localAddr); + } + if (this.wss_ts != null) { + this.wss_ts.setHost(localAddr); + } + if (this.https_fmp4 != null) { + this.https_fmp4.setHost(localAddr); + } + if (this.wss_fmp4 != null) { + this.wss_fmp4.setHost(localAddr); + } + if (this.rtcs != null) { + this.rtcs.setHost(localAddr); + } + if (this.rtsp != null) { + this.rtsp.setHost(localAddr); + } + if (this.rtsps != null) { + this.rtsps.setHost(localAddr); + } + if (this.rtmp != null) { + this.rtmp.setHost(localAddr); + } + if (this.rtmps != null) { + this.rtmps.setHost(localAddr); + } + } + + + public static class TransactionInfo{ + public String callId; + public String localTag; + public String remoteTag; + public String branch; + } + + private TransactionInfo transactionInfo; + + + @Override + public StreamInfo clone() { + StreamInfo instance = null; + try{ + instance = (StreamInfo)super.clone(); + if (this.flv != null) { + instance.flv=this.flv.clone(); + } + if (this.ws_flv != null ){ + instance.ws_flv= this.ws_flv.clone(); + } + if (this.hls != null ) { + instance.hls= this.hls.clone(); + } + if (this.ws_hls != null ) { + instance.ws_hls= this.ws_hls.clone(); + } + if (this.ts != null ) { + instance.ts= this.ts.clone(); + } + if (this.ws_ts != null ) { + instance.ws_ts= this.ws_ts.clone(); + } + if (this.fmp4 != null ) { + instance.fmp4= this.fmp4.clone(); + } + if (this.ws_fmp4 != null ) { + instance.ws_fmp4= this.ws_fmp4.clone(); + } + if (this.rtc != null ) { + instance.rtc= this.rtc.clone(); + } + if (this.https_flv != null) { + instance.https_flv= this.https_flv.clone(); + } + if (this.wss_flv != null) { + instance.wss_flv= this.wss_flv.clone(); + } + if (this.https_hls != null) { + instance.https_hls= this.https_hls.clone(); + } + if (this.wss_hls != null) { + instance.wss_hls= this.wss_hls.clone(); + } + if (this.wss_ts != null) { + instance.wss_ts= this.wss_ts.clone(); + } + if (this.https_fmp4 != null) { + instance.https_fmp4= this.https_fmp4.clone(); + } + if (this.wss_fmp4 != null) { + instance.wss_fmp4= this.wss_fmp4.clone(); + } + if (this.rtcs != null) { + instance.rtcs= this.rtcs.clone(); + } + if (this.rtsp != null) { + instance.rtsp= this.rtsp.clone(); + } + if (this.rtsps != null) { + instance.rtsps= this.rtsps.clone(); + } + if (this.rtmp != null) { + instance.rtmp= this.rtmp.clone(); + } + if (this.rtmps != null) { + instance.rtmps= this.rtmps.clone(); + } + }catch(CloneNotSupportedException e) { + e.printStackTrace(); + } + return instance; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/StreamURL.java b/src/main/java/com/genersoft/iot/vmp/common/StreamURL.java new file mode 100644 index 0000000..40bd7e2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/StreamURL.java @@ -0,0 +1,84 @@ +package com.genersoft.iot.vmp.common; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.io.Serializable; + + +@Schema(description = "流地址信息") +public class StreamURL implements Serializable,Cloneable { + + @Schema(description = "协议") + private String protocol; + + @Schema(description = "主机地址") + private String host; + + @Schema(description = "端口") + private int port = -1; + + @Schema(description = "定位位置") + private String file; + + @Schema(description = "拼接后的地址") + private String url; + + public StreamURL() { + } + + public StreamURL(String protocol, String host, int port, String file) { + this.protocol = protocol; + this.host = host; + this.port = port; + this.file = file; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } + + public String getUrl() { + return this.toString(); + } + + @Override + public String toString() { + if (protocol != null && host != null && port != -1 ) { + return String.format("%s://%s:%s/%s", protocol, host, port, file); + }else { + return null; + } + } + @Override + public StreamURL clone() throws CloneNotSupportedException { + return (StreamURL) super.clone(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/SubscribeCallback.java b/src/main/java/com/genersoft/iot/vmp/common/SubscribeCallback.java new file mode 100644 index 0000000..5443b50 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/SubscribeCallback.java @@ -0,0 +1,7 @@ +package com.genersoft.iot.vmp.common; + +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; + +public interface SubscribeCallback{ + public void run(String deviceId, SipTransactionInfo transactionInfo); +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/SystemAllInfo.java b/src/main/java/com/genersoft/iot/vmp/common/SystemAllInfo.java new file mode 100644 index 0000000..48485da --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/SystemAllInfo.java @@ -0,0 +1,54 @@ +package com.genersoft.iot.vmp.common; + +import java.util.List; + +public class SystemAllInfo { + + private List cpu; + private List mem; + private List net; + + private long netTotal; + + private Object disk; + + public List getCpu() { + return cpu; + } + + public void setCpu(List cpu) { + this.cpu = cpu; + } + + public List getMem() { + return mem; + } + + public void setMem(List mem) { + this.mem = mem; + } + + public List getNet() { + return net; + } + + public void setNet(List net) { + this.net = net; + } + + public Object getDisk() { + return disk; + } + + public void setDisk(Object disk) { + this.disk = disk; + } + + public long getNetTotal() { + return netTotal; + } + + public void setNetTotal(long netTotal) { + this.netTotal = netTotal; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/VersionPo.java b/src/main/java/com/genersoft/iot/vmp/common/VersionPo.java new file mode 100644 index 0000000..f2c8454 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/VersionPo.java @@ -0,0 +1,149 @@ +package com.genersoft.iot.vmp.common; + +import com.alibaba.fastjson2.annotation.JSONField; + +public class VersionPo { + /** + * git的全版本号 + */ + @JSONField(name="GIT_Revision") + private String GIT_Revision; + /** + * maven版本 + */ + @JSONField(name = "Create_By") + private String Create_By; + /** + * git的分支 + */ + @JSONField(name = "GIT_BRANCH") + private String GIT_BRANCH; + /** + * git的url + */ + @JSONField(name = "GIT_URL") + private String GIT_URL; + /** + * 构建日期 + */ + @JSONField(name = "BUILD_DATE") + private String BUILD_DATE; + /** + * 构建日期 + */ + @JSONField(name = "GIT_DATE") + private String GIT_DATE; + /** + * 项目名称 配合pom使用 + */ + @JSONField(name = "artifactId") + private String artifactId; + /** + * git局部版本号 + */ + @JSONField(name = "GIT_Revision_SHORT") + private String GIT_Revision_SHORT; + /** + * 项目的版本如2.0.1.0 配合pom使用 + */ + @JSONField(name = "version") + private String version; + /** + * 子系统名称 + */ + @JSONField(name = "project") + private String project; + /** + * jdk版本 + */ + @JSONField(name="Build_Jdk") + private String Build_Jdk; + + public void setGIT_Revision(String GIT_Revision) { + this.GIT_Revision = GIT_Revision; + } + + public void setCreate_By(String create_By) { + Create_By = create_By; + } + + public void setGIT_BRANCH(String GIT_BRANCH) { + this.GIT_BRANCH = GIT_BRANCH; + } + + public void setGIT_URL(String GIT_URL) { + this.GIT_URL = GIT_URL; + } + + public void setBUILD_DATE(String BUILD_DATE) { + this.BUILD_DATE = BUILD_DATE; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + public void setGIT_Revision_SHORT(String GIT_Revision_SHORT) { + this.GIT_Revision_SHORT = GIT_Revision_SHORT; + } + + public void setVersion(String version) { + this.version = version; + } + + public void setProject(String project) { + this.project = project; + } + + public void setBuild_Jdk(String build_Jdk) { + Build_Jdk = build_Jdk; + } + + public String getGIT_Revision() { + return GIT_Revision; + } + + public String getCreate_By() { + return Create_By; + } + + public String getGIT_BRANCH() { + return GIT_BRANCH; + } + + public String getGIT_URL() { + return GIT_URL; + } + + public String getBUILD_DATE() { + return BUILD_DATE; + } + + public String getArtifactId() { + return artifactId; + } + + public String getGIT_Revision_SHORT() { + return GIT_Revision_SHORT; + } + + public String getVersion() { + return version; + } + + public String getProject() { + return project; + } + + public String getBuild_Jdk() { + return Build_Jdk; + } + + public String getGIT_DATE() { + return GIT_DATE; + } + + public void setGIT_DATE(String GIT_DATE) { + this.GIT_DATE = GIT_DATE; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java b/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java new file mode 100644 index 0000000..6129cb2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java @@ -0,0 +1,180 @@ +package com.genersoft.iot.vmp.common; + +/** + * @description: 定义常量 + * @author: swwheihei + * @date: 2019年5月30日 下午3:04:04 + * + */ +public class VideoManagerConstants { + + public static final String WVP_SERVER_PREFIX = "VMP_SIGNALLING_SERVER_INFO_"; + + public static final String WVP_SERVER_LIST = "VMP_SERVER_LIST"; + + public static final String WVP_SERVER_STREAM_PREFIX = "VMP_SIGNALLING_STREAM_"; + + public static final String MEDIA_SERVER_PREFIX = "VMP_MEDIA_SERVER_INFO:"; + + public static final String ONLINE_MEDIA_SERVERS_PREFIX = "VMP_ONLINE_MEDIA_SERVERS:"; + + public static final String DEVICE_PREFIX = "VMP_DEVICE_INFO"; + + public static final String INVITE_PREFIX = "VMP_GB_INVITE_INFO"; + + public static final String SEND_RTP_PORT = "VM_SEND_RTP_PORT:"; + public static final String SEND_RTP_INFO_CALLID = "VMP_SEND_RTP_INFO:CALL_ID:"; + public static final String SEND_RTP_INFO_STREAM = "VMP_SEND_RTP_INFO:STREAM:"; + public static final String SEND_RTP_INFO_CHANNEL = "VMP_SEND_RTP_INFO:CHANNEL:"; + + public static final String SIP_INVITE_SESSION = "VMP_SIP_INVITE_SESSION_INFO:"; + public static final String SIP_INVITE_SESSION_CALL_ID = SIP_INVITE_SESSION + "CALL_ID:"; + public static final String SIP_INVITE_SESSION_STREAM = SIP_INVITE_SESSION + "STREAM:"; + + public static final String MEDIA_STREAM_AUTHORITY = "VMP_MEDIA_STREAM_AUTHORITY"; + + public static final String SIP_CSEQ_PREFIX = "VMP_SIP_CSEQ_"; + + public static final String SIP_SUBSCRIBE_PREFIX = "VMP_SIP_SUBSCRIBE_"; + + public static final String SYSTEM_INFO_CPU_PREFIX = "VMP_SYSTEM_INFO_CPU_"; + + public static final String SYSTEM_INFO_MEM_PREFIX = "VMP_SYSTEM_INFO_MEM_"; + + public static final String SYSTEM_INFO_NET_PREFIX = "VMP_SYSTEM_INFO_NET_"; + + public static final String SYSTEM_INFO_DISK_PREFIX = "VMP_SYSTEM_INFO_DISK_"; + public static final String BROADCAST_WAITE_INVITE = "task_broadcast_waite_invite_"; + + public static final String PUSH_STREAM_LIST = "VMP_PUSH_STREAM_LIST_"; + public static final String WAITE_SEND_PUSH_STREAM = "VMP_WAITE_SEND_PUSH_STREAM:"; + public static final String START_SEND_PUSH_STREAM = "VMP_START_SEND_PUSH_STREAM:"; + public static final String SSE_TASK_KEY = "SSE_TASK_"; + public static final String DRAW_THIN_PROCESS_PREFIX = "VMP_DRAW_THIN_PROCESS_"; + + + + + //************************** redis 消息********************************* + + /** + * 流变化的通知 + */ + public static final String WVP_MSG_STREAM_CHANGE_PREFIX = "WVP_MSG_STREAM_CHANGE_"; + + /** + * 接收推流设备的GPS变化通知 + */ + public static final String VM_MSG_GPS = "VM_MSG_GPS"; + + /** + * 接收推流设备的GPS变化通知 + */ + public static final String VM_MSG_PUSH_STREAM_STATUS_CHANGE = "VM_MSG_PUSH_STREAM_STATUS_CHANGE"; + /** + * 接收推流设备列表更新变化通知 + */ + public static final String VM_MSG_PUSH_STREAM_LIST_CHANGE = "VM_MSG_PUSH_STREAM_LIST_CHANGE"; + + /** + * 请求同步三方组织结构 + */ + public static final String VM_MSG_GROUP_LIST_REQUEST = "VM_MSG_GROUP_LIST_REQUEST"; + + /** + * 同步三方组织结构回复 + */ + public static final String VM_MSG_GROUP_LIST_RESPONSE = "VM_MSG_GROUP_LIST_RESPONSE"; + + /** + * 同步三方组织结构回复 + */ + public static final String VM_MSG_GROUP_LIST_CHANGE = "VM_MSG_GROUP_LIST_CHANGE"; + + /** + * redis 消息通知设备推流到平台 + */ + public static final String VM_MSG_STREAM_PUSH_REQUESTED = "VM_MSG_STREAM_PUSH_REQUESTED"; + + /** + * redis 消息通知上级平台开始观看流 + */ + public static final String VM_MSG_STREAM_START_PLAY_NOTIFY = "VM_MSG_STREAM_START_PLAY_NOTIFY"; + + /** + * redis 消息通知上级平台停止观看流 + */ + public static final String VM_MSG_STREAM_STOP_PLAY_NOTIFY = "VM_MSG_STREAM_STOP_PLAY_NOTIFY"; + + /** + * redis 消息接收关闭一个推流 + */ + public static final String VM_MSG_STREAM_PUSH_CLOSE_REQUESTED = "VM_MSG_STREAM_PUSH_CLOSE_REQUESTED"; + + + /** + * redis 消息通知平台通知设备推流结果 + */ + public static final String VM_MSG_STREAM_PUSH_RESPONSE = "VM_MSG_STREAM_PUSH_RESPONSE"; + + /** + * redis 通知平台关闭推流 + */ + public static final String VM_MSG_STREAM_PUSH_CLOSE = "VM_MSG_STREAM_PUSH_CLOSE"; + + /** + * redis 消息请求所有的在线通道 + */ + public static final String VM_MSG_GET_ALL_ONLINE_REQUESTED = "VM_MSG_GET_ALL_ONLINE_REQUESTED"; + + /** + * 报警订阅的通知(收到报警向redis发出通知) + */ + public static final String VM_MSG_SUBSCRIBE_ALARM = "alarm"; + + + /** + * 报警通知的发送 (收到redis发出的通知,转发给其他平台) + */ + public static final String VM_MSG_SUBSCRIBE_ALARM_RECEIVE= "alarm_receive"; + + /** + * 设备状态订阅的通知 + */ + public static final String VM_MSG_SUBSCRIBE_DEVICE_STATUS = "device"; + + + + + //************************** 第三方 **************************************** + + public static final String WVP_STREAM_GB_ID_PREFIX = "memberNo_"; + public static final String WVP_STREAM_GPS_MSG_PREFIX = "WVP_STREAM_GPS_MSG_"; + public static final String WVP_OTHER_SEND_RTP_INFO = "VMP_OTHER_SEND_RTP_INFO_"; + public static final String WVP_OTHER_SEND_PS_INFO = "VMP_OTHER_SEND_PS_INFO_"; + public static final String WVP_OTHER_RECEIVE_RTP_INFO = "VMP_OTHER_RECEIVE_RTP_INFO_"; + public static final String WVP_OTHER_RECEIVE_PS_INFO = "VMP_OTHER_RECEIVE_PS_INFO_"; + + /** + * Redis Const + * 设备录像信息结果前缀 + */ + public static final String REDIS_RECORD_INFO_RES_PRE = "GB_RECORD_INFO_RES_"; + /** + * Redis Const + * 设备录像信息结果前缀 + */ + public static final String REDIS_RECORD_INFO_RES_COUNT_PRE = "GB_RECORD_INFO_RES_COUNT:"; + + //************************** 1078 **************************************** + + + public static final String INVITE_INFO_1078_POSITION = "INVITE_INFO_1078_POSITION:"; + public static final String INVITE_INFO_1078_PLAY = "INVITE_INFO_1078_PLAY:"; + public static final String INVITE_INFO_1078_PLAYBACK = "INVITE_INFO_1078_PLAYBACK:"; + public static final String INVITE_INFO_1078_TALK = "INVITE_INFO_1078_TALK:"; + + + public static final String RECORD_LIST_1078 = "RECORD_LIST_1078:"; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/enums/ChannelDataType.java b/src/main/java/com/genersoft/iot/vmp/common/enums/ChannelDataType.java new file mode 100644 index 0000000..f4b6b86 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/enums/ChannelDataType.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.common.enums; + +/** + * 支持的通道数据类型 + */ + +public class ChannelDataType { + + public final static int GB28181 = 1; + public final static int STREAM_PUSH = 2; + public final static int STREAM_PROXY = 3; + public final static int JT_1078 = 200; + + public final static String PLAY_SERVICE = "sourceChannelPlayService"; + public final static String PLAYBACK_SERVICE = "sourceChannelPlaybackService"; + public final static String DOWNLOAD_SERVICE = "sourceChannelDownloadService"; + public final static String PTZ_SERVICE = "sourceChannelPTZService"; + + + public static String getDateTypeDesc(Integer dataType) { + if (dataType == null) { + return "未知"; + } + return switch (dataType) { + case ChannelDataType.GB28181 -> "国标28181"; + case ChannelDataType.STREAM_PUSH -> "推流设备"; + case ChannelDataType.STREAM_PROXY -> "拉流代理"; + case ChannelDataType.JT_1078 -> "部标设备"; + default -> "未知"; + }; + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/enums/DeviceControlType.java b/src/main/java/com/genersoft/iot/vmp/common/enums/DeviceControlType.java new file mode 100644 index 0000000..02202d8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/enums/DeviceControlType.java @@ -0,0 +1,77 @@ +package com.genersoft.iot.vmp.common.enums; + +import org.dom4j.Element; +import org.springframework.util.ObjectUtils; + + +/** + * @author gaofuwang + * @date 2023/01/18/ 10:09:00 + * @since 1.0 + */ +public enum DeviceControlType { + + /** + * 云台控制 + * 上下左右,预置位,扫描,辅助功能,巡航 + */ + PTZ("PTZCmd","云台控制"), + /** + * 远程启动 + */ + TELE_BOOT("TeleBoot","远程启动"), + /** + * 录像控制 + */ + RECORD("RecordCmd","录像控制"), + /** + * 布防撤防 + */ + GUARD("GuardCmd","布防撤防"), + /** + * 告警控制 + */ + ALARM("AlarmCmd","告警控制"), + /** + * 强制关键帧 + */ + I_FRAME("IFameCmd","强制关键帧"), + /** + * 拉框放大 + */ + DRAG_ZOOM_IN("DragZoomIn","拉框放大"), + /** + * 拉框缩小 + */ + DRAG_ZOOM_OUT("DragZoomOut","拉框缩小"), + /** + * 看守位 + */ + HOME_POSITION("HomePosition","看守位"); + + private final String val; + + private final String desc; + + DeviceControlType(String val, String desc) { + this.val = val; + this.desc = desc; + } + + public String getVal() { + return val; + } + + public String getDesc() { + return desc; + } + + public static DeviceControlType typeOf(Element rootElement) { + for (DeviceControlType item : DeviceControlType.values()) { + if (!ObjectUtils.isEmpty(rootElement.element(item.val)) || !ObjectUtils.isEmpty(rootElement.elements(item.val))) { + return item; + } + } + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/CivilCodeFileConf.java b/src/main/java/com/genersoft/iot/vmp/conf/CivilCodeFileConf.java new file mode 100644 index 0000000..15dd874 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/CivilCodeFileConf.java @@ -0,0 +1,78 @@ +package com.genersoft.iot.vmp.conf; + +import com.genersoft.iot.vmp.common.CivilCodePo; +import com.genersoft.iot.vmp.utils.CivilCodeUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.ObjectUtils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +/** + * 启动时读取行政区划表 + */ +@Slf4j +@Configuration +@Order(value=15) +public class CivilCodeFileConf implements CommandLineRunner { + + @Autowired + @Lazy + private UserSetting userSetting; + + @Override + public void run(String... args) throws Exception { + if (ObjectUtils.isEmpty(userSetting.getCivilCodeFile())) { + log.warn("[行政区划] 文件未设置,可能造成目录刷新结果不完整"); + return; + } + InputStream inputStream; + if (userSetting.getCivilCodeFile().startsWith("classpath:")){ + String filePath = userSetting.getCivilCodeFile().substring("classpath:".length()); + ClassPathResource civilCodeFile = new ClassPathResource(filePath); + if (!civilCodeFile.exists()) { + log.warn("[行政区划] 文件<{}>不存在,可能造成目录刷新结果不完整", userSetting.getCivilCodeFile()); + return; + } + inputStream = civilCodeFile.getInputStream(); + + }else { + File civilCodeFile = new File(userSetting.getCivilCodeFile()); + if (!civilCodeFile.exists()) { + log.warn("[行政区划] 文件<{}>不存在,可能造成目录刷新结果不完整", userSetting.getCivilCodeFile()); + return; + } + inputStream = Files.newInputStream(civilCodeFile.toPath()); + } + + BufferedReader inputStreamReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + int index = -1; + String line; + while ((line = inputStreamReader.readLine()) != null) { + index ++; + if (index == 0) { + continue; + } + String[] infoArray = line.split(","); + CivilCodePo civilCodePo = CivilCodePo.getInstance(infoArray); + CivilCodeUtil.INSTANCE.add(civilCodePo); + } + inputStreamReader.close(); + inputStream.close(); + if (CivilCodeUtil.INSTANCE.isEmpty()) { + log.warn("[行政区划] 文件内容为空,可能造成目录刷新结果不完整"); + }else { + log.info("[行政区划] 加载成功,共加载数据{}条", CivilCodeUtil.INSTANCE.size()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/CloudRecordTimer.java b/src/main/java/com/genersoft/iot/vmp/conf/CloudRecordTimer.java new file mode 100644 index 0000000..cf4fff3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/CloudRecordTimer.java @@ -0,0 +1,77 @@ +package com.genersoft.iot.vmp.conf; + + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.bean.CloudRecordItem; +import com.genersoft.iot.vmp.storager.dao.CloudRecordServiceMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +/** + * 录像文件定时删除 + */ +@Slf4j +@Component +public class CloudRecordTimer { + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private CloudRecordServiceMapper cloudRecordServiceMapper; + + /** + * 定时查询待删除的录像文件 + */ +// @Scheduled(fixedRate = 10000) //每五秒执行一次,方便测试 + @Scheduled(cron = "0 0 0 * * ?") //每天的0点执行 + public void execute(){ + log.info("[录像文件定时清理] 开始清理过期录像文件"); + // 获取配置了assist的流媒体节点 + List mediaServerItemList = mediaServerService.getAllOnline(); + if (mediaServerItemList.isEmpty()) { + return; + } + long result = 0; + for (MediaServer mediaServerItem : mediaServerItemList) { + + Calendar lastCalendar = Calendar.getInstance(); + if (mediaServerItem.getRecordDay() > 0) { + lastCalendar.setTime(new Date()); + // 获取保存的最后截至日[期,因为每个节点都有一个日期,也就是支持每个节点设置不同的保存日期, + lastCalendar.add(Calendar.DAY_OF_MONTH, -mediaServerItem.getRecordDay()); + Long lastDate = lastCalendar.getTimeInMillis(); + + // 获取到截至日期之前的录像文件列表,文件列表满足未被收藏和保持的。这两个字段目前共能一致, + // 为我自己业务系统相关的代码,大家使用的时候直接使用收藏(collect)这一个类型即可 + List cloudRecordItemList = cloudRecordServiceMapper.queryRecordListForDelete(lastDate, mediaServerItem.getId()); + if (cloudRecordItemList.isEmpty()) { + continue; + } + // TODO 后续可以删除空了的过期日期文件夹 + for (CloudRecordItem cloudRecordItem : cloudRecordItemList) { + String date = new File(cloudRecordItem.getFilePath()).getParentFile().getName(); + try { + boolean deleteResult = mediaServerService.deleteRecordDirectory(mediaServerItem, cloudRecordItem.getApp(), + cloudRecordItem.getStream(), date, cloudRecordItem.getFileName()); + if (deleteResult) { + log.warn("[录像文件定时清理] 删除磁盘文件成功: {}", cloudRecordItem.getFilePath()); + } + }catch (ControllerException ignored) {} + + } + result += cloudRecordServiceMapper.deleteList(cloudRecordItemList); + } + } + log.info("[录像文件定时清理] 共清理{}个过期录像文件", result); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/DynamicTask.java b/src/main/java/com/genersoft/iot/vmp/conf/DynamicTask.java new file mode 100644 index 0000000..3a1fb91 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/DynamicTask.java @@ -0,0 +1,159 @@ +package com.genersoft.iot.vmp.conf; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import java.time.Instant; +import java.util.Date; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * 动态定时任务 + * @author lin + */ +@Slf4j +@Component +public class DynamicTask { + + private ThreadPoolTaskScheduler threadPoolTaskScheduler; + + private final Map> futureMap = new ConcurrentHashMap<>(); + private final Map runnableMap = new ConcurrentHashMap<>(); + + @PostConstruct + public void DynamicTask() { + threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setPoolSize(300); + threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true); + threadPoolTaskScheduler.setAwaitTerminationSeconds(10); + threadPoolTaskScheduler.setThreadNamePrefix("dynamicTask-"); + threadPoolTaskScheduler.initialize(); + } + + /** + * 循环执行的任务 + * @param key 任务ID + * @param task 任务 + * @param cycleForCatalog 间隔 毫秒 + * @return + */ + public void startCron(String key, Runnable task, int cycleForCatalog) { + if(ObjectUtils.isEmpty(key)) { + return; + } + ScheduledFuture future = futureMap.get(key); + if (future != null) { + if (future.isCancelled()) { + log.debug("任务【{}】已存在但是关闭状态!!!", key); + } else { + log.debug("任务【{}】已存在且已启动!!!", key); + return; + } + } + // scheduleWithFixedDelay 必须等待上一个任务结束才开始计时period, cycleForCatalog表示执行的间隔 + + future = threadPoolTaskScheduler.scheduleAtFixedRate(task, new Date(System.currentTimeMillis() + cycleForCatalog), cycleForCatalog); + if (future != null){ + futureMap.put(key, future); + runnableMap.put(key, task); + log.debug("任务【{}】启动成功!!!", key); + }else { + log.debug("任务【{}】启动失败!!!", key); + } + } + + /** + * 延时任务 + * @param key 任务ID + * @param task 任务 + * @param delay 延时 /毫秒 + * @return + */ + public void startDelay(String key, Runnable task, int delay) { + if(ObjectUtils.isEmpty(key)) { + return; + } + stop(key); + + // 获取执行的时刻 + Instant startInstant = Instant.now().plusMillis(TimeUnit.MILLISECONDS.toMillis(delay)); + + ScheduledFuture future = futureMap.get(key); + if (future != null) { + if (future.isCancelled()) { + log.debug("任务【{}】已存在但是关闭状态!!!", key); + } else { + log.debug("任务【{}】已存在且已启动!!!", key); + return; + } + } + // scheduleWithFixedDelay 必须等待上一个任务结束才开始计时period, cycleForCatalog表示执行的间隔 + future = threadPoolTaskScheduler.schedule(task, startInstant); + if (future != null){ + futureMap.put(key, future); + runnableMap.put(key, task); + log.debug("任务【{}】启动成功!!!", key); + }else { + log.debug("任务【{}】启动失败!!!", key); + } + } + + public boolean stop(String key) { + if(ObjectUtils.isEmpty(key)) { + return false; + } + boolean result = false; + if (!ObjectUtils.isEmpty(futureMap.get(key)) && !futureMap.get(key).isCancelled() && !futureMap.get(key).isDone()) { + result = futureMap.get(key).cancel(false); + futureMap.remove(key); + runnableMap.remove(key); + } + return result; + } + + public boolean contains(String key) { + if(ObjectUtils.isEmpty(key)) { + return false; + } + return futureMap.get(key) != null; + } + + public Set getAllKeys() { + return futureMap.keySet(); + } + + public Runnable get(String key) { + if(ObjectUtils.isEmpty(key)) { + return null; + } + return runnableMap.get(key); + } + + /** + * 每五分钟检查失效的任务,并移除 + */ + @Scheduled(cron="0 0/5 * * * ?") + public void execute(){ + if (futureMap.size() > 0) { + for (String key : futureMap.keySet()) { + ScheduledFuture future = futureMap.get(key); + if (future.isDone() || future.isCancelled()) { + futureMap.remove(key); + runnableMap.remove(key); + } + } + } + } + + public boolean isAlive(String key) { + return futureMap.get(key) != null && !futureMap.get(key).isDone() && !futureMap.get(key).isCancelled(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/GlobalExceptionHandler.java b/src/main/java/com/genersoft/iot/vmp/conf/GlobalExceptionHandler.java new file mode 100644 index 0000000..5e29ff4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/GlobalExceptionHandler.java @@ -0,0 +1,88 @@ +package com.genersoft.iot.vmp.conf; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 全局异常处理 + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 默认异常处理 + * @param e 异常 + * @return 统一返回结果 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public WVPResult exceptionHandler(Exception e) { + log.error("[全局异常]: ", e); + return WVPResult.fail(ErrorCode.ERROR500.getCode(), e.getMessage()); + } + + /** + * 默认异常处理 + * @param e 异常 + * @return 统一返回结果 + */ + @ExceptionHandler(IllegalStateException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public WVPResult exceptionHandler(IllegalStateException e) { + return WVPResult.fail(ErrorCode.ERROR400); + } + + /** + * 默认异常处理 + * @param e 异常 + * @return 统一返回结果 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public WVPResult exceptionHandler(HttpRequestMethodNotSupportedException e) { + return WVPResult.fail(ErrorCode.ERROR400); + } + /** + * 断言异常处理 + * @param e 异常 + * @return 统一返回结果 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.OK) + public WVPResult exceptionHandler(IllegalArgumentException e) { + return WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + + + /** + * 自定义异常处理, 处理controller中返回的错误 + * @param e 异常 + * @return 统一返回结果 + */ + @ExceptionHandler(ControllerException.class) + @ResponseStatus(HttpStatus.OK) + public ResponseEntity> exceptionHandler(ControllerException e) { + return new ResponseEntity<>(WVPResult.fail(e.getCode(), e.getMsg()), HttpStatus.OK); + } + + /** + * 登陆失败 + * @param e 异常 + * @return 统一返回结果 + */ + @ExceptionHandler(BadCredentialsException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity> exceptionHandler(BadCredentialsException e) { + return new ResponseEntity<>(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMessage()), HttpStatus.OK); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/GlobalResponseAdvice.java b/src/main/java/com/genersoft/iot/vmp/conf/GlobalResponseAdvice.java new file mode 100644 index 0000000..2db3bb5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/GlobalResponseAdvice.java @@ -0,0 +1,76 @@ +package com.genersoft.iot.vmp.conf; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import org.jetbrains.annotations.NotNull; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import java.util.LinkedHashMap; + +/** + * 全局统一返回结果 + * @author lin + */ +@RestControllerAdvice +public class GlobalResponseAdvice implements ResponseBodyAdvice { + + + @Override + public boolean supports(@NotNull MethodParameter returnType, @NotNull Class> converterType) { + return true; + } + + + @Override + public Object beforeBodyWrite(Object body, @NotNull MethodParameter returnType, @NotNull MediaType selectedContentType, @NotNull Class> selectedConverterType, @NotNull ServerHttpRequest request, @NotNull ServerHttpResponse response) { + // 排除api文档的接口,这个接口不需要统一 + String[] excludePath = {"/v3/api-docs","/api/v1","/index/hook","/api/video-"}; + for (String path : excludePath) { + if (request.getURI().getPath().startsWith(path)) { + return body; + } + } + + if (selectedContentType.equals(MediaType.parseMediaType("application/x-protobuf"))) { + return body; + } + + if (body instanceof WVPResult) { + return body; + } + + if (body instanceof ErrorCode) { + ErrorCode errorCode = (ErrorCode) body; + return new WVPResult<>(errorCode.getCode(), errorCode.getMsg(), null); + } + + if (body instanceof String) { + return JSON.toJSONString(WVPResult.success(body)); + } + + if (body instanceof LinkedHashMap) { + LinkedHashMap bodyMap = (LinkedHashMap) body; + if (bodyMap.get("status") != null && (Integer)bodyMap.get("status") != 200) { + return body; + } + } + + return WVPResult.success(body); + } + + /** + * 防止返回string时出错 + * @return + */ + /*@Bean + public HttpMessageConverters custHttpMessageConverter() { + return new HttpMessageConverters(new FastJsonHttpMessageConverter()); + }*/ +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java new file mode 100644 index 0000000..51a586c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java @@ -0,0 +1,203 @@ +package com.genersoft.iot.vmp.conf; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.util.ObjectUtils; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.regex.Pattern; + +@Slf4j +@Configuration("mediaConfig") +@Order(0) +@Data +public class MediaConfig{ + + // 修改必须配置,不再支持自动获取 + @Value("${media.id}") + private String id; + + @Value("${media.ip}") + private String ip; + + @Value("${media.wan_ip:}") + private String wanIp; + + @Value("${media.hook-ip:127.0.0.1}") + private String hookIp; + + @Value("${sip.domain}") + private String sipDomain; + + @Value("${media.sdp-ip:${media.wan_ip:}}") + private String sdpIp; + + @Value("${media.stream-ip:${media.wan_ip:}}") + private String streamIp; + + @Value("${media.http-port:0}") + private Integer httpPort; + + @Value("${media.flv-port:0}") + private Integer flvPort = 0; + + @Value("${media.mp4-port:0}") + private Integer mp4Port = 0; + + @Value("${media.ws-flv-port:0}") + private Integer wsFlvPort = 0; + + @Value("${media.http-ssl-port:0}") + private Integer httpSSlPort = 0; + + @Value("${media.flv-ssl-port:0}") + private Integer flvSSlPort = 0; + + @Value("${media.ws-flv-ssl-port:0}") + private Integer wsFlvSSlPort = 0; + + @Value("${media.rtmp-port:0}") + private Integer rtmpPort = 0; + + @Value("${media.rtmp-ssl-port:0}") + private Integer rtmpSSlPort = 0; + + @Value("${media.rtp-proxy-port:0}") + private Integer rtpProxyPort = 0; + + @Value("${media.jtt-proxy-port:0}") + private Integer jttProxyPort = 0; + + @Value("${media.rtsp-port:0}") + private Integer rtspPort = 0; + + @Value("${media.rtsp-ssl-port:0}") + private Integer rtspSSLPort = 0; + + @Value("${media.auto-config:true}") + private boolean autoConfig = true; + + @Value("${media.secret}") + private String secret; + + @Value("${media.rtp.enable}") + private boolean rtpEnable; + + @Value("${media.rtp.port-range}") + private String rtpPortRange; + + @Value("${media.rtp.send-port-range}") + private String rtpSendPortRange; + + @Value("${media.record-assist-port:0}") + private Integer recordAssistPort = 0; + + @Value("${media.record-day:7}") + private Integer recordDay; + + @Value("${media.record-path:}") + private String recordPath; + + @Value("${media.type:zlm}") + private String type; + + + + public int getRtpProxyPort() { + if (rtpProxyPort == null) { + return 0; + }else { + return rtpProxyPort; + } + + } + + public Integer getJttProxyPort() { + if (jttProxyPort == null) { + return 0; + }else { + return jttProxyPort; + } + } + + public String getSdpIp() { + if (ObjectUtils.isEmpty(sdpIp)){ + return ip; + }else { + if (isValidIPAddress(sdpIp)) { + return sdpIp; + }else { + // 按照域名解析 + String hostAddress = null; + try { + hostAddress = InetAddress.getByName(sdpIp).getHostAddress(); + } catch (UnknownHostException e) { + log.error("[获取SDP IP]: 域名解析失败"); + } + return hostAddress; + } + } + } + + public String getStreamIp() { + if (ObjectUtils.isEmpty(streamIp)){ + return ip; + }else { + return streamIp; + } + } + + public MediaServer getMediaSerItem(){ + MediaServer mediaServer = new MediaServer(); + mediaServer.setId(id); + mediaServer.setIp(ip); + mediaServer.setDefaultServer(true); + mediaServer.setHookIp(getHookIp()); + mediaServer.setSdpIp(getSdpIp()); + mediaServer.setStreamIp(getStreamIp()); + mediaServer.setHttpPort(httpPort); + mediaServer.setFlvPort(flvPort); + mediaServer.setMp4Port(mp4Port); + mediaServer.setWsFlvPort(wsFlvPort); + mediaServer.setFlvSSLPort(flvSSlPort); + mediaServer.setWsFlvSSLPort(wsFlvSSlPort); + + mediaServer.setHttpSSlPort(httpSSlPort); + mediaServer.setRtmpPort(rtmpPort); + mediaServer.setRtmpSSlPort(rtmpSSlPort); + mediaServer.setRtpProxyPort(getRtpProxyPort()); + mediaServer.setJttProxyPort(getJttProxyPort()); + mediaServer.setRtspPort(rtspPort); + mediaServer.setRtspSSLPort(rtspSSLPort); + mediaServer.setAutoConfig(autoConfig); + mediaServer.setSecret(secret); + mediaServer.setRtpEnable(rtpEnable); + mediaServer.setRtpPortRange(rtpPortRange); + mediaServer.setSendRtpPortRange(rtpSendPortRange); + mediaServer.setRecordAssistPort(recordAssistPort); + mediaServer.setHookAliveInterval(10f); + mediaServer.setRecordDay(recordDay); + mediaServer.setStatus(false); + mediaServer.setType(type); + if (recordPath != null) { + mediaServer.setRecordPath(recordPath); + } + mediaServer.setCreateTime(DateUtil.getNow()); + mediaServer.setUpdateTime(DateUtil.getNow()); + + return mediaServer; + } + + private boolean isValidIPAddress(String ipAddress) { + if ((ipAddress != null) && (!ipAddress.isEmpty())) { + return Pattern.matches("^([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}$", ipAddress); + } + return false; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/MediaStatusTimerTask.java b/src/main/java/com/genersoft/iot/vmp/conf/MediaStatusTimerTask.java new file mode 100644 index 0000000..761250e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/MediaStatusTimerTask.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.conf; + +import org.springframework.scheduling.annotation.Scheduled; + +/** + * 定时向zlm同步媒体流状态 + */ +public class MediaStatusTimerTask { + + +// @Scheduled(fixedRate = 2 * 1000) //每3秒执行一次 + public void execute(){ + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/MybatisConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/MybatisConfig.java new file mode 100644 index 0000000..7f25a36 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/MybatisConfig.java @@ -0,0 +1,62 @@ +package com.genersoft.iot.vmp.conf; + +import org.apache.ibatis.logging.stdout.StdOutImpl; +import org.apache.ibatis.mapping.DatabaseIdProvider; +import org.apache.ibatis.mapping.VendorDatabaseIdProvider; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * 配置mybatis + */ +@Configuration +@Order(value=1) +public class MybatisConfig { + + @Autowired + private UserSetting userSetting; + + @Bean + public DatabaseIdProvider databaseIdProvider() { + VendorDatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider(); + Properties properties = new Properties(); + properties.setProperty("Oracle", "oracle"); + properties.setProperty("MySQL", "mysql"); + properties.setProperty("DB2", "db2"); + properties.setProperty("Derby", "derby"); + properties.setProperty("H2", "h2"); + properties.setProperty("HSQL", "hsql"); + properties.setProperty("Informix", "informix"); + properties.setProperty("MS-SQL", "ms-sql"); + properties.setProperty("PostgreSQL", "postgresql"); + properties.setProperty("Sybase", "sybase"); + properties.setProperty("Hana", "hana"); + properties.setProperty("DM", "dm"); + properties.setProperty("KingbaseES", "kingbase"); + properties.setProperty("KingBase8", "kingbase"); + databaseIdProvider.setProperties(properties); + return databaseIdProvider; + } + + @Bean + public SqlSessionFactory sqlSessionFactory(DataSource dataSource, DatabaseIdProvider databaseIdProvider) throws Exception { + final SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean(); + sqlSessionFactory.setDataSource(dataSource); + org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration(); + if (userSetting.getSqlLog()){ + config.setLogImpl(StdOutImpl.class); + } + config.setMapUnderscoreToCamelCase(true); + sqlSessionFactory.setConfiguration(config); + sqlSessionFactory.setDatabaseIdProvider(databaseIdProvider); + return sqlSessionFactory.getObject(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ScheduleConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/ScheduleConfig.java new file mode 100644 index 0000000..df88bcf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ScheduleConfig.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.conf; + +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; + +import static com.genersoft.iot.vmp.conf.ThreadPoolTaskConfig.cpuNum; + +/** + * "@Scheduled"是Spring框架提供的一种定时任务执行机制,默认情况下它是单线程的,在同时执行多个定时任务时可能会出现阻塞和性能问题。 + * 为了解决这种单线程瓶颈问题,可以将定时任务的执行机制改为支持多线程 + */ +@Configuration +public class ScheduleConfig implements SchedulingConfigurer { + + /** + * 核心线程数(默认线程数) + */ + private static final int corePoolSize = Math.max(cpuNum, 20); + + /** + * 线程池名前缀 + */ + private static final String threadNamePrefix = "schedule"; + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(corePoolSize, + new BasicThreadFactory.Builder().namingPattern(threadNamePrefix).daemon(true).build(), + new ThreadPoolExecutor.CallerRunsPolicy()); + taskRegistrar.setScheduler(scheduledThreadPoolExecutor); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ServiceInfo.java b/src/main/java/com/genersoft/iot/vmp/conf/ServiceInfo.java new file mode 100644 index 0000000..67a0e4f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ServiceInfo.java @@ -0,0 +1,26 @@ +package com.genersoft.iot.vmp.conf; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class ServiceInfo implements ApplicationListener { + + @Getter + private static int serverPort; + + @Override + public void onApplicationEvent(WebServerInitializedEvent event) { + // 项目启动获取启动的端口号 + ServiceInfo.serverPort = event.getWebServer().getPort(); + log.info("项目启动获取启动的端口号: {}", ServiceInfo.serverPort); + } + + public void setServerPort(int serverPort) { + ServiceInfo.serverPort = serverPort; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/SipConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/SipConfig.java new file mode 100644 index 0000000..5822d64 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/SipConfig.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.conf; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@ConfigurationProperties(prefix = "sip", ignoreInvalidFields = true) +@Order(0) +@Data +public class SipConfig { + + private String ip; + + private String showIp; + + private List monitorIps; + + private Integer port; + + private String domain; + + private String id; + + private String password; + + Integer ptzSpeed = 50; + + Integer registerTimeInterval = 120; + + private boolean alarm = false; + + private long timeout = 1000; +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/SpringDocConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/SpringDocConfig.java new file mode 100644 index 0000000..65ebc3f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/SpringDocConfig.java @@ -0,0 +1,117 @@ +package com.genersoft.iot.vmp.conf; + +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.core.annotation.Order; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author lin + */ +@Configuration +@Order(1) +@ConditionalOnProperty(value = "user-settings.doc-enable", havingValue = "true", matchIfMissing = true) +public class SpringDocConfig { + + @Value("${doc.enabled: true}") + private boolean enable; + + @Bean + public OpenAPI springShopOpenApi() { + Contact contact = new Contact(); + contact.setName("pan"); + contact.setEmail("648540858@qq.com"); + + return new OpenAPI() + .components(new Components() + .addSecuritySchemes(JwtUtils.HEADER, new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .bearerFormat("JWT"))) + .info(new Info().title("WVP-PRO 接口文档") + .contact(contact) + .description("开箱即用的28181协议视频平台。
    " + + "1. 打开登录接口" + + " 登录成功后返回AccessToken。
    " + + "2. 填写到AccessToken到参数值 Token配置
    " + + "后续接口就可以直接测试了") + .version("v3.1.0") + .license(new License().name("Apache 2.0").url("http://springdoc.org"))); + } + + /** + * 添加分组 + * @return + */ + @Bean + public GroupedOpenApi publicApi() { + return GroupedOpenApi.builder() + .group("1. 全部") + .packagesToScan("com.genersoft.iot.vmp") + .build(); + } + + @Bean + public GroupedOpenApi publicApi2() { + return GroupedOpenApi.builder() + .group("2. 国标28181") + .packagesToScan("com.genersoft.iot.vmp.gb28181") + .build(); + } + + @Bean + public GroupedOpenApi publicApi3() { + return GroupedOpenApi.builder() + .group("3. 拉流转发") + .packagesToScan("com.genersoft.iot.vmp.streamProxy") + .build(); + } + + @Bean + public GroupedOpenApi publicApi4() { + return GroupedOpenApi.builder() + .group("4. 推流管理") + .packagesToScan("com.genersoft.iot.vmp.streamPush") + .build(); + } + + @Bean + public GroupedOpenApi publicApi5() { + return GroupedOpenApi.builder() + .group("4. 服务管理") + .packagesToScan("com.genersoft.iot.vmp.server") + .build(); + } + + @Bean + public GroupedOpenApi publicApi6() { + return GroupedOpenApi.builder() + .group("5. 用户管理") + .packagesToScan("com.genersoft.iot.vmp.user") + .build(); + } + + @Bean + public GroupedOpenApi publicApi7() { + return GroupedOpenApi.builder() + .group("6. 部标设备") + .packagesToScan("com.genersoft.iot.vmp.jt1078.controller") + .build(); + } + + @Bean + public GroupedOpenApi publicApi99() { + return GroupedOpenApi.builder() + .group("99. 第三方接口") + .packagesToScan("com.genersoft.iot.vmp.web.custom") + .build(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/StatisticsInfoTask.java b/src/main/java/com/genersoft/iot/vmp/conf/StatisticsInfoTask.java new file mode 100644 index 0000000..c558681 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/StatisticsInfoTask.java @@ -0,0 +1,98 @@ +package com.genersoft.iot.vmp.conf; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.StatisticsInfo; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.GitUtil; +import com.genersoft.iot.vmp.utils.SystemInfoUtils; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.io.File; +import java.sql.DatabaseMetaData; +import java.util.Objects; + +@Component +@Order(value=100) +@Slf4j +public class StatisticsInfoTask implements CommandLineRunner { + + @Autowired + private GitUtil gitUtil; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private DataSource dataSource; + + @Override + public void run(String... args) throws Exception { + try { + StatisticsInfo statisticsInfo = new StatisticsInfo(); + statisticsInfo.setDeviceId(SystemInfoUtils.getHardwareId()); + statisticsInfo.setBranch(gitUtil.getBranch()); + statisticsInfo.setGitCommitId(gitUtil.getGitCommitId()); + statisticsInfo.setGitUrl(gitUtil.getGitUrl()); + statisticsInfo.setVersion(gitUtil.getBuildVersion()); + + statisticsInfo.setOsName(System.getProperty("os.name")); + statisticsInfo.setArch(System.getProperty("os.arch")); + statisticsInfo.setJdkVersion(System.getProperty("java.version")); + + statisticsInfo.setDocker(new File("/.dockerenv").exists()); + try { + statisticsInfo.setRedisVersion(getRedisVersion()); + }catch (Exception ignored) {} + try { + DatabaseMetaData metaData = dataSource.getConnection().getMetaData(); + statisticsInfo.setSqlVersion(metaData.getDatabaseProductVersion()); + statisticsInfo.setSqlType(metaData.getDriverName()); + }catch (Exception ignored) {} + statisticsInfo.setTime(DateUtil.getNow()); + sendPost(statisticsInfo); + + + }catch (Exception e) { + log.error("[获取信息失败] ", e); + } + } + + public String getRedisVersion() { + if (redisTemplate.getConnectionFactory() == null) { + return null; + } + RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); + if (connection.info() == null) { + return null; + } + return connection.info().getProperty("redis_version"); + } + + public void sendPost(StatisticsInfo statisticsInfo) { + OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); + OkHttpClient client = httpClientBuilder.build(); + + RequestBody requestBodyJson = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), JSON.toJSONString(statisticsInfo)); + + Request request = new Request.Builder() + .post(requestBodyJson) + .url("http://api.wvp-pro.cn:136/api/statistics/ping") +// .url("http://127.0.0.1:11236/api/statistics/ping") + .addHeader("Content-Type", "application/json") + .build(); + try { + Response response = client.newCall(request).execute(); + response.close(); + Objects.requireNonNull(response.body()).close(); + + }catch (Exception ignored){} + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/SystemInfoTimerTask.java b/src/main/java/com/genersoft/iot/vmp/conf/SystemInfoTimerTask.java new file mode 100644 index 0000000..0c6eb23 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/SystemInfoTimerTask.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.conf; + +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.SystemInfoUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * 获取系统信息写入redis + */ +@Slf4j +@Component +public class SystemInfoTimerTask { + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Scheduled(fixedRate = 2000) //每1秒执行一次 + public void execute(){ + try { + double cpuInfo = SystemInfoUtils.getCpuInfo(); + redisCatchStorage.addCpuInfo(cpuInfo); + double memInfo = SystemInfoUtils.getMemInfo(); + redisCatchStorage.addMemInfo(memInfo); + Map networkInterfaces = SystemInfoUtils.getNetworkInterfaces(); + redisCatchStorage.addNetInfo(networkInterfaces); + List> diskInfo =SystemInfoUtils.getDiskInfo(); + redisCatchStorage.addDiskInfo(diskInfo); + } catch (InterruptedException e) { + log.error("[获取系统信息失败] {}", e.getMessage()); + } + + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ThreadPoolTaskConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/ThreadPoolTaskConfig.java new file mode 100644 index 0000000..a549a05 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ThreadPoolTaskConfig.java @@ -0,0 +1,67 @@ +package com.genersoft.iot.vmp.conf; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.ThreadPoolExecutor; + +/** + * ThreadPoolTask 配置类 + * @author lin + */ +@Configuration +@Order(1) +@EnableAsync(proxyTargetClass = true) +public class ThreadPoolTaskConfig { + + public static final int cpuNum = Runtime.getRuntime().availableProcessors(); + + /** + * 默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务, + * 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中; + * 当队列满了,就继续创建线程,当线程数量大于等于maxPoolSize后,开始使用拒绝策略拒绝 + */ + + /** + * 核心线程数(默认线程数) + */ + private static final int corePoolSize = Math.max(cpuNum * 2, 16); + /** + * 最大线程数 + */ + private static final int maxPoolSize = corePoolSize * 10; + /** + * 允许线程空闲时间(单位:默认为秒) + */ + private static final int keepAliveTime = 30; + + /** + * 缓冲队列大小 + */ + private static final int queueCapacity = 10000; + /** + * 线程池名前缀 + */ + private static final String threadNamePrefix = "async-"; + + + @Bean("taskExecutor") // bean的名称,默认为首字母小写的方法名 + public ThreadPoolTaskExecutor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(corePoolSize); + executor.setMaxPoolSize(maxPoolSize); + executor.setQueueCapacity(queueCapacity); + executor.setKeepAliveSeconds(keepAliveTime); + executor.setThreadNamePrefix(threadNamePrefix); + + // 线程池对拒绝任务的处理策略 + // CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + // 初始化 + executor.initialize(); + return executor; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java new file mode 100644 index 0000000..77beba7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java @@ -0,0 +1,222 @@ +package com.genersoft.iot.vmp.conf; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 配置文件 user-settings 映射的配置信息 + */ +@Component +@ConfigurationProperties(prefix = "user-settings", ignoreInvalidFields = true) +@Order(0) +@Data +public class UserSetting { + + /** + * 是否保存位置的历史记录(轨迹) + */ + private Boolean savePositionHistory = Boolean.FALSE; + + /** + * 是否开始自动点播: 请求流为未拉起的流时,自动开启点播, 需要rtp.enable=true + */ + private Boolean autoApplyPlay = Boolean.FALSE; + + /** + * [可选] 部分设备需要扩展SDP,需要打开此设置,一般设备无需打开 + */ + private Boolean seniorSdp = Boolean.FALSE; + + /** + * 点播/录像回放 等待超时时间,单位:毫秒 + */ + private Integer playTimeout = 10000; + + /** + * 获取设备录像数据超时时间,单位:毫秒 + */ + private Integer recordInfoTimeout = 15000; + + /** + * 上级点播等待超时时间,单位:毫秒 + */ + private int platformPlayTimeout = 20000; + + /** + * 是否开启接口鉴权 + */ + private Boolean interfaceAuthentication = Boolean.TRUE; + + /** + * 接口鉴权例外的接口, 即不进行接口鉴权的接口,尽量详细书写,尽量不用/**,至少两级目录 + */ + private List interfaceAuthenticationExcludes = new ArrayList<>(); + + /** + * 推流直播是否录制 + */ + private Boolean recordPushLive = Boolean.TRUE; + + /** + * 国标是否录制 + */ + private Boolean recordSip = Boolean.TRUE; + + /** + * 使用推流状态作为推流通道状态 + */ + private Boolean usePushingAsStatus = Boolean.FALSE; + + /** + * 使用来源请求ip作为streamIp,当且仅当你只有zlm节点它与wvp在一起的情况下开启 + */ + private Boolean useSourceIpAsStreamIp = Boolean.FALSE; + + /** + * 是否使用设备来源Ip作为回复IP, 不设置则为 false + */ + private Boolean sipUseSourceIpAsRemoteAddress = Boolean.FALSE; + + /** + * 国标点播 按需拉流, true:有人观看拉流,无人观看释放, false:拉起后不自动释放 + */ + private Boolean streamOnDemand = Boolean.TRUE; + + /** + * 推流鉴权, 默认开启 + */ + private Boolean pushAuthority = Boolean.TRUE; + + /** + * 设备上线时是否自动同步通道 + */ + private Boolean syncChannelOnDeviceOnline = Boolean.FALSE; + + /** + * 是否开启sip日志 + */ + private Boolean sipLog = Boolean.FALSE; + + /** + * 是否开启mybatis-sql日志 + */ + private Boolean sqlLog = Boolean.FALSE; + + /** + * 消息通道功能-缺少国标ID是否给所有上级发送消息 + */ + private Boolean sendToPlatformsWhenIdLost = Boolean.FALSE; + + /** + * 保持通道状态,不接受notify通道状态变化, 兼容海康平台发送错误消息 + */ + private Boolean refuseChannelStatusChannelFormNotify = Boolean.FALSE; + + /** + * 设备/通道状态变化时发送消息 + */ + private Boolean deviceStatusNotify = Boolean.TRUE; + + /** + * 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式 + */ + private Boolean useCustomSsrcForParentInvite = Boolean.TRUE; + + /** + * 开启接口文档页面。 默认开启,生产环境建议关闭,遇到swagger相关的漏洞时也可以关闭 + */ + private Boolean docEnable = Boolean.TRUE; + + /** + * 服务ID,不写则为000000 + */ + private String serverId = "000000"; + + + /** + * 国标级联语音喊话发流模式 * UDP:udp传输 TCP-ACTIVE:tcp主动模式 TCP-PASSIVE:tcp被动模式 + */ + private String broadcastForPlatform = "UDP"; + + /** + * 行政区划信息文件,系统启动时会加载到系统里 + */ + private String civilCodeFile = "classpath:civilCode.csv"; + + /** + * 跨域配置,不配置此项则允许所有跨域请求,配置后则只允许配置的页面的地址请求, 可以配置多个 + */ + private List allowedOrigins = new ArrayList<>(); + + /** + * 设置notify缓存队列最大长度,超过此长度的数据将返回486 BUSY_HERE,消息丢弃, 默认100000 + */ + private int maxNotifyCountQueue = 100000; + + /** + * 国标级联离线后多久重试一次注册 + */ + private int registerAgainAfterTime = 60; + + /** + * 国标续订方式,true为续订,每次注册在同一个会话里,false为重新注册,每次使用新的会话 + */ + private boolean registerKeepIntDialog = false; + + /** + * # 国标设备离线后的上线策略, + * # 0: 国标标准实现,设备离线后不回复心跳,直到设备重新注册上线, + * # 1(默认): 对于离线设备,收到心跳就把设备设置为上线,并更新注册时间为上次这次心跳的时间。防止过期时间判断异常 + */ + private int gbDeviceOnline = 1; + + /** + * 登录超时时间(分钟), + */ + private long loginTimeout = 60; + + /** + * jwk文件路径,若不指定则使用resources目录下的jwk.json + */ + private String jwkFile = "classpath:jwk.json"; + + /** + * wvp集群模式下如果注册向上级的wvp奔溃,则自动选择一个其他wvp继续注册到上级 + */ + private boolean autoRegisterPlatform = false; + + /** + * 按需发送推流设备位置, 默认发送移动位置订阅时如果位置不变则不发送, 设置为false按照国标间隔持续发送 + */ + private boolean sendPositionOnDemand = true; + + /** + * 部分设备会在短时间内发送大量注册, 导致协议栈内存溢出, 开启此项可以防止这部分设备注册, 避免服务崩溃,但是会降低系统性能, 描述如下 + * 默认值为 true。 + * 将此设置为 false 会使 Stack 在 Server Transaction 进入 TERMINATED 状态后关闭服务器套接字。 + * 这允许服务器防止客户端发起的基于 TCP 的拒绝服务攻击(即发起数百个客户端事务)。 + * 如果为 true(默认作),则堆栈将保持套接字打开,以便以牺牲线程和内存资源为代价来最大化性能 - 使自身容易受到 DOS 攻击。 + */ + private boolean sipCacheServerConnections = true; + + /** + * 禁用date头,变相禁用了校时 + */ + private boolean disableDateHeader = false; + + /** + * 同步业务分组时自动生成分组国标编号的模板,不配置则默认参考当前的sip域信息生成 + */ + private String groupSyncDeviceTemplate; + + /** + * 与第三方进行分组同步时使用别名而不是分组ID, 如果没有设置此项为true,那么分组编号就是必须传递的。如果是设置为true则,自动为别名的分组生成新的编号 + */ + private boolean useAliasForGroupSync = false; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/VersionConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/VersionConfig.java new file mode 100644 index 0000000..3d5bb5e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/VersionConfig.java @@ -0,0 +1,39 @@ +package com.genersoft.iot.vmp.conf; + +import org.springframework.core.annotation.Order; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "version") +@Order(0) +public class VersionConfig { + + private String version; + private String artifactId; + private String description; + + public void setVersion(String version) { + this.version = version; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getVersion() { + return version; + } + + public String getArtifactId() { + return artifactId; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/VersionInfo.java b/src/main/java/com/genersoft/iot/vmp/conf/VersionInfo.java new file mode 100644 index 0000000..eb408ab --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/VersionInfo.java @@ -0,0 +1,26 @@ +package com.genersoft.iot.vmp.conf; + +import com.genersoft.iot.vmp.common.VersionPo; +import com.genersoft.iot.vmp.utils.GitUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class VersionInfo { + + @Autowired + GitUtil gitUtil; + + public VersionPo getVersion() { + VersionPo versionPo = new VersionPo(); + versionPo.setGIT_Revision(gitUtil.getGitCommitId()); + versionPo.setGIT_BRANCH(gitUtil.getBranch()); + versionPo.setGIT_URL(gitUtil.getGitUrl()); + versionPo.setBUILD_DATE(gitUtil.getBuildDate()); + versionPo.setGIT_Revision_SHORT(gitUtil.getCommitIdShort()); + versionPo.setVersion(gitUtil.getBuildVersion()); + versionPo.setGIT_DATE(gitUtil.getCommitTime()); + + return versionPo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/WVPTimerTask.java b/src/main/java/com/genersoft/iot/vmp/conf/WVPTimerTask.java new file mode 100644 index 0000000..4a2098a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/WVPTimerTask.java @@ -0,0 +1,28 @@ +package com.genersoft.iot.vmp.conf; + +import com.genersoft.iot.vmp.common.ServerInfo; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class WVPTimerTask { + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Value("${server.port}") + private int serverPort; + + @Autowired + private SipConfig sipConfig; + + @Scheduled(fixedDelay = 2, timeUnit = TimeUnit.SECONDS) //每3秒执行一次 + public void execute(){ + redisCatchStorage.updateWVPInfo(ServerInfo.create(sipConfig.getShowIp(), serverPort), 3); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/exception/ControllerException.java b/src/main/java/com/genersoft/iot/vmp/conf/exception/ControllerException.java new file mode 100644 index 0000000..d2e1206 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/exception/ControllerException.java @@ -0,0 +1,37 @@ +package com.genersoft.iot.vmp.conf.exception; + +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; + +/** + * 自定义异常,controller出现错误时直接抛出异常由全局异常捕获并返回结果 + */ +public class ControllerException extends RuntimeException{ + + private int code; + private String msg; + + public ControllerException(int code, String msg) { + this.code = code; + this.msg = msg; + } + public ControllerException(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.msg = errorCode.getMsg(); + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/exception/ServiceException.java b/src/main/java/com/genersoft/iot/vmp/conf/exception/ServiceException.java new file mode 100644 index 0000000..5566d4b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/exception/ServiceException.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.conf.exception; + +/** + * @author lin + */ +public class ServiceException extends Exception{ + private String msg; + + + + public ServiceException(String msg) { + this.msg = msg; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + @Override + public String getMessage() { + return msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/exception/SsrcTransactionNotFoundException.java b/src/main/java/com/genersoft/iot/vmp/conf/exception/SsrcTransactionNotFoundException.java new file mode 100644 index 0000000..e1a738a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/exception/SsrcTransactionNotFoundException.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.conf.exception; + +/** + * @author lin + */ +public class SsrcTransactionNotFoundException extends Exception{ + private String deviceId; + private String channelId; + private String callId; + private String stream; + + + + public SsrcTransactionNotFoundException(String deviceId, String channelId, String callId, String stream) { + this.deviceId = deviceId; + this.channelId = channelId; + this.callId = callId; + this.stream = stream; + } + + public String getDeviceId() { + return deviceId; + } + + public String getChannelId() { + return channelId; + } + + public String getCallId() { + return callId; + } + + public String getStream() { + return stream; + } + + @Override + public String getMessage() { + StringBuffer msg = new StringBuffer(); + msg.append(String.format("缓存事务信息未找到,device:%s channel: %s ", deviceId, channelId)); + if (callId != null) { + msg.append(",callId: " + callId); + } + if (stream != null) { + msg.append(",stream: " + stream); + } + return msg.toString(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FileCallback.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FileCallback.java new file mode 100644 index 0000000..aa564fc --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FileCallback.java @@ -0,0 +1,8 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import java.io.OutputStream; + +public interface FileCallback { + + OutputStream run(String path); +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpAuthority.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpAuthority.java new file mode 100644 index 0000000..0ccb144 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpAuthority.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import org.apache.ftpserver.ftplet.Authority; +import org.apache.ftpserver.ftplet.AuthorizationRequest; + +public class FtpAuthority implements Authority { + + @Override + public boolean canAuthorize(AuthorizationRequest authorizationRequest) { + return true; + } + + @Override + public AuthorizationRequest authorize(AuthorizationRequest authorizationRequest) { + return authorizationRequest; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemFactory.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemFactory.java new file mode 100644 index 0000000..5d771e9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemFactory.java @@ -0,0 +1,33 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import org.apache.ftpserver.ftplet.FileSystemFactory; +import org.apache.ftpserver.ftplet.FileSystemView; +import org.apache.ftpserver.ftplet.FtpException; +import org.apache.ftpserver.ftplet.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.OutputStream; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class FtpFileSystemFactory implements FileSystemFactory { + + private final Map outputStreamMap = new ConcurrentHashMap<>(); + + @Override + public FileSystemView createFileSystemView(User user) throws FtpException { + return new FtpFileSystemView(user, path -> { + return outputStreamMap.get(path); + }); + } + + public void addOutputStream(String filePath, OutputStream outputStream) { + outputStreamMap.put(filePath, outputStream); + } + + public void removeOutputStream(String filePath) { + outputStreamMap.remove(filePath); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemView.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemView.java new file mode 100644 index 0000000..e7e8d3d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemView.java @@ -0,0 +1,63 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import org.apache.ftpserver.ftplet.FileSystemView; +import org.apache.ftpserver.ftplet.FtpException; +import org.apache.ftpserver.ftplet.FtpFile; +import org.apache.ftpserver.ftplet.User; + +import java.io.OutputStream; + +public class FtpFileSystemView implements FileSystemView { + + private User user; + + private FileCallback fileCallback; + + public FtpFileSystemView(User user, FileCallback fileCallback) { + this.user = user; + this.fileCallback = fileCallback; + } + + public static String HOME_PATH = "root"; + + public FtpFile workDir = VirtualFtpFile.getDir(HOME_PATH); + + @Override + public FtpFile getHomeDirectory() throws FtpException { + return VirtualFtpFile.getDir(HOME_PATH); + } + + @Override + public FtpFile getWorkingDirectory() throws FtpException { + return workDir; + } + + @Override + public boolean changeWorkingDirectory(String dir) throws FtpException { + workDir = VirtualFtpFile.getDir(dir); + return true; + } + + @Override + public FtpFile getFile(String file) throws FtpException { + VirtualFtpFile ftpFile = VirtualFtpFile.getFile(file); + if (fileCallback != null) { + OutputStream outputStream = fileCallback.run(workDir.getName()); + if (outputStream != null) { + ftpFile.setOutputStream(outputStream); + } + } + return ftpFile; + } + + @Override + public boolean isRandomAccessible() throws FtpException { + return true; + } + + @Override + public void dispose() { + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpServerConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpServerConfig.java new file mode 100644 index 0000000..d040163 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpServerConfig.java @@ -0,0 +1,69 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import lombok.extern.slf4j.Slf4j; +import org.apache.ftpserver.*; +import org.apache.ftpserver.ftplet.FtpException; +import org.apache.ftpserver.listener.Listener; +import org.apache.ftpserver.listener.ListenerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@ConditionalOnProperty(value = "ftp.enable", havingValue = "true") +@Slf4j +public class FtpServerConfig { + + @Autowired + private UserManager userManager; + + @Autowired + private FtpFileSystemFactory fileSystemFactory; + + @Autowired + private Ftplet ftplet; + + @Autowired + private FtpSetting ftpSetting; + + /** + * ftp server init + */ + @Bean + public FtpServer ftpServer() { + FtpServerFactory serverFactory = new FtpServerFactory(); + ListenerFactory listenerFactory = new ListenerFactory(); + // 1、设置服务端口 + listenerFactory.setPort(ftpSetting.getPort()); + // 2、设置被动模式数据上传的接口范围,云服务器需要开放对应区间的端口给客户端 + DataConnectionConfigurationFactory dataConnectionConfFactory = new DataConnectionConfigurationFactory(); + dataConnectionConfFactory.setPassivePorts(ftpSetting.getPassivePorts()); + listenerFactory.setDataConnectionConfiguration(dataConnectionConfFactory.createDataConnectionConfiguration()); + // 4、替换默认的监听器 + Listener listener = listenerFactory.createListener(); + serverFactory.addListener("default", listener); + // 5、配置自定义用户事件 + Map ftpLets = new HashMap<>(); + ftpLets.put("ftpService", ftplet); + serverFactory.setFtplets(ftpLets); + // 6、读取用户的配置信息 + // 6.2、设置用信息 + serverFactory.setUserManager(userManager); + serverFactory.setFileSystem(fileSystemFactory); + // 7、实例化FTP Server + FtpServer server = serverFactory.createServer(); + try { + server.start(); + if (!server.isStopped()) { + log.info("[FTP服务] 已启动, 端口: {}", ftpSetting.getPort()); + } + } catch (FtpException e) { + log.info("[FTP服务] 启动失败 ", e); + } + return server; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpSetting.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpSetting.java new file mode 100644 index 0000000..91acc8b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpSetting.java @@ -0,0 +1,21 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * 配置文件 user-settings 映射的配置信息 + */ +@Component +@ConfigurationProperties(prefix = "ftp", ignoreInvalidFields = true) +@Order(0) +@Data +public class FtpSetting { + + private Boolean enable = Boolean.FALSE; + + private int port = 21; + private String passivePorts = "10000-10500"; +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/Ftplet.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/Ftplet.java new file mode 100644 index 0000000..b413a42 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/Ftplet.java @@ -0,0 +1,59 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import com.genersoft.iot.vmp.jt1078.event.FtpUploadEvent; +import org.apache.ftpserver.ftplet.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import java.io.IOException; + + +@Component +public class Ftplet extends DefaultFtplet { + + private final Logger logger = LoggerFactory.getLogger(Ftplet.class); + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Override + public FtpletResult onUploadEnd(FtpSession session, FtpRequest request) throws FtpException, IOException { + FtpFile file = session.getFileSystemView().getFile(request.getArgument()); + if (file == null) { + return super.onUploadEnd(session, request); + } + sendEvent(file.getAbsolutePath()); + return super.onUploadUniqueEnd(session, request); + } + + @Override + public FtpletResult onAppendEnd(FtpSession session, FtpRequest request) throws FtpException, IOException { + FtpFile file = session.getFileSystemView().getFile(request.getArgument()); + if (file == null) { + return super.onUploadEnd(session, request); + } + sendEvent(file.getAbsolutePath()); + return super.onUploadUniqueEnd(session, request); + } + + @Override + public FtpletResult onUploadUniqueEnd(FtpSession session, FtpRequest request) throws FtpException, IOException { + FtpFile file = session.getFileSystemView().getFile(request.getArgument()); + if (file == null) { + return super.onUploadEnd(session, request); + } + sendEvent(file.getAbsolutePath()); + return super.onUploadUniqueEnd(session, request); + } + + private void sendEvent(String filePath){ + FtpUploadEvent event = new FtpUploadEvent(this); + logger.info("[文件已上传]: {}", filePath); + event.setFileName(filePath); + applicationEventPublisher.publishEvent(event); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/UserManager.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/UserManager.java new file mode 100644 index 0000000..da5c5ed --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/UserManager.java @@ -0,0 +1,86 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.ftpserver.ftplet.*; +import org.apache.ftpserver.usermanager.UsernamePasswordAuthentication; +import org.apache.ftpserver.usermanager.impl.BaseUser; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +@Component +public class UserManager implements org.apache.ftpserver.ftplet.UserManager { + + private static final String PREFIX = "VMP_FTP_USER_"; + + @Autowired + private RedisTemplate redisTemplate; + + + @Override + public User getUserByName(String username) throws FtpException { + return (BaseUser)redisTemplate.opsForValue().get(PREFIX + username); + } + + @Override + public String[] getAllUserNames() throws FtpException { + return new String[0]; + } + + @Override + public void delete(String username) throws FtpException { + + } + + @Override + public void save(User user) throws FtpException {} + + @Override + public boolean doesExist(String username) throws FtpException { + return redisTemplate.opsForValue().get(PREFIX + username) != null; + } + + @Override + public User authenticate(Authentication authentication) throws AuthenticationFailedException { + UsernamePasswordAuthentication usernamePasswordAuthentication = (UsernamePasswordAuthentication) authentication; + BaseUser user = (BaseUser)redisTemplate.opsForValue().get(PREFIX + usernamePasswordAuthentication.getUsername()); + if (user != null && usernamePasswordAuthentication.getPassword().equals(user.getPassword())) { + return user; + } + return null; + } + + @Override + public String getAdminName() throws FtpException { + return null; + } + + @Override + public boolean isAdmin(String username) throws FtpException { + return false; + } + + public BaseUser getRandomUser(){ + BaseUser use = new BaseUser(); + use.setName(RandomStringUtils.randomAlphabetic(6).toLowerCase()); + use.setPassword(RandomStringUtils.randomAlphabetic(6).toLowerCase()); + use.setEnabled(true); + use.setHomeDirectory("/"); + List authorities = new ArrayList<>(); + authorities.add(new FtpAuthority()); + use.setAuthorities(authorities); + String key = PREFIX + use.getName(); + + // 随机用户信息十分钟自动失效 + Duration duration = Duration.ofMinutes(10); + redisTemplate.opsForValue().set(key, use, duration); + return use; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/VirtualFtpFile.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/VirtualFtpFile.java new file mode 100644 index 0000000..5ee48ab --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/VirtualFtpFile.java @@ -0,0 +1,166 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import lombok.Setter; +import org.apache.ftpserver.ftplet.FtpFile; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collections; +import java.util.List; + +public class VirtualFtpFile implements FtpFile { + + @Setter + private String name; + + @Setter + private boolean hidden = false; + + @Setter + private boolean directory = false; + + @Setter + private String ownerName; + + private Long lastModified = null; + + @Setter + private long size = 0; + + @Setter + private OutputStream outputStream; + + public static VirtualFtpFile getFile(String name) { + VirtualFtpFile virtualFtpFile = new VirtualFtpFile(); + virtualFtpFile.setName(name); + return virtualFtpFile; + } + + public static VirtualFtpFile getDir(String name) { + if (name.endsWith("/")) { + name = name.replaceAll("/", ""); + } + VirtualFtpFile virtualFtpFile = new VirtualFtpFile(); + virtualFtpFile.setName(name); + virtualFtpFile.setDirectory(true); + return virtualFtpFile; + } + + @Override + public String getAbsolutePath() { + return FtpFileSystemView.HOME_PATH + "/" + name; + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isHidden() { + return hidden; + } + + @Override + public boolean isDirectory() { + return directory; + } + + @Override + public boolean isFile() { + return !directory; + } + + @Override + public boolean doesExist() { + return false; + } + + @Override + public boolean isReadable() { + return true; + } + + @Override + public boolean isWritable() { + return true; + } + + @Override + public boolean isRemovable() { + return true; + } + + @Override + public String getOwnerName() { + return ownerName; + } + + @Override + public String getGroupName() { + return "root"; + } + + @Override + public int getLinkCount() { + return 0; + } + + @Override + public long getLastModified() { + if (lastModified == null) { + lastModified = System.currentTimeMillis(); + } + return lastModified; + } + + @Override + public boolean setLastModified(long time) { + lastModified = time; + return true; + } + + @Override + public long getSize() { + return size; + } + + @Override + public Object getPhysicalFile() { + System.err.println("getPhysicalFile"); + return null; + } + + @Override + public boolean mkdir() { + return true; + } + + @Override + public boolean delete() { + return true; + } + + @Override + public boolean move(FtpFile destination) { + this.name = destination.getName(); + return true; + } + + @Override + public List listFiles() { + return Collections.emptyList(); + } + + @Override + public OutputStream createOutputStream(long offset) throws IOException { + return outputStream; + } + + @Override + public InputStream createInputStream(long offset) throws IOException { + System.out.println("createInputStream----"); + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisMsgListenConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisMsgListenConfig.java new file mode 100644 index 0000000..bbb94b3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisMsgListenConfig.java @@ -0,0 +1,76 @@ +package com.genersoft.iot.vmp.conf.redis; + + +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.service.redisMsg.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + + +/** + * @description:Redis中间件配置类,使用spring-data-redis集成,自动从application.yml中加载redis配置 + * @author: swwheihei + * @date: 2019年5月30日 上午10:58:25 + * + */ +@Configuration +@Order(value=1) +public class RedisMsgListenConfig { + + @Autowired + private RedisGpsMsgListener redisGPSMsgListener; + + @Autowired + private RedisAlarmMsgListener redisAlarmMsgListener; + + @Autowired + private RedisPushStreamStatusMsgListener redisPushStreamStatusMsgListener; + + @Autowired + private RedisPushStreamListMsgListener pushStreamListMsgListener; + + @Autowired + private RedisGroupMsgListener groupMsgListener; + + @Autowired + private RedisGroupChangeListener groupChangeListener; + + @Autowired + private RedisCloseStreamMsgListener redisCloseStreamMsgListener; + + @Autowired + private RedisRpcConfig redisRpcConfig; + + @Autowired + private RedisPushStreamResponseListener redisPushStreamCloseResponseListener; + + + /** + * redis消息监听器容器 可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器 + * 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理 + * + * @param connectionFactory + * @return + */ + @Bean + RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) { + + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + container.addMessageListener(redisGPSMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_GPS)); + container.addMessageListener(redisAlarmMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_SUBSCRIBE_ALARM_RECEIVE)); + container.addMessageListener(redisPushStreamStatusMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_PUSH_STREAM_STATUS_CHANGE)); + container.addMessageListener(pushStreamListMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_PUSH_STREAM_LIST_CHANGE)); + container.addMessageListener(redisCloseStreamMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_STREAM_PUSH_CLOSE)); + container.addMessageListener(redisRpcConfig, new PatternTopic(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY)); + container.addMessageListener(redisPushStreamCloseResponseListener, new PatternTopic(VideoManagerConstants.VM_MSG_STREAM_PUSH_RESPONSE)); + container.addMessageListener(groupMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_GROUP_LIST_RESPONSE)); + container.addMessageListener(groupChangeListener, new PatternTopic(VideoManagerConstants.VM_MSG_GROUP_LIST_CHANGE)); + return container; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java new file mode 100644 index 0000000..e413d72 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java @@ -0,0 +1,261 @@ +package com.genersoft.iot.vmp.conf.redis; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcClassHandler; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class RedisRpcConfig implements MessageListener { + + public final static String REDIS_REQUEST_CHANNEL_KEY = "WVP_REDIS_REQUEST_CHANNEL_KEY"; + + private final Random random = new Random(); + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + private ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Qualifier("taskExecutor") + @Autowired + private ThreadPoolTaskExecutor taskExecutor; + + private final static Map protocolHash = new HashMap<>(); + + public void addHandler(String path, RedisRpcClassHandler handler) { + protocolHash.put(path, handler); + } + +// @Override +// public void run(String... args) throws Exception { +// List> classList = ClassUtil.getClassList("com.genersoft.iot.vmp.service.redisMsg.control", RedisRpcController.class); +// for (Class handlerClass : classList) { +// String controllerPath = handlerClass.getAnnotation(RedisRpcController.class).value(); +// Object bean = ClassUtil.getBean(controllerPath, handlerClass); +// // 扫描其下的方法 +// Method[] methods = handlerClass.getDeclaredMethods(); +// for (Method method : methods) { +// RedisRpcMapping annotation = method.getAnnotation(RedisRpcMapping.class); +// if (annotation != null) { +// String methodPath = annotation.value(); +// if (methodPath != null) { +// protocolHash.put(controllerPath + "/" + methodPath, new RedisRpcClassHandler(bean, method)); +// } +// } +// +// } +// +// } +// for (String s : protocolHash.keySet()) { +// System.out.println(s); +// } +// if (log.isDebugEnabled()) { +// log.debug("消息ID缓存表 protocolHash:{}", protocolHash); +// } +// } + + @Override + public void onMessage(Message message, byte[] pattern) { + boolean isEmpty = taskQueue.isEmpty(); + taskQueue.offer(message); + if (isEmpty) { + taskExecutor.execute(() -> { + while (!taskQueue.isEmpty()) { + Message msg = taskQueue.poll(); + try { + RedisRpcMessage redisRpcMessage = JSON.parseObject(new String(msg.getBody()), RedisRpcMessage.class); + if (redisRpcMessage.getRequest() != null) { + handlerRequest(redisRpcMessage.getRequest()); + } else if (redisRpcMessage.getResponse() != null){ + handlerResponse(redisRpcMessage.getResponse()); + } else { + log.error("[redis-rpc]解析失败 {}", JSON.toJSONString(redisRpcMessage)); + } + } catch (Exception e) { + log.error("[redis-rpc]解析异常 {}",new String(msg.getBody()), e); + } + } + }); + } + } + + private void handlerResponse(RedisRpcResponse response) { + if (userSetting.getServerId().equals(response.getToId())) { + return; + } + log.info("[redis-rpc] << {}", response); + response(response); + } + + private void handlerRequest(RedisRpcRequest request) { + try { + if (userSetting.getServerId().equals(request.getFromId())) { + return; + } + log.info("[redis-rpc] << {}", request); + RedisRpcClassHandler redisRpcClassHandler = protocolHash.get(request.getUri()); + if (redisRpcClassHandler == null) { + log.error("[redis-rpc] 路径: {}不存在", request.getUri()); + return; + } + RpcController controller = redisRpcClassHandler.getController(); + Method method = redisRpcClassHandler.getMethod(); + // 没有携带目标ID的可以理解为哪个wvp有结果就哪个回复,携带目标ID,但是如果是不存在的uri则直接回复404 + if (userSetting.getServerId().equals(request.getToId())) { + if (method == null) { + // 回复404结果 + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.ERROR404.getCode()); + sendResponse(response); + return; + } + RedisRpcResponse response = (RedisRpcResponse)method.invoke(controller, request); + if(response != null) { + sendResponse(response); + } + }else { + if (method == null) { + // 回复404结果 + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.ERROR404.getCode()); + sendResponse(response); + return; + } + RedisRpcResponse response = (RedisRpcResponse)method.invoke(controller, request); + if (response != null) { + sendResponse(response); + } + } + }catch (Exception e) { + log.error("[redis-rpc ] 处理请求失败 ", e); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.ERROR100.getCode()); + sendResponse(response); + } + } + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(REDIS_REQUEST_CHANNEL_KEY, message); + } + + private void sendRequest(RedisRpcRequest request){ + log.info("[redis-rpc] >> {}", request); + RedisRpcMessage message = new RedisRpcMessage(); + message.setRequest(request); + redisTemplate.convertAndSend(REDIS_REQUEST_CHANNEL_KEY, message); + } + + private final Map> topicSubscribers = new ConcurrentHashMap<>(); + private final Map> callbacks = new ConcurrentHashMap<>(); + + public RedisRpcResponse request(RedisRpcRequest request, long timeOut) { + return request(request, timeOut, TimeUnit.SECONDS); + } + + public RedisRpcResponse request(RedisRpcRequest request, long timeOut, TimeUnit timeUnit) { + request.setSn((long) random.nextInt(1000) + 1); + SynchronousQueue subscribe = subscribe(request.getSn()); + + try { + sendRequest(request); + return subscribe.poll(timeOut, timeUnit); + } catch (InterruptedException e) { + log.warn("[redis rpc timeout] uri: {}, sn: {}", request.getUri(), request.getSn(), e); + RedisRpcResponse redisRpcResponse = new RedisRpcResponse(); + redisRpcResponse.setStatusCode(ErrorCode.ERROR486.getCode()); + return redisRpcResponse; + } finally { + this.unsubscribe(request.getSn()); + } + } + + public void request(RedisRpcRequest request, CommonCallback callback) { + request.setSn((long) random.nextInt(1000) + 1); + setCallback(request.getSn(), callback); + sendRequest(request); + } + + public Boolean response(RedisRpcResponse response) { + SynchronousQueue queue = topicSubscribers.get(response.getSn()); + CommonCallback callback = callbacks.get(response.getSn()); + if (queue != null) { + try { + return queue.offer(response, 2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error("{}", e.getMessage(), e); + } + }else if (callback != null) { + callback.run(response); + callbacks.remove(response.getSn()); + } + return false; + } + + private void unsubscribe(long key) { + topicSubscribers.remove(key); + } + + + private SynchronousQueue subscribe(long key) { + SynchronousQueue queue = null; + if (!topicSubscribers.containsKey(key)) + topicSubscribers.put(key, queue = new SynchronousQueue<>()); + return queue; + } + + private void setCallback(long key, CommonCallback callback) { + // TODO 如果多个上级点播同一个通道会有问题 + callbacks.put(key, callback); + } + + public void removeCallback(long key) { + callbacks.remove(key); + } + + + public int getCallbackCount(){ + return callbacks.size(); + } + + + + +// @Scheduled(fixedRate = 1000) //每1秒执行一次 +// public void execute(){ +// logger.info("callbacks的长度: " + callbacks.size()); +// logger.info("队列的长度: " + topicSubscribers.size()); +// logger.info("HOOK监听的长度: " + hookSubscribe.size()); +// logger.info(""); +// } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java new file mode 100644 index 0000000..f36a4d5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.conf.redis; + +import com.alibaba.fastjson2.support.spring.data.redis.GenericFastJsonRedisSerializer; +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisTemplateConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + // 使用fastJson序列化 + GenericFastJsonRedisSerializer fastJsonRedisSerializer = new GenericFastJsonRedisSerializer(); + // value值的序列化采用fastJsonRedisSerializer + redisTemplate.setValueSerializer(fastJsonRedisSerializer); + redisTemplate.setHashValueSerializer(fastJsonRedisSerializer); + + // key的序列化采用StringRedisSerializer + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory); + return redisTemplate; + } + + @Bean + public RedisTemplate getRedisTemplateForMobilePosition(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + // 使用fastJson序列化 + GenericFastJsonRedisSerializer fastJsonRedisSerializer = new GenericFastJsonRedisSerializer(); + // value值的序列化采用fastJsonRedisSerializer + redisTemplate.setValueSerializer(fastJsonRedisSerializer); + redisTemplate.setHashValueSerializer(fastJsonRedisSerializer); + + // key的序列化采用StringRedisSerializer + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory); + return redisTemplate; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcClassHandler.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcClassHandler.java new file mode 100644 index 0000000..1fab24b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcClassHandler.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.conf.redis.bean; + +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import lombok.Data; + +import java.lang.reflect.Method; + +@Data +public class RedisRpcClassHandler { + + private RpcController controller; + private Method method; + + public RedisRpcClassHandler(RpcController controller, Method method) { + this.controller = controller; + this.method = method; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcMessage.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcMessage.java new file mode 100644 index 0000000..061df6f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcMessage.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.conf.redis.bean; + +public class RedisRpcMessage { + + private RedisRpcRequest request; + + private RedisRpcResponse response; + + public RedisRpcRequest getRequest() { + return request; + } + + public void setRequest(RedisRpcRequest request) { + this.request = request; + } + + public RedisRpcResponse getResponse() { + return response; + } + + public void setResponse(RedisRpcResponse response) { + this.response = response; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcRequest.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcRequest.java new file mode 100644 index 0000000..a02db67 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcRequest.java @@ -0,0 +1,93 @@ +package com.genersoft.iot.vmp.conf.redis.bean; + +/** + * 通过redis发送请求 + */ +public class RedisRpcRequest { + + /** + * 来自的WVP ID + */ + private String fromId; + + + /** + * 目标的WVP ID + */ + private String toId; + + /** + * 序列号 + */ + private long sn; + + /** + * 访问的路径 + */ + private String uri; + + /** + * 参数 + */ + private Object param; + + public String getFromId() { + return fromId; + } + + public void setFromId(String fromId) { + this.fromId = fromId; + } + + public String getToId() { + return toId; + } + + public void setToId(String toId) { + this.toId = toId; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public Object getParam() { + return param; + } + + public void setParam(Object param) { + this.param = param; + } + + public long getSn() { + return sn; + } + + public void setSn(long sn) { + this.sn = sn; + } + + @Override + public String toString() { + return "RedisRpcRequest{" + + "uri='" + uri + '\'' + + ", fromId='" + fromId + '\'' + + ", toId='" + toId + '\'' + + ", sn=" + sn + + ", param=" + param + + '}'; + } + + public RedisRpcResponse getResponse() { + RedisRpcResponse response = new RedisRpcResponse(); + response.setFromId(fromId); + response.setToId(toId); + response.setSn(sn); + response.setUri(uri); + return response; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcResponse.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcResponse.java new file mode 100644 index 0000000..21f9e7e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcResponse.java @@ -0,0 +1,99 @@ +package com.genersoft.iot.vmp.conf.redis.bean; + +/** + * 通过redis发送回复 + */ +public class RedisRpcResponse { + + /** + * 来自的WVP ID + */ + private String fromId; + + + /** + * 目标的WVP ID + */ + private String toId; + + + /** + * 序列号 + */ + private long sn; + + /** + * 状态码 + */ + private int statusCode; + + /** + * 访问的路径 + */ + private String uri; + + /** + * 参数 + */ + private Object body; + + public String getFromId() { + return fromId; + } + + public void setFromId(String fromId) { + this.fromId = fromId; + } + + public String getToId() { + return toId; + } + + public void setToId(String toId) { + this.toId = toId; + } + + public long getSn() { + return sn; + } + + public void setSn(long sn) { + this.sn = sn; + } + + public int getStatusCode() { + return statusCode; + } + + public void setStatusCode(int statusCode) { + this.statusCode = statusCode; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public Object getBody() { + return body; + } + + public void setBody(Object body) { + this.body = body; + } + + @Override + public String toString() { + return "RedisRpcResponse{" + + "uri='" + uri + '\'' + + ", fromId='" + fromId + '\'' + + ", toId='" + toId + '\'' + + ", sn=" + sn + + ", statusCode=" + statusCode + + ", body=" + body + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/AnonymousAuthenticationEntryPoint.java b/src/main/java/com/genersoft/iot/vmp/conf/security/AnonymousAuthenticationEntryPoint.java new file mode 100644 index 0000000..08827c5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/AnonymousAuthenticationEntryPoint.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.conf.security; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.security.dto.JwtUser; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * 处理匿名用户访问逻辑 + * @author lin + */ +@Component +public class AnonymousAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { + String jwt = request.getHeader(JwtUtils.getHeader()); + JwtUser jwtUser = JwtUtils.verifyToken(jwt); + String username = jwtUser.getUserName(); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, jwtUser.getPassword() ); + SecurityContextHolder.getContext().setAuthentication(token); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("code", ErrorCode.ERROR401.getCode()); + jsonObject.put("msg", ErrorCode.ERROR401.getMsg()); + String logUri = "api/user/login"; + if (request.getRequestURI().contains(logUri)){ + jsonObject.put("msg", e.getMessage()); + } + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + try { + response.getWriter().print(jsonObject.toJSONString()); + } catch (IOException ioException) { + ioException.printStackTrace(); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/DefaultUserDetailsServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/conf/security/DefaultUserDetailsServiceImpl.java new file mode 100644 index 0000000..d21fdaa --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/DefaultUserDetailsServiceImpl.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.conf.security; + +import com.alibaba.excel.util.StringUtils; +import com.genersoft.iot.vmp.conf.security.dto.LoginUser; +import com.genersoft.iot.vmp.service.IUserService; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * 用户登录认证逻辑 + */ +@Slf4j +@Component +public class DefaultUserDetailsServiceImpl implements UserDetailsService { + + @Autowired + private IUserService userService; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + if (StringUtils.isBlank(username)) { + log.info("登录用户:{} 不存在", username); + throw new UsernameNotFoundException("登录用户:" + username + " 不存在"); + } + + // 查出密码 + User user = userService.getUserByUsername(username); + if (user == null) { + log.info("登录用户:{} 不存在", username); + throw new UsernameNotFoundException("登录用户:" + username + " 不存在"); + } + String password = SecurityUtils.encryptPassword(user.getPassword()); + user.setPassword(password); + return new LoginUser(user, LocalDateTime.now()); + } + + + + + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..19fd4f3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java @@ -0,0 +1,114 @@ +package com.genersoft.iot.vmp.conf.security; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.security.dto.JwtUser; +import com.genersoft.iot.vmp.storager.dao.dto.Role; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * jwt token 过滤器 + */ + +@Slf4j +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final static String WSHeader = "sec-websocket-protocol"; + + + @Autowired + private UserSetting userSetting; + + + @Override + protected void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + ContentCachingRequestWrapper request = new ContentCachingRequestWrapper(servletRequest); + // 忽略登录请求的token验证 + String requestURI = request.getRequestURI(); + if ((requestURI.startsWith("/doc.html") || requestURI.startsWith("/swagger-ui") ) && !userSetting.getDocEnable()) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + if (requestURI.equalsIgnoreCase("/api/user/login")) { + chain.doFilter(request, response); + return; + } + + if (!userSetting.getInterfaceAuthentication()) { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(null, null, new ArrayList<>() ); + SecurityContextHolder.getContext().setAuthentication(token); + chain.doFilter(request, response); + return; + } + + String jwt = request.getHeader(JwtUtils.getHeader()); + // 这里如果没有jwt,继续往后走,因为后面还有鉴权管理器等去判断是否拥有身份凭证,所以是可以放行的 + // 没有jwt相当于匿名访问,若有一些接口是需要权限的,则不能访问这些接口 + + // websocket 鉴权信息默认存储在这里 + String secWebsocketProtocolHeader = request.getHeader(WSHeader); + if (StringUtils.isBlank(jwt)) { + + if (secWebsocketProtocolHeader != null) { + jwt = secWebsocketProtocolHeader; + response.setHeader(WSHeader, secWebsocketProtocolHeader); + }else { + jwt = request.getParameter(JwtUtils.getHeader()); + } + if (StringUtils.isBlank(jwt)) { + jwt = request.getHeader(JwtUtils.getApiKeyHeader()); + if (StringUtils.isBlank(jwt)) { + chain.doFilter(request, response); + return; + } + } + } + + JwtUser jwtUser = JwtUtils.verifyToken(jwt); + String username = jwtUser.getUserName(); + // TODO 处理各个状态 + switch (jwtUser.getStatus()){ + case EXPIRED: + response.setStatus(401); + chain.doFilter(request, response); + // 异常 + return; + case EXCEPTION: + // 过期 + response.setStatus(400); + chain.doFilter(request, response); + return; + case EXPIRING_SOON: + // 即将过期 +// return; + default: + } + // 构建UsernamePasswordAuthenticationToken,这里密码为null,是因为提供了正确的JWT,实现自动登录 + User user = new User(); + user.setId(jwtUser.getUserId()); + user.setUsername(jwtUser.getUserName()); + user.setPassword(jwtUser.getPassword()); + Role role = new Role(); + role.setId(jwtUser.getRoleId()); + user.setRole(role); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, jwtUser.getPassword(), new ArrayList<>() ); + SecurityContextHolder.getContext().setAuthentication(token); + chain.doFilter(request, response); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java new file mode 100644 index 0000000..f6528a0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java @@ -0,0 +1,274 @@ +package com.genersoft.iot.vmp.conf.security; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.security.dto.JwtUser; +import com.genersoft.iot.vmp.service.IUserApiKeyService; +import com.genersoft.iot.vmp.service.IUserService; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import com.genersoft.iot.vmp.storager.dao.dto.UserApiKey; +import lombok.extern.slf4j.Slf4j; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jwk.JsonWebKeySet; +import org.jose4j.jwk.RsaJsonWebKey; +import org.jose4j.jwk.RsaJwkGenerator; +import org.jose4j.jws.AlgorithmIdentifiers; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.jwt.NumericDate; +import org.jose4j.jwt.consumer.ErrorCodes; +import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.jwt.consumer.JwtConsumer; +import org.jose4j.jwt.consumer.JwtConsumerBuilder; +import org.jose4j.lang.JoseException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +public class JwtUtils implements InitializingBean { + + public static final String HEADER = "access-token"; + + public static final String API_KEY_HEADER = "api-key"; + + private static final String AUDIENCE = "Audience"; + + private static final String keyId = "3e79646c4dbc408383a9eed09f2b85ae"; + + /** + * token过期时间(分钟) + */ + public static final long EXPIRATION_TIME = 30; + + private static RsaJsonWebKey rsaJsonWebKey; + + private static IUserService userService; + + private static IUserApiKeyService userApiKeyService; + + private static UserSetting userSetting; + + public static String getApiKeyHeader() { + return API_KEY_HEADER; + } + + @Resource + public void setUserService(IUserService userService) { + JwtUtils.userService = userService; + } + + @Resource + public void setUserApiKeyService(IUserApiKeyService userApiKeyService) { + JwtUtils.userApiKeyService = userApiKeyService; + } + + @Resource + public void setUserSetting(UserSetting userSetting) { + JwtUtils.userSetting = userSetting; + } + + @Override + public void afterPropertiesSet() { + try { + rsaJsonWebKey = generateRsaJsonWebKey(); + } catch (JoseException e) { + log.error("生成RsaJsonWebKey报错。", e); + } + } + + /** + * 创建密钥对 + * + * @throws JoseException JoseException + */ + private RsaJsonWebKey generateRsaJsonWebKey() throws JoseException { + RsaJsonWebKey rsaJsonWebKey = null; + try { + String jwkFile = userSetting.getJwkFile(); + InputStream inputStream = null; + if (jwkFile.startsWith("classpath:")){ + String filePath = jwkFile.substring("classpath:".length()); + ClassPathResource civilCodeFile = new ClassPathResource(filePath); + if (civilCodeFile.exists()) { + inputStream = civilCodeFile.getInputStream(); + } + }else { + File civilCodeFile = new File(userSetting.getCivilCodeFile()); + if (civilCodeFile.exists()) { + inputStream = Files.newInputStream(civilCodeFile.toPath()); + } + + } + if (inputStream == null ) { + log.warn("[API AUTH] 读取jwk.json失败,文件不存在,将使用新生成的随机RSA密钥对"); + // 生成一个RSA密钥对,该密钥对将用于JWT的签名和验证,包装在JWK中 + rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048); + // 给JWK一个密钥ID + rsaJsonWebKey.setKeyId(keyId); + return rsaJsonWebKey; + } + BufferedReader inputStreamReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + int index = -1; + String line; + StringBuilder content = new StringBuilder(); + while ((line = inputStreamReader.readLine()) != null) { + content.append(line); + index ++; + if (index == 0) { + continue; + } + } + inputStreamReader.close(); + inputStream.close(); + + + String jwkJson = content.toString(); + JsonWebKeySet jsonWebKeySet = new JsonWebKeySet(jwkJson); + List jsonWebKeys = jsonWebKeySet.getJsonWebKeys(); + if (!jsonWebKeys.isEmpty()) { + JsonWebKey jsonWebKey = jsonWebKeys.get(0); + if (jsonWebKey instanceof RsaJsonWebKey) { + rsaJsonWebKey = (RsaJsonWebKey) jsonWebKey; + } + } + } catch (Exception ignore) {} + if (rsaJsonWebKey == null) { + log.warn("[API AUTH] 读取jwk.json失败,获取内容失败,将使用新生成的随机RSA密钥对"); + // 生成一个RSA密钥对,该密钥对将用于JWT的签名和验证,包装在JWK中 + rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048); + // 给JWK一个密钥ID + rsaJsonWebKey.setKeyId(keyId); + }else { + log.info("[API AUTH] 读取jwk.json成功"); + } + return rsaJsonWebKey; + } + + public static String createToken(String username, Long expirationTime, Map extra) { + try { + /* + * “iss” (issuer) 发行人 + * “sub” (subject) 主题 + * “aud” (audience) 接收方 用户 + * “exp” (expiration time) 到期时间 + * “nbf” (not before) 在此之前不可用 + * “iat” (issued at) jwt的签发时间 + */ + JwtClaims claims = new JwtClaims(); + claims.setGeneratedJwtId(); + claims.setIssuedAtToNow(); + // 令牌将过期的时间 分钟 + if (expirationTime != null) { + claims.setExpirationTimeMinutesInTheFuture(expirationTime); + } + claims.setNotBeforeMinutesInThePast(0); + claims.setSubject("login"); + claims.setAudience(AUDIENCE); + //添加自定义参数,必须是字符串类型 + claims.setClaim("userName", username); + if (extra != null) { + extra.forEach(claims::setClaim); + } + //jws + JsonWebSignature jws = new JsonWebSignature(); + //签名算法RS256 + jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256); + jws.setKeyIdHeaderValue(keyId); + jws.setPayload(claims.toJson()); + + jws.setKey(rsaJsonWebKey.getPrivateKey()); + + //get token + return jws.getCompactSerialization(); + } catch (JoseException e) { + log.error("[Token生成失败]: {}", e.getMessage()); + } + return null; + } + + public static String createToken(String username, Long expirationTime) { + return createToken(username, expirationTime, null); + } + + public static String createToken(String username) { + return createToken(username, userSetting.getLoginTimeout()); + } + + public static String getHeader() { + return HEADER; + } + + public static JwtUser verifyToken(String token) { + + JwtUser jwtUser = new JwtUser(); + + try { + JwtConsumer consumer = new JwtConsumerBuilder() + //.setRequireExpirationTime() + //.setMaxFutureValidityInMinutes(5256000) + .setAllowedClockSkewInSeconds(30) + .setRequireSubject() + //.setExpectedIssuer("") + .setExpectedAudience(AUDIENCE) + .setVerificationKey(rsaJsonWebKey.getPublicKey()) + .build(); + + JwtClaims claims = consumer.processToClaims(token); + NumericDate expirationTime = claims.getExpirationTime(); + if (expirationTime != null) { + // 判断是否即将过期, 默认剩余时间小于5分钟未即将过期 + // 剩余时间 (秒) + long timeRemaining = expirationTime.getValue() - LocalDateTime.now().toEpochSecond(ZoneOffset.ofHours(8)); + if (timeRemaining < 5 * 60) { + jwtUser.setStatus(JwtUser.TokenStatus.EXPIRING_SOON); + } else { + jwtUser.setStatus(JwtUser.TokenStatus.NORMAL); + } + } else { + jwtUser.setStatus(JwtUser.TokenStatus.NORMAL); + } + + Long apiKeyId = claims.getClaimValue("apiKeyId", Long.class); + if (apiKeyId != null) { + UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(apiKeyId.intValue()); + if (userApiKey == null || !userApiKey.isEnable()) { + jwtUser.setStatus(JwtUser.TokenStatus.EXPIRED); + } + } + + String username = (String) claims.getClaimValue("userName"); + User user = userService.getUserByUsername(username); + + jwtUser.setUserName(username); + jwtUser.setPassword(user.getPassword()); + jwtUser.setRoleId(user.getRole().getId()); + jwtUser.setUserId(user.getId()); + + return jwtUser; + } catch (InvalidJwtException e) { + if (e.hasErrorCode(ErrorCodes.EXPIRED)) { + jwtUser.setStatus(JwtUser.TokenStatus.EXPIRED); + } else { + jwtUser.setStatus(JwtUser.TokenStatus.EXCEPTION); + } + return jwtUser; + } catch (Exception e) { + log.error("[Token解析失败]: {}", e.getMessage()); + jwtUser.setStatus(JwtUser.TokenStatus.EXPIRED); + return jwtUser; + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/LogoutHandler.java b/src/main/java/com/genersoft/iot/vmp/conf/security/LogoutHandler.java new file mode 100644 index 0000000..4809e32 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/LogoutHandler.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.conf.security; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.stereotype.Component; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 退出登录成功 + */ +@Slf4j +@Component +public class LogoutHandler implements LogoutSuccessHandler { + + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { + String username = request.getParameter("username"); + log.info("[退出登录成功] - [{}]", username); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/SecurityUtils.java b/src/main/java/com/genersoft/iot/vmp/conf/security/SecurityUtils.java new file mode 100644 index 0000000..f012f7e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/SecurityUtils.java @@ -0,0 +1,84 @@ +package com.genersoft.iot.vmp.conf.security; + +import com.genersoft.iot.vmp.conf.security.dto.LoginUser; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import javax.security.sasl.AuthenticationException; +import java.time.LocalDateTime; + +public class SecurityUtils { + + /** + * 描述根据账号密码进行调用security进行认证授权 主动调 + * 用AuthenticationManager的authenticate方法实现 + * 授权成功后将用户信息存入SecurityContext当中 + * @param username 用户名 + * @param password 密码 + * @param authenticationManager 认证授权管理器, + * @see AuthenticationManager + * @return UserInfo 用户信息 + */ + public static LoginUser login(String username, String password, AuthenticationManager authenticationManager) throws AuthenticationException { + //使用security框架自带的验证token生成器 也可以自定义。 + UsernamePasswordAuthenticationToken token =new UsernamePasswordAuthenticationToken(username,password); + //认证 如果失败,这里会自动异常后返回,所以这里不需要判断返回值是否为空,确定是否登录成功 + Authentication authenticate = authenticationManager.authenticate(token); + LoginUser user = (LoginUser) authenticate.getPrincipal(); + + SecurityContextHolder.getContext().setAuthentication(token); + + return user; + } + + /** + * 获取当前登录的所有认证信息 + * @return + */ + public static Authentication getAuthentication(){ + SecurityContext context = SecurityContextHolder.getContext(); + return context.getAuthentication(); + } + + /** + * 获取当前登录用户信息 + * @return + */ + public static LoginUser getUserInfo(){ + Authentication authentication = getAuthentication(); + if(authentication!=null){ + Object principal = authentication.getPrincipal(); + if(principal!=null && !"anonymousUser".equals(principal.toString())){ + + User user = (User) principal; + return new LoginUser(user, LocalDateTime.now()); + } + } + return null; + } + + /** + * 获取当前登录用户ID + * @return + */ + public static int getUserId(){ + LoginUser user = getUserInfo(); + return user.getId(); + } + + /** + * 生成BCryptPasswordEncoder密码 + * + * @param password 密码 + * @return 加密字符串 + */ + public static String encryptPassword(String password) { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.encode(password); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java new file mode 100644 index 0000000..7099337 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java @@ -0,0 +1,159 @@ +package com.genersoft.iot.vmp.conf.security; + +import com.genersoft.iot.vmp.conf.UserSetting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 配置Spring Security + * + * @author lin + */ +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +@Order(1) +@Slf4j +public class WebSecurityConfig { + + @Autowired + private UserSetting userSetting; + + @Autowired + private DefaultUserDetailsServiceImpl userDetailsService; + /** + * 登出成功的处理 + */ + @Autowired + private LogoutHandler logoutHandler; + /** + * 未登录的处理 + */ + @Autowired + private AnonymousAuthenticationEntryPoint anonymousAuthenticationEntryPoint; + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + public AuthenticationProvider authProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + // 设置不隐藏 未找到用户异常 + provider.setHideUserNotFoundExceptions(true); + // 用户认证service - 查询数据库的逻辑 + provider.setUserDetailsService(userDetailsService); + // 设置密码加密算法 + provider.setPasswordEncoder(passwordEncoder()); + return provider; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + List defaultExcludes = new ArrayList<>(); + defaultExcludes.add("/"); + defaultExcludes.add("/#/**"); + defaultExcludes.add("/static/**"); + + defaultExcludes.add("/swagger-ui.html"); + defaultExcludes.add("/swagger-ui/**"); + defaultExcludes.add("/swagger-resources/**"); + defaultExcludes.add("/doc.html"); + defaultExcludes.add("/doc.html#/**"); + defaultExcludes.add("/v3/api-docs/**"); + + defaultExcludes.add("/index.html"); + defaultExcludes.add("/webjars/**"); + + defaultExcludes.add("/js/**"); + defaultExcludes.add("/api/device/query/snap/**"); + defaultExcludes.add("/record_proxy/*/**"); + defaultExcludes.add("/api/emit"); + defaultExcludes.add("/favicon.ico"); + defaultExcludes.add("/api/user/login"); + defaultExcludes.add("/index/hook/**"); + defaultExcludes.add("/api/device/query/snap/**"); + defaultExcludes.add("/index/hook/abl/**"); + defaultExcludes.add("/api/jt1078/playback/download"); + defaultExcludes.add("/api/jt1078/snap"); + + if (userSetting.getInterfaceAuthentication() && !userSetting.getInterfaceAuthenticationExcludes().isEmpty()) { + defaultExcludes.addAll(userSetting.getInterfaceAuthenticationExcludes()); + } + + http + .headers(headers -> headers.contentTypeOptions(contentType -> contentType.disable())) + .cors(cors -> cors.configurationSource(configurationSource())) + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + // 配置拦截规则 + .authorizeHttpRequests(auth -> auth + .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() + .requestMatchers(defaultExcludes.toArray(new String[0])).permitAll() + .anyRequest().authenticated() + ) + // 异常处理器 + .exceptionHandling(exception -> exception.authenticationEntryPoint(anonymousAuthenticationEntryPoint)) + .logout(logout -> logout.logoutUrl("/api/user/logout") + .permitAll() + .logoutSuccessHandler(logoutHandler)); + + return http.build(); + } + + CorsConfigurationSource configurationSource() { + // 配置跨域 + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.setAllowedHeaders(Arrays.asList("*")); + corsConfiguration.setAllowedMethods(Arrays.asList("*")); + corsConfiguration.setMaxAge(3600L); + if (userSetting.getAllowedOrigins() != null && !userSetting.getAllowedOrigins().isEmpty()) { + corsConfiguration.setAllowCredentials(true); + corsConfiguration.setAllowedOrigins(userSetting.getAllowedOrigins()); + } else { + // 在SpringBoot 2.4及以上版本处理跨域时,遇到错误提示:当allowCredentials为true时,allowedOrigins不能包含特殊值"*"。 + // 解决方法是明确指定allowedOrigins或使用allowedOriginPatterns。 + corsConfiguration.setAllowCredentials(true); + corsConfiguration.addAllowedOriginPattern(CorsConfiguration.ALL); // 默认全部允许所有跨域 + } + + corsConfiguration.setExposedHeaders(Arrays.asList(JwtUtils.getHeader())); + + UrlBasedCorsConfigurationSource url = new UrlBasedCorsConfigurationSource(); + url.registerCorsConfiguration("/**", corsConfiguration); + return url; + } + + /** + * 描述: 密码加密算法 BCrypt 推荐使用 + **/ + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/dto/JwtUser.java b/src/main/java/com/genersoft/iot/vmp/conf/security/dto/JwtUser.java new file mode 100644 index 0000000..df29c33 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/dto/JwtUser.java @@ -0,0 +1,72 @@ +package com.genersoft.iot.vmp.conf.security.dto; + +public class JwtUser { + + public enum TokenStatus{ + /** + * 正常的使用状态 + */ + NORMAL, + /** + * 过期而失效 + */ + EXPIRED, + /** + * 即将过期 + */ + EXPIRING_SOON, + /** + * 异常 + */ + EXCEPTION + } + + private int userId; + private String userName; + + private String password; + + private int roleId; + + private TokenStatus status; + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public TokenStatus getStatus() { + return status; + } + + public void setStatus(TokenStatus status) { + this.status = status; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getRoleId() { + return roleId; + } + + public void setRoleId(int roleId) { + this.roleId = roleId; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/dto/LoginUser.java b/src/main/java/com/genersoft/iot/vmp/conf/security/dto/LoginUser.java new file mode 100644 index 0000000..65dc2b7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/dto/LoginUser.java @@ -0,0 +1,114 @@ +package com.genersoft.iot.vmp.conf.security.dto; + +import com.genersoft.iot.vmp.storager.dao.dto.Role; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import lombok.Getter; +import lombok.Setter; +import org.springframework.security.core.CredentialsContainer; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.security.core.userdetails.UserDetails; + +import java.time.LocalDateTime; +import java.util.Collection; + +public class LoginUser implements UserDetails, CredentialsContainer { + + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + /** + * 用户 + */ + private User user; + + @Getter + @Setter + private String accessToken; + + @Setter + @Getter + private String serverId; + + /** + * 登录时间 + */ + private LocalDateTime loginTime; + + public LoginUser(User user, LocalDateTime loginTime) { + this.user = user; + this.loginTime = loginTime; + } + + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getUsername(); + } + + /** + * 账户是否未过期,过期无法验证 + */ + @Override + public boolean isAccountNonExpired() { + return true; + } + + /** + * 指定用户是否解锁,锁定的用户无法进行身份验证 + *

    + * 密码锁定 + *

    + */ + @Override + public boolean isAccountNonLocked() { + return true; + } + + /** + * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证 + */ + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + /** + * 用户是否被启用或禁用。禁用的用户无法进行身份验证。 + */ + @Override + public boolean isEnabled() { + return true; + } + + /** + * 认证完成后,擦除密码 + */ + @Override + public void eraseCredentials() { + user.setPassword(null); + } + + + public int getId() { + return user.getId(); + } + + public Role getRole() { + return user.getRole(); + } + + public String getPushKey() { + return user.getPushKey(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/webLog/LogChannel.java b/src/main/java/com/genersoft/iot/vmp/conf/webLog/LogChannel.java new file mode 100644 index 0000000..bd94fbc --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/webLog/LogChannel.java @@ -0,0 +1,65 @@ +package com.genersoft.iot.vmp.conf.webLog; + +import lombok.extern.slf4j.Slf4j; + +import jakarta.websocket.*; +import jakarta.websocket.server.ServerEndpoint; +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@ServerEndpoint(value = "/channel/log") +@Slf4j +public class LogChannel { + + public static final ConcurrentMap CHANNELS = new ConcurrentHashMap<>(); + + private Session session; + + @OnMessage(maxMessageSize = 1) // MaxMessage 1 byte + public void onMessage(String message) { + + try { + this.session.close(new CloseReason(CloseReason.CloseCodes.TOO_BIG, "此节点不接收任何客户端信息")); + } catch (IOException e) { + log.error("[Web-Log] 连接关闭失败: id={}, err={}", this.session.getId(), e.getMessage()); + } + } + + @OnOpen + public void onOpen(Session session, EndpointConfig endpointConfig) { + this.session = session; + this.session.setMaxIdleTimeout(0); + CHANNELS.put(this.session.getId(), this); + + log.info("[Web-Log] 连接已建立: id={}", this.session.getId()); + } + + @OnClose + public void onClose(CloseReason closeReason) { + + log.info("[Web-Log] 连接已断开: id={}, err={}", this.session.getId(), closeReason); + CHANNELS.remove(this.session.getId()); + } + + @OnError + public void onError(Throwable throwable) throws IOException { + log.info("[Web-Log] 连接错误: id={}, err= {}", this.session.getId(), throwable.getMessage()); + if (this.session.isOpen()) { + this.session.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, throwable.getMessage())); + } + } + + /** + * Push messages to all clients + * + * @param message + */ + public static void push(String message) { + CHANNELS.values().stream().forEach(endpoint -> { + if (endpoint.session.isOpen()) { + endpoint.session.getAsyncRemote().sendText(message); + } + }); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/webLog/WebSocketAppender.java b/src/main/java/com/genersoft/iot/vmp/conf/webLog/WebSocketAppender.java new file mode 100644 index 0000000..000ab86 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/webLog/WebSocketAppender.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.conf.webLog; + +import ch.qos.logback.classic.encoder.PatternLayoutEncoder; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.nio.charset.StandardCharsets; + +@Data +@EqualsAndHashCode(callSuper = true) +public class WebSocketAppender extends AppenderBase { + + private PatternLayoutEncoder encoder; + + @Override + protected void append(ILoggingEvent loggingEvent) { + byte[] data = this.encoder.encode(loggingEvent); + // Push to client. +// LogChannel.push(DateUtil.timestampMsTo_yyyy_MM_dd_HH_mm_ss(loggingEvent.getTimeStamp()) + " " + loggingEvent.getFormattedMessage()); + LogChannel.push(new String(data, StandardCharsets.UTF_8)); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/websocket/WebSocketConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/websocket/WebSocketConfig.java new file mode 100644 index 0000000..0ef5267 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/websocket/WebSocketConfig.java @@ -0,0 +1,19 @@ +package com.genersoft.iot.vmp.conf.websocket; + +import com.genersoft.iot.vmp.conf.webLog.LogChannel; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +@Configuration +public class WebSocketConfig { + + @Bean + public ServerEndpointExporter serverEndpointExporter(){ + ServerEndpointExporter endpointExporter = new ServerEndpointExporter(); + + endpointExporter.setAnnotatedEndpointClasses(LogChannel.class); + + return endpointExporter; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java b/src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java new file mode 100644 index 0000000..f633c2b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java @@ -0,0 +1,185 @@ +package com.genersoft.iot.vmp.gb28181; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.GbStringMsgParserFactory; +import com.genersoft.iot.vmp.gb28181.conf.DefaultProperties; +import com.genersoft.iot.vmp.gb28181.transmit.ISIPProcessorObserver; +import gov.nist.javax.sip.SipProviderImpl; +import gov.nist.javax.sip.SipStackImpl; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.*; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Component +@Order(value=10) +public class SipLayer implements CommandLineRunner { + + @Autowired + private SipConfig sipConfig; + + @Autowired + private ISIPProcessorObserver sipProcessorObserver; + + @Autowired + private UserSetting userSetting; + + private final Map tcpSipProviderMap = new ConcurrentHashMap<>(); + private final Map udpSipProviderMap = new ConcurrentHashMap<>(); + private final List monitorIps = new ArrayList<>(); + + @Override + public void run(String... args) { + if (ObjectUtils.isEmpty(sipConfig.getIp())) { + try { + // 获得本机的所有网络接口 + Enumeration nifs = NetworkInterface.getNetworkInterfaces(); + while (nifs.hasMoreElements()) { + NetworkInterface nif = nifs.nextElement(); + // 获得与该网络接口绑定的 IP 地址,一般只有一个 + Enumeration addresses = nif.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = addresses.nextElement(); + if (addr instanceof Inet4Address) { + if (addr.getHostAddress().equals("127.0.0.1")){ + continue; + } + if (nif.getName().startsWith("docker")) { + continue; + } + log.info("[自动配置SIP监听网卡] 网卡接口地址: {}", addr.getHostAddress());// 只关心 IPv4 地址 + monitorIps.add(addr.getHostAddress()); + } + } + } + }catch (Exception e) { + log.error("[读取网卡信息失败]", e); + } + if (monitorIps.isEmpty()) { + log.error("[自动配置SIP监听网卡信息失败], 请手动配置SIP.IP后重新启动"); + System.exit(1); + } + }else { + // 使用逗号分割多个ip + String separator = ","; + if (sipConfig.getIp().indexOf(separator) > 0) { + String[] split = sipConfig.getIp().split(separator); + monitorIps.addAll(Arrays.asList(split)); + }else { + monitorIps.add(sipConfig.getIp()); + } + } + sipConfig.setMonitorIps(monitorIps); + if (ObjectUtils.isEmpty(sipConfig.getShowIp())){ + sipConfig.setShowIp(String.join(",", monitorIps)); + } + SipFactory.getInstance().setPathName("gov.nist"); + if (!monitorIps.isEmpty()) { + for (String monitorIp : monitorIps) { + addListeningPoint(monitorIp, sipConfig.getPort()); + } + if (udpSipProviderMap.size() + tcpSipProviderMap.size() == 0) { + System.exit(1); + } + } + } + + private void addListeningPoint(String monitorIp, int port){ + SipStackImpl sipStack; + try { + sipStack = (SipStackImpl)SipFactory.getInstance().createSipStack(DefaultProperties.getProperties("GB28181_SIP", userSetting.getSipLog(), userSetting.isSipCacheServerConnections())); + sipStack.setMessageParserFactory(new GbStringMsgParserFactory()); + } catch (PeerUnavailableException e) { + log.error("[SIP SERVER] SIP服务启动失败, 监听地址{}失败,请检查ip是否正确", monitorIp); + return; + } + + try { + ListeningPoint tcpListeningPoint = sipStack.createListeningPoint(monitorIp, port, "TCP"); + SipProviderImpl tcpSipProvider = (SipProviderImpl)sipStack.createSipProvider(tcpListeningPoint); + + tcpSipProvider.setDialogErrorsAutomaticallyHandled(); + tcpSipProvider.addSipListener(sipProcessorObserver); + tcpSipProviderMap.put(monitorIp, tcpSipProvider); + log.info("[SIP SERVER] tcp://{}:{} 启动成功", monitorIp, port); + } catch (TransportNotSupportedException + | TooManyListenersException + | ObjectInUseException + | InvalidArgumentException e) { + log.error("[SIP SERVER] tcp://{}:{} SIP服务启动失败,请检查端口是否被占用或者ip是否正确" + , monitorIp, port); + } + + try { + ListeningPoint udpListeningPoint = sipStack.createListeningPoint(monitorIp, port, "UDP"); + + SipProviderImpl udpSipProvider = (SipProviderImpl)sipStack.createSipProvider(udpListeningPoint); + udpSipProvider.addSipListener(sipProcessorObserver); + udpSipProvider.setDialogErrorsAutomaticallyHandled(); + udpSipProviderMap.put(monitorIp, udpSipProvider); + + log.info("[SIP SERVER] udp://{}:{} 启动成功", monitorIp, port); + } catch (TransportNotSupportedException + | TooManyListenersException + | ObjectInUseException + | InvalidArgumentException e) { + log.error("[SIP SERVER] udp://{}:{} SIP服务启动失败,请检查端口是否被占用或者ip是否正确" + , monitorIp, port); + } + } + + public SipProviderImpl getUdpSipProvider(String ip) { + if (udpSipProviderMap.size() == 1) { + return udpSipProviderMap.values().stream().findFirst().get(); + } + if (ObjectUtils.isEmpty(ip)) { + return null; + } + return udpSipProviderMap.get(ip); + } + + public SipProviderImpl getUdpSipProvider() { + if (udpSipProviderMap.size() != 1) { + return null; + } + return udpSipProviderMap.values().stream().findFirst().get(); + } + + public SipProviderImpl getTcpSipProvider() { + if (tcpSipProviderMap.size() != 1) { + return null; + } + return tcpSipProviderMap.values().stream().findFirst().get(); + } + + public SipProviderImpl getTcpSipProvider(String ip) { + if (tcpSipProviderMap.size() == 1) { + return tcpSipProviderMap.values().stream().findFirst().get(); + } + if (ObjectUtils.isEmpty(ip)) { + return null; + } + return tcpSipProviderMap.get(ip); + } + + public String getLocalIp(String deviceLocalIp) { + if (monitorIps.size() == 1) { + return monitorIps.get(0); + } + if (!ObjectUtils.isEmpty(deviceLocalIp)) { + return deviceLocalIp; + } + return getUdpSipProvider().getListeningPoint().getIPAddress(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/auth/DigestServerAuthenticationHelper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/auth/DigestServerAuthenticationHelper.java new file mode 100644 index 0000000..27fb7b8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/auth/DigestServerAuthenticationHelper.java @@ -0,0 +1,238 @@ +/* + * Conditions Of Use + * + * This software was developed by employees of the National Institute of + * Standards and Technology (NIST), an agency of the Federal Government. + * Pursuant to title 15 Untied States Code Section 105, works of NIST + * employees are not subject to copyright protection in the United States + * and are considered to be in the public domain. As a result, a formal + * license is not needed to use the software. + * + * This software is provided by NIST as a service and is expressly + * provided "AS IS." NIST MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED + * OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT + * AND DATA ACCURACY. NIST does not warrant or make any representations + * regarding the use of the software or the results thereof, including but + * not limited to the correctness, accuracy, reliability or usefulness of + * the software. + * + * Permission to use this software is contingent upon your acceptance + * of the terms of this agreement + * + * . + * + */ +package com.genersoft.iot.vmp.gb28181.auth; + +import gov.nist.core.InternalErrorHandler; +import lombok.extern.slf4j.Slf4j; + +import javax.sip.address.URI; +import javax.sip.header.AuthorizationHeader; +import javax.sip.header.HeaderFactory; +import javax.sip.header.WWWAuthenticateHeader; +import javax.sip.message.Request; +import javax.sip.message.Response; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Instant; +import java.util.Random; + +/** + * Implements the HTTP digest authentication method server side functionality. + * + * @author M. Ranganathan + * @author Marc Bednarek + */ +@Slf4j +public class DigestServerAuthenticationHelper { + + private final MessageDigest messageDigest; + + public static final String DEFAULT_ALGORITHM = "MD5"; + public static final String DEFAULT_SCHEME = "Digest"; + + /** to hex converter */ + private static final char[] toHex = { '0', '1', '2', '3', '4', '5', '6', + '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + /** + * Default constructor. + */ + public DigestServerAuthenticationHelper() + throws NoSuchAlgorithmException { + messageDigest = MessageDigest.getInstance(DEFAULT_ALGORITHM); + } + + public static String toHexString(byte[] b) { + int pos = 0; + char[] c = new char[b.length * 2]; + for (byte value : b) { + c[pos++] = toHex[(value >> 4) & 0x0F]; + c[pos++] = toHex[value & 0x0f]; + } + return new String(c); + } + + /** + * Generate the challenge string. + * + * @return a generated nonce. + */ + private String generateNonce() { + long time = Instant.now().toEpochMilli(); + Random rand = new Random(); + long pad = rand.nextLong(); + String nonceString = Long.valueOf(time).toString() + + Long.valueOf(pad).toString(); + byte[] mdBytes = messageDigest.digest(nonceString.getBytes()); + return toHexString(mdBytes); + } + + public Response generateChallenge(HeaderFactory headerFactory, Response response, String realm) { + try { + WWWAuthenticateHeader proxyAuthenticate = headerFactory + .createWWWAuthenticateHeader(DEFAULT_SCHEME); + proxyAuthenticate.setParameter("realm", realm); + proxyAuthenticate.setParameter("qop", "auth"); + proxyAuthenticate.setParameter("nonce", generateNonce()); + proxyAuthenticate.setParameter("algorithm", DEFAULT_ALGORITHM); + + response.setHeader(proxyAuthenticate); + } catch (Exception ex) { + InternalErrorHandler.handleException(ex); + } + return response; + } + /** + * Authenticate the inbound request. + * + * @param request - the request to authenticate. + * @param hashedPassword -- the MD5 hashed string of username:realm:plaintext password. + * + * @return true if authentication succeded and false otherwise. + */ + public boolean doAuthenticateHashedPassword(Request request, String hashedPassword) { + AuthorizationHeader authHeader = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME); + if ( authHeader == null ) { + return false; + } + String realm = authHeader.getRealm(); + String username = authHeader.getUsername(); + + if ( username == null || realm == null ) { + return false; + } + + String nonce = authHeader.getNonce(); + URI uri = authHeader.getURI(); + if (uri == null) { + return false; + } + + String A2 = request.getMethod().toUpperCase() + ":" + uri.toString(); + String HA1 = hashedPassword; + + + byte[] mdbytes = messageDigest.digest(A2.getBytes()); + String HA2 = toHexString(mdbytes); + + String cnonce = authHeader.getCNonce(); + String KD = HA1 + ":" + nonce; + if (cnonce != null) { + KD += ":" + cnonce; + } + KD += ":" + HA2; + mdbytes = messageDigest.digest(KD.getBytes()); + String mdString = toHexString(mdbytes); + String response = authHeader.getResponse(); + + + return mdString.equals(response); + } + + /** + * 鉴权 + * + * A1 = username + ":" + realm + ":" + password + * A2 = REGISTER:URI + * + * HA1 = md5(A1) + * HA2 = md5(A2) + * + * KD = HA2:HA2:qop + * + * response = md5(KD) + * @param request - the request to authenticate. + * @param pass -- the plain text password. + * + * @return true if authentication succeded and false otherwise. + */ + public boolean doAuthenticatePlainTextPassword(Request request, String pass) { + AuthorizationHeader authHeader = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME); + if ( authHeader == null || authHeader.getRealm() == null) { + return false; + } + if ( authHeader.getUsername() == null || authHeader.getRealm() == null ) { + return false; + } + String realm = authHeader.getRealm().trim(); + String username = authHeader.getUsername().trim(); + + String nonce = authHeader.getNonce(); + URI uri = authHeader.getURI(); + if (uri == null) { + return false; + } + // qop 保护质量 包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略 + String qop = authHeader.getQop(); + + // 客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。 + // 这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护 + String cnonce = authHeader.getCNonce(); + + // nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 + int nc = authHeader.getNonceCount(); + String ncStr = String.format("%08x", nc).toUpperCase(); + + String A1 = username + ":" + realm + ":" + pass; + + String A2 = request.getMethod().toUpperCase() + ":" + uri.toString(); + + byte[] mdbytes = messageDigest.digest(A1.getBytes()); + String HA1 = toHexString(mdbytes); + log.debug("A1: {}", A1); + log.debug("A2: {}", A2); + mdbytes = messageDigest.digest(A2.getBytes()); + String HA2 = toHexString(mdbytes); + log.debug("HA1: {}", HA1); + log.debug("HA2: {}", HA2); + // String cnonce = authHeader.getCNonce(); + log.debug("nonce: {}", nonce); + log.debug("nc: {}", ncStr); + log.debug("cnonce: {}", cnonce); + log.debug("qop: {}", qop); + String KD = HA1 + ":" + nonce; + + if (qop != null && qop.equalsIgnoreCase("auth") ) { + if (nc != -1) { + KD += ":" + ncStr; + } + if (cnonce != null) { + KD += ":" + cnonce; + } + KD += ":" + qop; + } + KD += ":" + HA2; + log.debug("KD: {}", KD); + mdbytes = messageDigest.digest(KD.getBytes()); + String mdString = toHexString(mdbytes); + log.debug("mdString: {}", mdString); + String response = authHeader.getResponse(); + log.debug("response: {}", response); + return mdString.equals(response); + + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AlarmChannelMessage.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AlarmChannelMessage.java new file mode 100644 index 0000000..4b3bb13 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AlarmChannelMessage.java @@ -0,0 +1,30 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Data; + +/** + * 通过redis分发报警消息 + */ +@Data +public class AlarmChannelMessage { + /** + * 通道国标编号 + */ + private String gbId; + + /** + * 报警编号 + */ + private int alarmSn; + + /** + * 告警类型 + */ + private int alarmType; + + /** + * 报警描述 + */ + private String alarmDescription; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AudioBroadcastCatch.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AudioBroadcastCatch.java new file mode 100644 index 0000000..e1c9c9c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AudioBroadcastCatch.java @@ -0,0 +1,89 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import com.genersoft.iot.vmp.gb28181.controller.bean.AudioBroadcastEvent; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.Data; + +/** + * 缓存语音广播的状态 + * @author lin + */ +@Data +public class AudioBroadcastCatch { + + + public AudioBroadcastCatch( + String deviceId, + Integer channelId, + MediaServer mediaServerItem, + String app, + String stream, + AudioBroadcastEvent event, + AudioBroadcastCatchStatus status, + boolean isFromPlatform + ) { + this.deviceId = deviceId; + this.channelId = channelId; + this.status = status; + this.event = event; + this.isFromPlatform = isFromPlatform; + this.app = app; + this.stream = stream; + this.mediaServerItem = mediaServerItem; + } + + public AudioBroadcastCatch() { + } + + /** + * 设备编号 + */ + private String deviceId; + + /** + * 通道编号 + */ + private Integer channelId; + + /** + * 流媒体信息 + */ + private MediaServer mediaServerItem; + + /** + * 关联的流APP + */ + private String app; + + /** + * 关联的流STREAM + */ + private String stream; + + /** + * 是否是级联语音喊话 + */ + private boolean isFromPlatform; + + /** + * 语音广播状态 + */ + private AudioBroadcastCatchStatus status; + + /** + * 请求信息 + */ + private SipTransactionInfo sipTransactionInfo; + + /** + * 请求结果回调 + */ + private AudioBroadcastEvent event; + + + public void setSipTransactionInfoByRequest(SIPResponse sipResponse) { + this.sipTransactionInfo = new SipTransactionInfo(sipResponse); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AudioBroadcastCatchStatus.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AudioBroadcastCatchStatus.java new file mode 100644 index 0000000..7d4f7c8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AudioBroadcastCatchStatus.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +/** + * 语音广播状态 + * @author lin + */ +public enum AudioBroadcastCatchStatus { + + // 发送语音广播消息等待对方回复语音广播 + Ready, + // 收到回复等待invite消息 + WaiteInvite, + // 收到invite消息 + Ok, +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/BaiduPoint.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/BaiduPoint.java new file mode 100644 index 0000000..8f33f30 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/BaiduPoint.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public class BaiduPoint { + + String bdLng; + + String bdLat; + + public String getBdLng() { + return bdLng; + } + + public void setBdLng(String bdLng) { + this.bdLng = bdLng; + } + + public String getBdLat() { + return bdLat; + } + + public void setBdLat(String bdLat) { + this.bdLat = bdLat; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/BasicParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/BasicParam.java new file mode 100644 index 0000000..1ec7a72 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/BasicParam.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 基础配置 + */ +@Data +@Schema(description = "基础配置") +public class BasicParam { + + @Schema(description = "设备ID") + private String deviceId; + + @Schema(description = "通道ID,如果时对设备配置直接设置同设备ID一样即可") + private String channelId; + + @Schema(description = "名称") + private String name; + + @Schema(description = "注册过期时间") + private String expiration; + + @Schema(description = "心跳间隔时间") + private Integer heartBeatInterval; + + @Schema(description = "心跳超时次数") + private Integer heartBeatCount; + + @Schema(description = "定位功能支持情况。取值:0-不支持;1-支持 GPS定位;2-支持北斗定位(可选,默认取值为0)," + + "用于接受配置查询结果, 基础配置时无效") + private Integer positionCapability; + + @Schema(description = "经度(可选),用于接受配置查询结果, 基础配置时无效") + private Double longitude; + + @Schema(description = "纬度(可选),用于接受配置查询结果, 基础配置时无效") + private Double latitude; + + public static BasicParam getInstance(String name, String expiration, Integer heartBeatInterval, Integer heartBeatCount) { + BasicParam basicParam = new BasicParam(); + basicParam.setName(name); + basicParam.setExpiration(expiration); + basicParam.setHeartBeatInterval(heartBeatInterval); + basicParam.setHeartBeatCount(heartBeatCount); + return basicParam; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogChannelEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogChannelEvent.java new file mode 100644 index 0000000..cf7c9cb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogChannelEvent.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; + +import java.lang.reflect.InvocationTargetException; + +@Data +@Slf4j +@EqualsAndHashCode(callSuper = true) +public class CatalogChannelEvent extends DeviceChannel{ + + private String event; + + private DeviceChannel channel; + + public static CatalogChannelEvent decode(Element element) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { + Element eventElement = element.element("Event"); + CatalogChannelEvent catalogChannelEvent = new CatalogChannelEvent(); + if (eventElement != null) { + catalogChannelEvent.setEvent(eventElement.getText()); + }else { + catalogChannelEvent.setEvent(CatalogEvent.ADD); + } + DeviceChannel deviceChannel; + if (CatalogEvent.ADD.equalsIgnoreCase(catalogChannelEvent.getEvent()) || + CatalogEvent.UPDATE.equalsIgnoreCase(catalogChannelEvent.getEvent()) ){ + deviceChannel = DeviceChannel.decode(element); + }else { + deviceChannel = DeviceChannel.decodeWithOnlyDeviceId(element); + } + catalogChannelEvent.setChannel(deviceChannel); + return catalogChannelEvent; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogData.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogData.java new file mode 100644 index 0000000..fb687b3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogData.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +import java.time.Instant; +import java.util.HashSet; +import java.util.Set; + +/** + * @author lin + */ +@Data +public class CatalogData { + /** + * 命令序列号 + */ + private int sn; + private Integer total; + private Instant time; + private Device device; + private String errorMsg; + private Set redisKeysForChannel = new HashSet<>(); + private Set errorChannel = new HashSet<>(); + private Set redisKeysForRegion = new HashSet<>(); + private Set redisKeysForGroup = new HashSet<>(); + + public enum CatalogDataStatus{ + ready, runIng, end + } + private CatalogDataStatus status; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CmdType.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CmdType.java new file mode 100644 index 0000000..ed5cad6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CmdType.java @@ -0,0 +1,8 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public class CmdType { + + public static final String CATALOG = "Catalog"; + public static final String ALARM = "Alarm"; + public static final String MOBILE_POSITION = "MobilePosition"; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CommonGBChannel.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CommonGBChannel.java new file mode 100644 index 0000000..a9d1936 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CommonGBChannel.java @@ -0,0 +1,399 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "国标通道") +public class CommonGBChannel { + + @Schema(description = "国标-数据库自增ID") + private int gbId; + + @Schema(description = "国标-编码") + private String gbDeviceId; + + @Schema(description = "国标-名称") + private String gbName; + + @Schema(description = "国标-设备厂商") + private String gbManufacturer; + + @Schema(description = "国标-设备型号") + private String gbModel; + + // 2016 + @Schema(description = "国标-设备归属") + private String gbOwner; + + @Schema(description = "国标-行政区域") + private String gbCivilCode; + + @Schema(description = "国标-警区") + private String gbBlock; + + @Schema(description = "国标-安装地址") + private String gbAddress; + + @Schema(description = "国标-是否有子设备") + private Integer gbParental; + + @Schema(description = "国标-父节点ID") + private String gbParentId; + + // 2016 + @Schema(description = "国标-信令安全模式") + private Integer gbSafetyWay; + + @Schema(description = "国标-注册方式") + private Integer gbRegisterWay; + + // 2016 + @Schema(description = "国标-证书序列号") + private String gbCertNum; + + // 2016 + @Schema(description = "国标-证书有效标识") + private Integer gbCertifiable; + + // 2016 + @Schema(description = "国标-无效原因码(有证书且证书无效的设备必选)") + private Integer gbErrCode; + + // 2016 + @Schema(description = "国标-证书终止有效期(有证书且证书无效的设备必选)") + private String gbEndTime; + + @Schema(description = "国标-保密属性(必选)缺省为0;0-不涉密,1-涉密") + private Integer gbSecrecy; + + @Schema(description = "国标-设备/系统IPv4/IPv6地址") + private String gbIpAddress; + + @Schema(description = "国标-设备/系统端口") + private Integer gbPort; + + @Schema(description = "国标-设备口令") + private String gbPassword; + + @Schema(description = "国标-设备状态") + private String gbStatus; + + @Schema(description = "国标-经度 WGS-84坐标系") + private Double gbLongitude; + + @Schema(description = "国标-纬度 WGS-84坐标系") + private Double gbLatitude; + + private Double gpsAltitude; + + private Double gpsSpeed; + + private Double gpsDirection; + + private String gpsTime; + + @Schema(description = "国标-虚拟组织所属的业务分组ID") + private String gbBusinessGroupId; + + @Schema(description = "国标-摄像机结构类型,标识摄像机类型: 1-球机; 2-半球; 3-固定枪机; 4-遥控枪机;5-遥控半球;6-多目设备的全景/拼接通道;" + + "7-多目设备的分割通道; 99-移动设备(非标)98-会议设备(非标)") + private Integer gbPtzType; + + // 2016 + @Schema(description = "-摄像机位置类型扩展。1-省际检查站、2-党政机关、3-车站码头、4-中心广场、5-体育场馆、6-商业中心、7-宗教场所、" + + "8-校园周边、9-治安复杂区域、10-交通干线。当目录项为摄像机时可选。") + private Integer gbPositionType; + + @Schema(description = "国标-摄像机安装位置室外、室内属性。1-室外、2-室内。") + private Integer gbRoomType; + + // 2016 + @Schema(description = "国标-用途属性") + private Integer gbUseType; + + @Schema(description = "国标-摄像机补光属性。1-无补光;2-红外补光;3-白光补光;4-激光补光;9-其他") + private Integer gbSupplyLightType; + + @Schema(description = "国标-摄像机监视方位(光轴方向)属性。1-东(西向东)、2-西(东向西)、3-南(北向南)、4-北(南向北)、" + + "5-东南(西北到东南)、6-东北(西南到东北)、7-西南(东北到西南)、8-西北(东南到西北)") + private Integer gbDirectionType; + + @Schema(description = "国标-摄像机支持的分辨率,可多值") + private String gbResolution; + + @Schema(description = "国标-下载倍速(可选),可多值") + private String gbDownloadSpeed; + + @Schema(description = "国标-空域编码能力,取值0-不支持;1-1级增强(1个增强层);2-2级增强(2个增强层);3-3级增强(3个增强层)") + private Integer gbSvcSpaceSupportMod; + + @Schema(description = "国标-时域编码能力,取值0-不支持;1-1级增强;2-2级增强;3-3级增强(可选)") + private Integer gbSvcTimeSupportMode; + + @Schema(description = "二进制保存的录制计划, 每一位表示每个小时的前半个小时") + private Long recordPLan; + + @Schema(description = "关联的数据类型") + private Integer dataType; + + @Schema(description = "关联的设备ID") + private Integer dataDeviceId; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "更新时间") + private String updateTime; + + @Schema(description = "流唯一编号,存在表示正在直播") + private String streamId; + + @Schema(description = "是否支持对讲 1支持,0不支持") + private Integer enableBroadcast; + + @Schema(description = "抽稀后的图层层级") + private Integer mapLevel; + + public String encode(String serverDeviceId) { + return encode(null, serverDeviceId); + } + + public String encode(String event,String serverDeviceId) { + String content; + if (event == null) { + return getFullContent(null, serverDeviceId); + } + switch (event) { + case CatalogEvent.DEL: + case CatalogEvent.DEFECT: + case CatalogEvent.VLOST: + content = "\n" + + "" + this.getGbDeviceId() + "\n" + + "" + event + "\n" + + "\n"; + break; + case CatalogEvent.ON: + case CatalogEvent.OFF: + content = "\n" + + "" + this.getGbDeviceId() + "\n" + + "" + event + "\r\n" + + "\n"; + break; + case CatalogEvent.ADD: + case CatalogEvent.UPDATE: + content = getFullContent(event, serverDeviceId); + break; + default: + content = null; + break; + } + return content; + } + + private String getFullContent(String event, String serverDeviceId) { + StringBuilder content = new StringBuilder(); + // 行政区划目录项 + content.append("\n") + .append("" + this.getGbDeviceId() + "\n") + .append("" + this.getGbName() + "\n"); + + + if (this.getGbDeviceId().length() > 8) { + + String type = this.getGbDeviceId().substring(10, 13); + if (type.equals("200")) { + // 业务分组目录项 + if (this.getGbManufacturer() != null) { + content.append("" + this.getGbManufacturer() + "\n"); + } + if (this.getGbModel() != null) { + content.append("" + this.getGbModel() + "\n"); + } + if (this.getGbOwner() != null) { + content.append("" + this.getGbOwner() + "\n"); + } + if (this.getGbCivilCode() != null) { + content.append("" + this.getGbCivilCode() + "\n"); + } + if (this.getGbAddress() != null) { + content.append("
    " + this.getGbAddress() + "
    \n"); + } + if (this.getGbRegisterWay() != null) { + content.append("" + this.getGbRegisterWay() + "\n"); + } + if (this.getGbSecrecy() != null) { + content.append("" + this.getGbSecrecy() + "\n"); + } + } else if (type.equals("215")) { + // 业务分组 + if (this.getGbCivilCode() != null) { + content.append("" + this.getGbCivilCode() + "\n"); + } + content.append("" + serverDeviceId + "\n"); + } else if (type.equals("216")) { + // 虚拟组织目录项 + if (this.getGbCivilCode() != null) { + content.append("" + this.getGbCivilCode() + "\n"); + } + if (this.getGbParentId() != null) { + content.append("" + this.getGbParentId() + "\n"); + } + content.append("" + this.getGbBusinessGroupId() + "\n"); + } else { + if (this.getGbManufacturer() != null) { + content.append("" + this.getGbManufacturer() + "\n"); + } + if (this.getGbModel() != null) { + content.append("" + this.getGbModel() + "\n"); + } + if (this.getGbOwner() != null) { + content.append("" + this.getGbOwner() + "\n"); + } + if (this.getGbCivilCode() != null) { + content.append("" + this.getGbCivilCode() + "\n"); + } + if (this.getGbAddress() != null) { + content.append("
    " + this.getGbAddress() + "
    \n"); + } + if (this.getGbRegisterWay() != null) { + content.append("" + this.getGbRegisterWay() + "\n"); + } + if (this.getGbSecrecy() != null) { + content.append("" + this.getGbSecrecy() + "\n"); + } + if (this.getGbParentId() != null) { + content.append("" + this.getGbParentId() + "\n"); + } + if (this.getGbParental() != null) { + content.append("" + this.getGbParental() + "\n"); + } + if (this.getGbSafetyWay() != null) { + content.append("" + this.getGbSafetyWay() + "\n"); + } + if (this.getGbRegisterWay() != null) { + content.append("" + this.getGbRegisterWay() + "\n"); + } + if (this.getGbCertNum() != null) { + content.append("" + this.getGbCertNum() + "\n"); + } + if (this.getGbCertifiable() != null) { + content.append("" + this.getGbCertifiable() + "\n"); + } + if (this.getGbErrCode() != null) { + content.append("" + this.getGbErrCode() + "\n"); + } + if (this.getGbEndTime() != null) { + content.append("" + this.getGbEndTime() + "\n"); + } + if (this.getGbSecrecy() != null) { + content.append("" + this.getGbSecrecy() + "\n"); + } + if (this.getGbIpAddress() != null) { + content.append("" + this.getGbIpAddress() + "\n"); + } + if (this.getGbPort() != null) { + content.append("" + this.getGbPort() + "\n"); + } + if (this.getGbPassword() != null) { + content.append("" + this.getGbPassword() + "\n"); + } + if (this.getGbStatus() != null) { + content.append("" + this.getGbStatus() + "\n"); + } + if (this.getGbLongitude() != null) { + content.append("" + this.getGbLongitude() + "\n"); + } + if (this.getGbLatitude() != null) { + content.append("" + this.getGbLatitude() + "\n"); + } + content.append("\n"); + + if (this.getGbPtzType() != null) { + content.append(" " + this.getGbPtzType() + "\n"); + } + if (this.getGbPositionType() != null) { + content.append(" " + this.getGbPositionType() + "\n"); + } + if (this.getGbRoomType() != null) { + content.append(" " + this.getGbRoomType() + "\n"); + } + if (this.getGbUseType() != null) { + content.append(" " + this.getGbUseType() + "\n"); + } + if (this.getGbSupplyLightType() != null) { + content.append(" " + this.getGbSupplyLightType() + "\n"); + } + if (this.getGbDirectionType() != null) { + content.append(" " + this.getGbDirectionType() + "\n"); + } + if (this.getGbResolution() != null) { + content.append(" " + this.getGbResolution() + "\n"); + } + if (this.getGbBusinessGroupId() != null) { + content.append(" " + this.getGbBusinessGroupId() + "\n"); + } + if (this.getGbDownloadSpeed() != null) { + content.append(" " + this.getGbDownloadSpeed() + "\n"); + } + if (this.getGbSvcSpaceSupportMod() != null) { + content.append(" " + this.getGbSvcSpaceSupportMod() + "\n"); + } + if (this.getGbSvcTimeSupportMode() != null) { + content.append(" " + this.getGbSvcTimeSupportMode() + "\n"); + } + if (this.getEnableBroadcast() != null) { + content.append(" " + this.getEnableBroadcast() + "\n"); + } + content.append("\n"); + } + } + if (event != null) { + content.append("" + event + "\n"); + } + content.append("
    \n"); + return content.toString(); + } + + public static CommonGBChannel build(Group group) { + GbCode gbCode = GbCode.decode(group.getDeviceId()); + CommonGBChannel channel = new CommonGBChannel(); + if (gbCode.getTypeCode().equals("215")) { + // 业务分组 + channel.setGbName(group.getName()); + channel.setGbDeviceId(group.getDeviceId()); + channel.setGbCivilCode(group.getCivilCode()); + } else { + // 虚拟组织 + channel.setGbName(group.getName()); + channel.setGbDeviceId(group.getDeviceId()); + channel.setGbParentId(group.getParentDeviceId()); + channel.setGbBusinessGroupId(group.getBusinessGroup()); + channel.setGbCivilCode(group.getCivilCode()); + } + return channel; + } + + public static CommonGBChannel build(Platform platform) { + CommonGBChannel commonGBChannel = new CommonGBChannel(); + commonGBChannel.setGbDeviceId(platform.getDeviceGBId()); + commonGBChannel.setGbName(platform.getName()); + commonGBChannel.setGbManufacturer(platform.getManufacturer()); + commonGBChannel.setGbModel(platform.getModel()); + commonGBChannel.setGbCivilCode(platform.getCivilCode()); + commonGBChannel.setGbAddress(platform.getAddress()); + commonGBChannel.setGbRegisterWay(platform.getRegisterWay()); + commonGBChannel.setGbSecrecy(platform.getSecrecy()); + commonGBChannel.setGbStatus(platform.isStatus() ? "ON" : "OFF"); + return commonGBChannel; + } + + public static CommonGBChannel build(Region region) { + CommonGBChannel commonGBChannel = new CommonGBChannel(); + commonGBChannel.setGbDeviceId(region.getDeviceId()); + commonGBChannel.setGbName(region.getName()); + return commonGBChannel; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CommonRecordInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CommonRecordInfo.java new file mode 100644 index 0000000..90d8306 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CommonRecordInfo.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +@Data +public class CommonRecordInfo { + + // 开始时间 + private String startTime; + + // 结束时间 + private String endTime; + + // 文件大小 单位byte + private String fileSize; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java new file mode 100644 index 0000000..83a302e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java @@ -0,0 +1,219 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 国标设备/平台 + * @author lin + */ +@Data +@Schema(description = "国标设备/平台") +public class Device { + + @Schema(description = "数据库自增ID") + private int id; + + /** + * 设备国标编号 + */ + @Schema(description = "设备国标编号") + private String deviceId; + + /** + * 设备名 + */ + @Schema(description = "名称") + private String name; + + /** + * 生产厂商 + */ + @Schema(description = "生产厂商") + private String manufacturer; + + /** + * 型号 + */ + @Schema(description = "型号") + private String model; + + /** + * 固件版本 + */ + @Schema(description = "固件版本") + private String firmware; + + /** + * 传输协议 + * UDP/TCP + */ + @Schema(description = "传输协议(UDP/TCP)") + private String transport; + + /** + * 数据流传输模式 + * UDP:udp传输 + * TCP-ACTIVE:tcp主动模式 + * TCP-PASSIVE:tcp被动模式 + */ + @Schema(description = "数据流传输模式") + private String streamMode; + + /** + * wan地址_ip + */ + @Schema(description = "IP") + private String ip; + + /** + * wan地址_port + */ + @Schema(description = "端口") + private int port; + + /** + * wan地址 + */ + @Schema(description = "wan地址") + private String hostAddress; + + /** + * 在线 + */ + @Schema(description = "是否在线,true为在线,false为离线") + private boolean onLine; + + + /** + * 注册时间 + */ + @Schema(description = "注册时间") + private String registerTime; + + + /** + * 心跳时间 + */ + @Schema(description = "心跳时间") + private String keepaliveTime; + + + /** + * 心跳间隔 + */ + @Schema(description = "心跳间隔") + private Integer heartBeatInterval; + + + /** + * 心跳超时次数 + */ + @Schema(description = "心跳超时次数") + private Integer heartBeatCount; + + + /** + * 定位功能支持情况 + */ + @Schema(description = "定位功能支持情况。取值:0-不支持;1-支持 GPS定位;2-支持北斗定位(可选,默认取值为0") + private Integer positionCapability; + + /** + * 通道个数 + */ + @Schema(description = "通道个数") + private int channelCount; + + /** + * 注册有效期 + */ + @Schema(description = "注册有效期") + private int expires; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + private String createTime; + + /** + * 更新时间 + */ + @Schema(description = "更新时间") + private String updateTime; + + /** + * 设备使用的媒体id, 默认为null + */ + @Schema(description = "设备使用的媒体id, 默认为null") + private String mediaServerId; + + /** + * 字符集, 支持 UTF-8 与 GB2312 + */ + @Schema(description = "符集, 支持 UTF-8 与 GB2312") + private String charset ; + + /** + * 目录订阅周期,0为不订阅 + */ + @Schema(description = "目录订阅周期,o为不订阅") + private int subscribeCycleForCatalog; + + /** + * 移动设备位置订阅周期,0为不订阅 + */ + @Schema(description = "移动设备位置订阅周期,0为不订阅") + private int subscribeCycleForMobilePosition; + + /** + * 移动设备位置信息上报时间间隔,单位:秒,默认值5 + */ + @Schema(description = "移动设备位置信息上报时间间隔,单位:秒,默认值5") + private int mobilePositionSubmissionInterval = 5; + + /** + * 报警订阅周期,0为不订阅 + */ + @Schema(description = "报警心跳时间订阅周期,0为不订阅") + private int subscribeCycleForAlarm; + + /** + * 是否开启ssrc校验,默认关闭,开启可以防止串流 + */ + @Schema(description = "是否开启ssrc校验,默认关闭,开启可以防止串流") + private boolean ssrcCheck = false; + + /** + * 地理坐标系, 目前支持 WGS84,GCJ02, 此字段保留,暂无用 + */ + @Schema(description = "地理坐标系, 目前支持 WGS84,GCJ02") + private String geoCoordSys; + + @Schema(description = "密码") + private String password; + + @Schema(description = "收流IP") + private String sdpIp; + + @Schema(description = "SIP交互IP(设备访问平台的IP)") + private String localIp; + + @Schema(description = "是否作为消息通道") + private boolean asMessageChannel; + + @Schema(description = "设备注册的事务信息") + private SipTransactionInfo sipTransactionInfo; + + @Schema(description = "控制语音对讲流程,释放收到ACK后发流") + private boolean broadcastPushAfterAck; + + @Schema(description = "所属服务Id") + private String serverId; + + public boolean checkWgs84() { + return geoCoordSys.equalsIgnoreCase("WGS84"); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarm.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarm.java new file mode 100644 index 0000000..22d3dd2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarm.java @@ -0,0 +1,269 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author lin + */ +@Schema(description = "报警信息") +@Data +public class DeviceAlarm { + + @Schema(description = "数据库id") + private String id; + + @Schema(description = "设备的国标编号") + private String deviceId; + + @Schema(description = "设备名称") + private String deviceName; + + /** + * 通道Id + */ + @Schema(description = "通道的国标编号") + private String channelId; + + /** + * 报警级别, 1为一级警情, 2为二级警情, 3为三级警情, 4为四级警情 + */ + @Schema(description = "报警级别, 1为一级警情, 2为二级警情, 3为三级警情, 4为四级警情") + private String alarmPriority; + + @Schema(description = "报警级别, 1为一级警情, 2为二级警情, 3为三级警情, 4为四级警情") + private String alarmPriorityDescription; + + public String getAlarmPriorityDescription() { + switch (alarmPriority) { + case "1": + return "一级警情"; + case "2": + return "二级警情"; + case "3": + return "三级警情"; + case "4": + return "四级警情"; + default: + return alarmPriority; + } + } + + /** + * 报警方式 , 1为电话报警, 2为设备报警, 3为短信报警, 4为 GPS报警, 5为视频报警, 6为设备故障报警, + * 7其他报警;可以为直接组合如12为电话报警或 设备报警- + */ + @Schema(description = "报警方式 , 1为电话报警, 2为设备报警, 3为短信报警, 4为 GPS报警, 5为视频报警, 6为设备故障报警,\n" + + "\t * 7其他报警;可以为直接组合如12为电话报警或设备报警") + private String alarmMethod; + + + private String alarmMethodDescription; + + public String getAlarmMethodDescription() { + StringBuilder stringBuilder = new StringBuilder(); + char[] charArray = alarmMethod.toCharArray(); + for (char c : charArray) { + switch (c) { + case '1': + stringBuilder.append("-电话报警"); + break; + case '2': + stringBuilder.append("-设备报警"); + break; + case '3': + stringBuilder.append("-短信报警"); + break; + case '4': + stringBuilder.append("-GPS报警"); + break; + case '5': + stringBuilder.append("-视频报警"); + break; + case '6': + stringBuilder.append("-设备故障报警"); + break; + case '7': + stringBuilder.append("-其他报警"); + break; + } + } + stringBuilder.delete(0, 1); + return stringBuilder.toString(); + } + + /** + * 报警时间 + */ + @Schema(description = "报警时间") + private String alarmTime; + + /** + * 报警内容描述 + */ + @Schema(description = "报警内容描述") + private String alarmDescription; + + /** + * 经度 + */ + @Schema(description = "经度") + private double longitude; + + /** + * 纬度 + */ + @Schema(description = "纬度") + private double latitude; + + /** + * 报警类型, + * 报警方式为2时,不携带 AlarmType为默认的报警设备报警, + * 携带 AlarmType取值及对应报警类型如下: + * 1-视频丢失报警; + * 2-设备防拆报警; + * 3-存储设备磁盘满报警; + * 4-设备高温报警; + * 5-设备低温报警。 + * 报警方式为5时,取值如下: + * 1-人工视频报警; + * 2-运动目标检测报警; + * 3-遗留物检测报警; + * 4-物体移除检测报警; + * 5-绊线检测报警; + * 6-入侵检测报警; + * 7-逆行检测报警; + * 8-徘徊检测报警; + * 9-流量统计报警; + * 10-密度检测报警; + * 11-视频异常检测报警; + * 12-快速移动报警。 + * 报警方式为6时,取值下: + * 1-存储设备磁盘故障报警; + * 2-存储设备风扇故障报警。 + */ + @Schema(description = "报警类型") + private String alarmType; + + public String getAlarmTypeDescription() { + if (alarmType == null) { + return ""; + } + char[] charArray = alarmMethod.toCharArray(); + Set alarmMethodSet = new HashSet<>(); + for (char c : charArray) { + alarmMethodSet.add(Character.toString(c)); + } + String result = alarmType; + if (alarmMethodSet.contains("2")) { + switch (alarmType) { + case "1": + result = "视频丢失报警"; + break; + case "2": + result = "设备防拆报警"; + break; + case "3": + result = "存储设备磁盘满报警"; + break; + case "4": + result = "设备高温报警"; + break; + case "5": + result = "设备低温报警"; + break; + } + } + if (alarmMethodSet.contains("5")) { + switch (alarmType) { + case "1": + result = "人工视频报警"; + break; + case "2": + result = "运动目标检测报警"; + break; + case "3": + result = "遗留物检测报警"; + break; + case "4": + result = "物体移除检测报警"; + break; + case "5": + result = "绊线检测报警"; + break; + case "6": + result = "入侵检测报警"; + break; + case "7": + result = "逆行检测报警"; + break; + case "8": + result = "徘徊检测报警"; + break; + case "9": + result = "流量统计报警"; + break; + case "10": + result = "密度检测报警"; + break; + case "11": + result = "视频异常检测报警"; + break; + case "12": + result = "快速移动报警"; + break; + } + } + if (alarmMethodSet.contains("6")) { + switch (alarmType) { + case "1": + result = "人工视频报警"; + break; + case "2": + result = "运动目标检测报警"; + break; + case "3": + result = "遗留物检测报警"; + break; + case "4": + result = "物体移除检测报警"; + break; + case "5": + result = "绊线检测报警"; + break; + case "6": + result = "入侵检测报警"; + break; + case "7": + result = "逆行检测报警"; + break; + case "8": + result = "徘徊检测报警"; + break; + case "9": + result = "流量统计报警"; + break; + case "10": + result = "密度检测报警"; + break; + case "11": + result = "视频异常检测报警"; + break; + case "12": + result = "快速移动报警"; + break; + } + } + return result; + } + + @Schema(description = "报警类型描述") + private String alarmTypeDescription; + + @Schema(description = "创建时间") + private String createTime; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarmMethod.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarmMethod.java new file mode 100644 index 0000000..d1fb6db --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarmMethod.java @@ -0,0 +1,54 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +/** + * 报警方式 + * @author lin + * 1为电话报警, 2为设备报警, 3为短信报警, 4为 GPS报警, 5为视频报警, 6为设备故障报警, + * 7其他报警;可以为直接组合如12为电话报警或 设备报警- + */ +public enum DeviceAlarmMethod { + // 1为电话报警 + Telephone(1), + + // 2为设备报警 + Device(2), + + // 3为短信报警 + SMS(3), + + // 4为 GPS报警 + GPS(4), + + // 5为视频报警 + Video(5), + + // 6为设备故障报警 + DeviceFailure(6), + + // 7其他报警 + Other(7); + + private final int val; + + DeviceAlarmMethod(int val) { + this.val=val; + } + + public int getVal() { + return val; + } + + /** + * 查询是否匹配类型 + * @param code + * @return + */ + public static DeviceAlarmMethod typeOf(int code) { + for (DeviceAlarmMethod item : DeviceAlarmMethod.values()) { + if (code==item.getVal()) { + return item; + } + } + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannel.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannel.java new file mode 100644 index 0000000..1ba09b5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannel.java @@ -0,0 +1,265 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.utils.MessageElementForCatalog; +import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.util.ObjectUtils; + +import java.lang.reflect.InvocationTargetException; + +@Data +@Slf4j +@Schema(description = "通道信息") +@EqualsAndHashCode(callSuper = true) +public class DeviceChannel extends CommonGBChannel { + + @Schema(description = "数据库自增ID") + private int id; + + @Schema(description = "父设备编码") + private String parentDeviceId; + + @Schema(description = "父设备名称") + private String parentName; + + @MessageElementForCatalog("DeviceID") + @Schema(description = "编码") + private String deviceId; + + @MessageElementForCatalog("Name") + @Schema(description = "名称") + private String name; + + @MessageElementForCatalog("Manufacturer") + @Schema(description = "设备厂商") + private String manufacturer; + + @MessageElementForCatalog("Model") + @Schema(description = "设备型号") + private String model; + + // 2016 + @MessageElementForCatalog("Owner") + @Schema(description = "设备归属") + private String owner; + + @MessageElementForCatalog("CivilCode") + @Schema(description = "行政区域") + private String civilCode; + + @MessageElementForCatalog("Block") + @Schema(description = "警区") + private String block; + + @MessageElementForCatalog("Address") + @Schema(description = "安装地址") + private String address; + + @MessageElementForCatalog("Parental") + @Schema(description = "是否有子设备(必选)1有,0没有") + private Integer parental; + + + @MessageElementForCatalog("ParentID") + @Schema(description = "父节点ID") + private String parentId; + + // 2016 + @MessageElementForCatalog("SafetyWay") + @Schema(description = "信令安全模式") + private Integer safetyWay; + + @MessageElementForCatalog("RegisterWay") + @Schema(description = "注册方式") + private Integer registerWay; + + // 2016 + @MessageElementForCatalog("CertNum") + @Schema(description = "证书序列号") + private String certNum; + + // 2016 + @MessageElementForCatalog("Certifiable") + @Schema(description = "证书有效标识, 缺省为0;证书有效标识:0:无效 1:有效") + private Integer certifiable; + + // 2016 + @MessageElementForCatalog("ErrCode") + @Schema(description = "无效原因码(有证书且证书无效的设备必选)") + private Integer errCode; + + // 2016 + @MessageElementForCatalog("EndTime") + @Schema(description = "证书终止有效期(有证书且证书无效的设备必选)") + private String endTime; + + @MessageElementForCatalog("Secrecy") + @Schema(description = "保密属性(必选)缺省为0;0-不涉密,1-涉密") + private Integer secrecy; + + @MessageElementForCatalog("IPAddress") + @Schema(description = "设备/系统IPv4/IPv6地址") + private String ipAddress; + + @MessageElementForCatalog("Port") + @Schema(description = "设备/系统端口") + private Integer port; + + @MessageElementForCatalog("Password") + @Schema(description = "设备口令") + private String password; + + @MessageElementForCatalog("Status") + @Schema(description = "设备状态") + private String status; + + @MessageElementForCatalog("Longitude") + @Schema(description = "经度 WGS-84坐标系") + private Double longitude; + + @MessageElementForCatalog("Latitude") + @Schema(description = ",纬度 WGS-84坐标系") + private Double latitude; + + @MessageElementForCatalog("Info.PTZType") + @Schema(description = "摄像机结构类型,标识摄像机类型: 1-球机; 2-半球; 3-固定枪机; 4-遥控枪机;5-遥控半球;6-多目设备的全景/拼接通道;7-多目设备的分割通道") + private Integer ptzType; + + @MessageElementForCatalog("Info.PositionType") + @Schema(description = "摄像机位置类型扩展。1-省际检查站、2-党政机关、3-车站码头、4-中心广场、5-体育场馆、" + + "6-商业中心、7-宗教场所、8-校园周边、9-治安复杂区域、10-交通干线") + private Integer positionType; + + @MessageElementForCatalog("Info.RoomType") + @Schema(description = "摄像机安装位置室外、室内属性。1-室外、2-室内。") + private Integer roomType; + + @MessageElementForCatalog("Info.UseType") + @Schema(description = "用途属性, 1-治安、2-交通、3-重点。") + private Integer useType; + + @MessageElementForCatalog("Info.SupplyLightType") + @Schema(description = "摄像机补光属性。1-无补光;2-红外补光;3-白光补光;4-激光补光;9-其他") + private Integer supplyLightType; + + @MessageElementForCatalog("Info.DirectionType") + @Schema(description = "摄像机监视方位(光轴方向)属性。1-东(西向东)、2-西(东向西)、3-南(北向南)、4-北(南向北)、" + + "5-东南(西北到东南)、6-东北(西南到东北)、7-西南(东北到西南)、8-西北(东南到西北)") + private Integer directionType; + + @MessageElementForCatalog("Info.Resolution") + @Schema(description = "摄像机支持的分辨率,可多值") + private String resolution; + + @MessageElementForCatalog({"BusinessGroupID","Info.BusinessGroupID"}) + @Schema(description = "虚拟组织所属的业务分组ID") + private String businessGroupId; + + @MessageElementForCatalog("Info.DownloadSpeed") + @Schema(description = "下载倍速(可选),可多值") + private String downloadSpeed; + + @MessageElementForCatalog("Info.SVCSpaceSupportMode") + @Schema(description = "空域编码能力,取值0-不支持;1-1级增强(1个增强层);2-2级增强(2个增强层);3-3级增强(3个增强层)") + private Integer svcSpaceSupportMod; + + @MessageElementForCatalog("Info.SVCTimeSupportMode") + @Schema(description = "时域编码能力,取值0-不支持;1-1级增强;2-2级增强;3-3级增强(可选)") + private Integer svcTimeSupportMode; + + @Schema(description = "云台类型描述字符串") + private String ptzTypeText; + + @Schema(description = "子设备数") + private int subCount; + + @Schema(description = "是否含有音频") + private boolean hasAudio; + + @Schema(description = "GPS的更新时间") + private String gpsTime; + + @Schema(description = "码流标识,优先级高于设备中码流标识," + + "用于选择码流时组成码流标识。默认为null,不设置。可选值: stream/streamnumber/streamprofile/streamMode") + private String streamIdentification; + + @Schema(description = "通道类型, 默认0, 0: 普通通道,1 行政区划 2 业务分组/虚拟组织") + private int channelType; + + private Integer dataType = ChannelDataType.GB28181; + + public void setPtzType(int ptzType) { + this.ptzType = ptzType; + switch (ptzType) { + case 0: + this.ptzTypeText = "未知"; + break; + case 1: + this.ptzTypeText = "球机"; + break; + case 2: + this.ptzTypeText = "半球"; + break; + case 3: + this.ptzTypeText = "固定枪机"; + break; + case 4: + this.ptzTypeText = "遥控枪机"; + break; + case 5: + this.ptzTypeText = "遥控半球"; + break; + case 6: + this.ptzTypeText = "多目设备的全景/拼接通道"; + break; + case 7: + this.ptzTypeText = "多目设备的分割通道"; + break; + } + } + + public static DeviceChannel decode(Element element) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { + DeviceChannel deviceChannel = XmlUtil.elementDecode(element, DeviceChannel.class); + if(deviceChannel.getCivilCode() != null ) { + if (ObjectUtils.isEmpty(deviceChannel.getCivilCode()) + || deviceChannel.getCivilCode().length() > 8 ){ + deviceChannel.setCivilCode(null); + } + // 此处对于不在wvp缓存中的行政区划,默认直接存储.保证即使出现wvp的行政区划缓存过老,也可以通过用户自主创建的方式正常使用系统 + } + GbCode gbCode = GbCode.decode(deviceChannel.getDeviceId()); + if (gbCode != null && "138".equals(gbCode.getTypeCode())) { + deviceChannel.setHasAudio(true); + if (deviceChannel.getEnableBroadcast() == null && "138".equals(gbCode.getTypeCode())) { + deviceChannel.setEnableBroadcast(1); + } + } + + return deviceChannel; + } + + public static DeviceChannel decodeWithOnlyDeviceId(Element element) { + Element deviceElement = element.element("DeviceID"); + DeviceChannel deviceChannel = new DeviceChannel(); + deviceChannel.setDeviceId(deviceElement.getText()); + deviceChannel.setDataType(ChannelDataType.GB28181); + return deviceChannel; + } + + public CommonGBChannel buildCommonGBChannelForStatus() { + CommonGBChannel commonGBChannel = new CommonGBChannel(); + commonGBChannel.setGbId(id); + commonGBChannel.setGbDeviceId(deviceId); + commonGBChannel.setGbName(name); + commonGBChannel.setDataType(ChannelDataType.GB28181); + commonGBChannel.setDataDeviceId(getDataDeviceId()); + return commonGBChannel; + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannelInPlatform.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannelInPlatform.java new file mode 100644 index 0000000..c61bb08 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannelInPlatform.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public class DeviceChannelInPlatform extends DeviceChannel{ + + private String platFormId; + private String catalogId; + + public String getPlatFormId() { + return platFormId; + } + + public void setPlatFormId(String platFormId) { + this.platFormId = platFormId; + } + + public String getCatalogId() { + return catalogId; + } + + public void setCatalogId(String catalogId) { + this.catalogId = catalogId; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceNotFoundEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceNotFoundEvent.java new file mode 100644 index 0000000..bb65dbf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceNotFoundEvent.java @@ -0,0 +1,16 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +import javax.sip.Dialog; +import java.util.EventObject; + +@Data +public class DeviceNotFoundEvent { + + private String callId; + + public DeviceNotFoundEvent(String callId) { + this.callId = callId; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceType.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceType.java new file mode 100644 index 0000000..5a82526 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceType.java @@ -0,0 +1,58 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import org.jetbrains.annotations.NotNull; + +public class DeviceType implements Comparable{ + + /** + * 编号 + */ + private String name; + + /** + * 名称 + */ + private String code; + + /** + * 归属名称 + */ + private String ownerName; + public static DeviceType getInstance(DeviceTypeEnum typeEnum) { + DeviceType deviceType = new DeviceType(); + deviceType.setName(typeEnum.getName()); + deviceType.setCode(typeEnum.getCode()); + deviceType.setOwnerName(typeEnum.getOwnerName()); + return deviceType; + } + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getOwnerName() { + return ownerName; + } + + public void setOwnerName(String ownerName) { + this.ownerName = ownerName; + } + + @Override + public int compareTo(@NotNull DeviceType deviceType) { + return Integer.compare(Integer.parseInt(this.code), Integer.parseInt(deviceType.getCode())); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceTypeEnum.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceTypeEnum.java new file mode 100644 index 0000000..8a7c07a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceTypeEnum.java @@ -0,0 +1,98 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +/** + * 收录行业编码 + */ +public enum DeviceTypeEnum { + DVR("111", "DVR编码", "前端主设备"), + VIDEO_SERVER("112", "视频服务器编码", "前端主设备"), + ENCODER("113", "编码器编码", "前端主设备"), + DECODER("114", "解码器编码", "前端主设备"), + VIDEO_SWITCHING_MATRIX("115", "视频切换矩阵编码", "前端主设备"), + AUDIO_SWITCHING_MATRIX("116", "音频切换矩阵编码", "前端主设备"), + ALARM_CONTROLLER("117", "报警控制器编码", "前端主设备"), + NVR("118", "网络视频录像机(NVR)编码", "前端主设备"), + RESERVE("119", "预留", "前端主设备"), + ONLINE_VIDEO_IMAGE_INFORMATION_ACQUISITION_SYSTEM("120", "在线视频图像信息采集系统编码", "前端主设备"), + VIDEO_CHECKPOINT("121", "视频卡口编码", "前端主设备"), + MULTI_CAMERA_DEVICE("122", "多目设备编码", "前端主设备"), + PARKING_LOT_ENTRANCE_AND_EXIT_CONTROL_EQUIPMENT("123", "停车场出入口控制设备编码", "前端主设备"), + PERSONNEL_ACCESS_CONTROL_EQUIPMENT("124", "人员出入口控制设备编码", "前端主设备"), + SECURITY_INSPECTION_EQUIPMENT("125", "安检设备编码", "前端主设备"), + HVR("130", "混合硬盘录像机(HVR)编码", "前端主设备"), + CAMERA("131", "摄像机编码", "前端外围设备"), + IPC("132", "网络摄像机(IPC)/在线视频图像信息采集设备编码", "前端外围设备"), + MONITOR("133", "显示器编码", "前端外围设备"), + ALARM_INPUT_DEVICE("134", "报警输入设备编码(如红外、烟感、门禁等报警设备)", "前端外围设备"), + ALARM_OUTPUT_DEVICE("135", "报警输出设备编码(如警灯、警铃等设备)", "前端外围设备"), + VOICE_INPUT_DEVICE("136", "语音输入设备编码", "前端外围设备"), + VOICE_OUTPUT_DEVICE("137", "语音输出设备", "前端外围设备"), + MOBILE_TRANSMISSION_EQUIPMENT("138", "移动传输设备编码", "前端外围设备"), + OTHER_PERIPHERAL_DEVICES("139", "其他外围设备编码", "前端外围设备"), + ALARM_OUTPUT_DEVICE2("140", "报警输出设备编码(如继电器或触发器控制的设备)", "前端外围设备"), + BARRIER_GATE("141", "道闸(控制车辆通行)", "前端外围设备"), + SMART_DOOR("142", "智能门(控制人员通行)", "前端外围设备"), + VOUCHER_RECOGNITION_UNIT("143", "凭证识别单元", "前端外围设备"), + CENTRAL_SIGNALING_CONTROL_SERVER("200", "中心信令控制服务器编码", "平台设备"), + WEB_APPLICATION_SERVER("201", "Web应用服务器编码", "平台设备"), + PROXY_SERVER("203", "代理服务器编码", "平台设备"), + SECURITY_SERVER("204", "安全服务器编码", "平台设备"), + ALARM_SERVER("205", "报警服务器编码", "平台设备"), + DATABASE_SERVER("206", "数据库服务器编码", "平台设备"), + GIS_SERVER("207", "GIS服务器编码", "平台设备"), + MANAGER_SERVER("208", "管理服务器编码", "平台设备"), + ACCESS_GATEWAY("209", "接入网关编码", "平台设备"), + MEDIA_STORAGE_SERVER("210", "媒体存储服务器编码", "平台设备"), + SIGNALING_SECURITY_ROUTING_GATEWAY("211", "信令安全路由网关编码", "平台设备"), + BUSINESS_GROUP("215", "业务分组编码", "平台设备"), + VIRTUAL_ORGANIZATION("216", "虚拟组织编码", "平台设备"), + CENTRAL_USER("300", "中心用户", "中心用户"), + END_USER("400", "终端用户", "终端用户"), + VIDEO_IMAGE_INFORMATION_SYNTHESIS("500", "视频图像信息综合应用平台", "平台外接服务器"), + VIDEO_IMAGE_INFORMATION_OPERATION_AND_MAINTENANCE_MANAGEMENT("501", "视频图像信息运维管理平台", "平台外接服务器"), + VIDEO_IMAGE_ANALYSIS("502", "视频图像分析系统", "平台外接服务器"), + VIDEO_IMAGE_INFORMATION_DATABASE("503", "视频图像信息数据库", "平台外接服务器"), + VIDEO_IMAGE_ANALYSIS_EQUIPMENT("505", "视频图像分析设备", "平台外接服务器"), + ; + + /** + * 编号 + */ + private final String name; + + /** + * 名称 + */ + private String code; + + /** + * 归属名称 + */ + private String ownerName; + + DeviceTypeEnum(String code, String name, String ownerName) { + this.name = name; + this.code = code; + this.ownerName = ownerName; + } + + public String getName() { + return name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getOwnerName() { + return ownerName; + } + + public void setOwnerName(String ownerName) { + this.ownerName = ownerName; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DragZoomParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DragZoomParam.java new file mode 100644 index 0000000..d9b3a81 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DragZoomParam.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.gb28181.utils.MessageElement; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "拉框放大/缩小控制参数") +public class DragZoomParam { + + @MessageElement("Length") + @Schema(description = "播放窗口长度像素值(必选)") + protected Integer length; + + @MessageElement("Width") + @Schema(description = "播放窗口宽度像素值(必选)") + protected Integer width; + + @MessageElement("MidPointX") + @Schema(description = "拉框中心的横轴坐标像素值(必选)") + protected Integer midPointX; + + @MessageElement("MidPointY") + @Schema(description = "拉框中心的纵轴坐标像素值(必选)") + protected Integer midPointY; + + @MessageElement("LengthX") + @Schema(description = "拉框长度像素值(必选)") + protected Integer lengthX; + + @MessageElement("LengthY") + @Schema(description = "拉框宽度像素值(必选)") + protected Integer lengthY; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DragZoomRequest.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DragZoomRequest.java new file mode 100644 index 0000000..1a58c10 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DragZoomRequest.java @@ -0,0 +1,30 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.gb28181.utils.MessageElement; +import lombok.Data; + +/** + * 设备信息查询响应 + * + * @author Y.G + * @version 1.0 + * @date 2022/6/28 14:55 + */ +@Data +public class DragZoomRequest { + /** + * 序列号 + */ + @MessageElement("SN") + private String sn; + + @MessageElement("DeviceID") + private String deviceId; + + @MessageElement(value = "DragZoomIn") + private DragZoomParam dragZoomIn; + + @MessageElement(value = "DragZoomOut") + private DragZoomParam dragZoomOut; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DrawThinProcess.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DrawThinProcess.java new file mode 100644 index 0000000..9438798 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DrawThinProcess.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class DrawThinProcess { + + private double process; + private String msg; + + public DrawThinProcess(double process, String msg) { + this.process = process; + this.msg = msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndCode.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndCode.java new file mode 100644 index 0000000..c155b53 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndCode.java @@ -0,0 +1,184 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + + +/** + * 解析收到的前端控制指令 + */ +@Data +public class FrontEndCode { + + + public static String encode(IFrontEndControlCode frontEndControlCode){ + return frontEndControlCode.encode(); + } + + public static IFrontEndControlCode decode(@NotNull String cmdStr) { + if (cmdStr.length() != 16) { + return null; + } + String cmdCodeStr = cmdStr.substring(6, 8); + int cmdCode = Integer.parseInt(cmdCodeStr, 16); + if (cmdCode < 39) { + // PTZ指令 + FrontEndControlCodeForPTZ codeForPTZ = new FrontEndControlCodeForPTZ(); + int zoomOut = cmdCode >> 5 & 1; + if (zoomOut == 1) { + codeForPTZ.setZoom(0); + } + int zoomIn = cmdCode >> 4 & 1; + if (zoomIn == 1) { + codeForPTZ.setZoom(1); + } + int tiltUp = cmdCode >> 3 & 1; + if (tiltUp == 1) { + codeForPTZ.setTilt(0); + } + int tiltDown = cmdCode >> 2 & 1; + if (tiltDown == 1) { + codeForPTZ.setTilt(1); + } + int panLeft = cmdCode >> 1 & 1; + if (panLeft == 1) { + codeForPTZ.setPan(0); + } + int panRight = cmdCode & 1; + if (panRight == 1) { + codeForPTZ.setPan(1); + } + String param1Str = cmdStr.substring(8, 10); + codeForPTZ.setPanSpeed(Integer.parseInt(param1Str, 16)); + String param2Str = cmdStr.substring(10, 12); + codeForPTZ.setTiltSpeed(Integer.parseInt(param2Str, 16)); + String param3Str = cmdStr.substring(12, 13); + codeForPTZ.setZoomSpeed(Integer.parseInt(param3Str, 16)); + return codeForPTZ; + }else if (cmdCode < 74) { + // FI指令 + FrontEndControlCodeForFI codeForFI = new FrontEndControlCodeForFI(); + int irisOut = cmdCode >> 3 & 1; + if (irisOut == 1) { + codeForFI.setIris(0); + } + int irisIn = cmdCode >> 2 & 1; + if (irisIn == 1) { + codeForFI.setIris(1); + } + int focusNear = cmdCode >> 1 & 1; + if (focusNear == 1) { + codeForFI.setFocus(0); + } + int focusFar = cmdCode & 1; + if (focusFar == 1) { + codeForFI.setFocus(1); + } + + String param1Str = cmdStr.substring(8, 10); + codeForFI.setFocusSpeed(Integer.parseInt(param1Str, 16)); + String param2Str = cmdStr.substring(10, 12); + codeForFI.setIrisSpeed(Integer.parseInt(param2Str, 16)); + return codeForFI; + }else if (cmdCode < 131) { + // 预置位指令 + FrontEndControlCodeForPreset codeForPreset = new FrontEndControlCodeForPreset(); + switch (cmdCode) { + case 0x81: // 设置预置位 + codeForPreset.setCode(1); + break; + case 0x82: // 调用预置位 + codeForPreset.setCode(2); + break; + case 0x83: // 删除预置位 + codeForPreset.setCode(3); + break; + default: + return null; + } + // 预置位编号 + String param2Str = cmdStr.substring(10, 12); + codeForPreset.setPresetId(Integer.parseInt(param2Str, 16)); + return codeForPreset; + }else if (cmdCode < 136) { + // 巡航指令 + FrontEndControlCodeForTour codeForTour = new FrontEndControlCodeForTour(); + String param3Str = cmdStr.substring(12, 13); + switch (cmdCode) { + case 0x84: // 加入巡航点 + codeForTour.setCode(1); + break; + case 0x85: // 删除一个巡航点 + codeForTour.setCode(2); + break; + case 0x86: // 设置巡航速度 + codeForTour.setCode(3); + codeForTour.setTourSpeed(Integer.parseInt(param3Str, 16)); + break; + case 0x87: // 设置巡航停留时间 + codeForTour.setCode(4); + codeForTour.setTourTime(Integer.parseInt(param3Str, 16)); + break; + case 0x88: // 开始巡航 + codeForTour.setCode(5); + break; + default: + return null; + } + String param1Str = cmdStr.substring(8, 10); + codeForTour.setTourId(Integer.parseInt(param1Str, 16)); + String param2Str = cmdStr.substring(10, 12); + codeForTour.setPresetId(Integer.parseInt(param2Str, 16)); + return codeForTour; + }else if (cmdCode < 138) { + // 扫描指令 + FrontEndControlCodeForScan controlCodeForScan = new FrontEndControlCodeForScan(); + String param2Str = cmdStr.substring(10, 11); + int param2Code = Integer.parseInt(param2Str, 16); + switch (cmdCode) { + case 0x89: + switch (param2Code) { + case 0x00: // 开始自动扫描 + controlCodeForScan.setCode(1); + break; + case 0x01: // 设置自动扫描左边界 + controlCodeForScan.setCode(2); + break; + case 0x02: // 设置自动扫描右边界 + controlCodeForScan.setCode(3); + break; + } + break; + case 0x8A: // 删除一个巡航点 + controlCodeForScan.setCode(4); + String param3Str = cmdStr.substring(12, 13); + controlCodeForScan.setScanSpeed(Integer.parseInt(param3Str, 16)); + break; + default: + return null; + } + String param1Str = cmdStr.substring(8, 10); + controlCodeForScan.setScanId(Integer.parseInt(param1Str, 16)); + return controlCodeForScan; + }else if (cmdCode < 141) { + // 辅助开关 + FrontEndControlCodeForAuxiliary codeForAuxiliary = new FrontEndControlCodeForAuxiliary(); + switch (cmdCode) { + case 0x8C: // 开 + codeForAuxiliary.setCode(1); + break; + case 0x8D: // 关 + codeForAuxiliary.setCode(2); + break; + default: + return null; + } + // 预置位编号 + String param2Str = cmdStr.substring(10, 12); + codeForAuxiliary.setAuxiliaryId(Integer.parseInt(param2Str, 16)); + return codeForAuxiliary; + }else { + return null; + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForAuxiliary.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForAuxiliary.java new file mode 100644 index 0000000..4dc994a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForAuxiliary.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Getter; +import lombok.Setter; + +public class FrontEndControlCodeForAuxiliary implements IFrontEndControlCode { + + private final FrontEndControlType type = FrontEndControlType.AUXILIARY; + + @Override + public FrontEndControlType getType() { + return type; + } + + /** + * 辅助开关控制指令: 1为开, 2为关 + */ + @Getter + @Setter + private Integer code; + + /** + * 辅助开关编号 + */ + @Getter + @Setter + private Integer auxiliaryId; + + @Override + public String encode() { + return ""; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForFI.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForFI.java new file mode 100644 index 0000000..89c9f8d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForFI.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Getter; +import lombok.Setter; + +public class FrontEndControlCodeForFI implements IFrontEndControlCode { + + private final FrontEndControlType type = FrontEndControlType.FI; + + @Override + public FrontEndControlType getType() { + return type; + } + + /** + * 光圈,0为缩小 1为放大 + */ + @Getter + @Setter + private Integer iris; + + /** + * 聚焦 0 近, 1远 + */ + @Getter + @Setter + private Integer focus; + + /** + * 聚焦速度 + */ + @Getter + @Setter + private Integer focusSpeed; + + /** + * 光圈速度 + */ + @Getter + @Setter + private Integer irisSpeed; + + @Override + public String encode() { + return ""; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForPTZ.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForPTZ.java new file mode 100644 index 0000000..3759860 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForPTZ.java @@ -0,0 +1,62 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Getter; +import lombok.Setter; + +public class FrontEndControlCodeForPTZ implements IFrontEndControlCode { + + private final FrontEndControlType type = FrontEndControlType.PTZ; + + @Override + public FrontEndControlType getType() { + return type; + } + + /** + * 镜头变倍,0为缩小 1为放大 + */ + @Getter + @Setter + private Integer zoom; + + /** + * 云台垂直方向控制 0 为上, 1为下 + */ + @Getter + @Setter + private Integer tilt; + + /** + * 云台水平方向控制 0 为左, 1为右 + */ + @Getter + @Setter + private Integer pan; + + /** + * 水平控制速度相对值 + */ + @Getter + @Setter + private Integer panSpeed; + + /** + * 垂直控制速度相对值 + */ + @Getter + @Setter + private Integer tiltSpeed; + + /** + * 变倍控制速度相对值 + */ + @Getter + @Setter + private Integer zoomSpeed; + + @Override + public String encode() { + return ""; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForPreset.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForPreset.java new file mode 100644 index 0000000..8212d6a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForPreset.java @@ -0,0 +1,42 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Getter; +import lombok.Setter; + +public class FrontEndControlCodeForPreset implements IFrontEndControlCode { + + private final FrontEndControlType type = FrontEndControlType.PRESET; + + @Override + public FrontEndControlType getType() { + return type; + } + + /** + * 预置位指令: 1为设置预置位, 2为调用预置位, 3为删除预置位 + */ + @Getter + @Setter + private Integer code; + + /** + * 预置位编号 + */ + @Getter + @Setter + private Integer presetId; + + /** + * 预置位名称 + */ + @Getter + @Setter + private String presetName; + + + @Override + public String encode() { + return ""; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForScan.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForScan.java new file mode 100644 index 0000000..ce16537 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForScan.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Getter; +import lombok.Setter; + +public class FrontEndControlCodeForScan implements IFrontEndControlCode { + + private final FrontEndControlType type = FrontEndControlType.SCAN; + + @Override + public FrontEndControlType getType() { + return type; + } + + /** + * 预置位指令: 1为开始自动扫描, 2为设置自动扫描左边界, 3为设置自动扫描右边界, 4为设置自动扫描速度, 5为停止自动扫描 + */ + @Getter + @Setter + private Integer code; + + /** + * 自动扫描速度 + */ + @Getter + @Setter + private Integer scanSpeed; + + /** + * 扫描组号 + */ + @Getter + @Setter + private Integer scanId; + + @Override + public String encode() { + return ""; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForTour.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForTour.java new file mode 100644 index 0000000..91ecb25 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForTour.java @@ -0,0 +1,55 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Getter; +import lombok.Setter; + +public class FrontEndControlCodeForTour implements IFrontEndControlCode { + + private final FrontEndControlType type = FrontEndControlType.TOUR; + + @Override + public FrontEndControlType getType() { + return type; + } + + /** + * 巡航指令: 1为加入巡航点, 2为删除一个巡航点, 3为设置巡航速度, 4为设置巡航停留时间, 5为开始巡航, 6为停止巡航 + */ + @Getter + @Setter + private Integer code; + + /** + * 巡航点 + */ + @Getter + @Setter + private Integer tourId; + + /** + * 巡航停留时间 + */ + @Getter + @Setter + private Integer tourTime; + + /** + * 巡航速度 + */ + @Getter + @Setter + private Integer tourSpeed; + + /** + * 预置位编号 + */ + @Getter + @Setter + private Integer presetId; + + @Override + public String encode() { + return ""; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForWiper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForWiper.java new file mode 100644 index 0000000..9e1af0e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForWiper.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Getter; +import lombok.Setter; + +public class FrontEndControlCodeForWiper implements IFrontEndControlCode { + + private final FrontEndControlType type = FrontEndControlType.AUXILIARY; + + @Override + public FrontEndControlType getType() { + return type; + } + + /** + * 辅助开关控制指令: 1为开, 2为关 + */ + @Getter + @Setter + private Integer code; + + @Override + public String encode() { + return ""; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlType.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlType.java new file mode 100644 index 0000000..b79fbed --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlType.java @@ -0,0 +1,6 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public enum FrontEndControlType { + + PTZ,FI,PRESET,TOUR,SCAN,AUXILIARY +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GBStringMsgParser.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GBStringMsgParser.java new file mode 100755 index 0000000..d4dab07 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GBStringMsgParser.java @@ -0,0 +1,455 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import gov.nist.core.Host; +import gov.nist.core.HostNameParser; +import gov.nist.javax.sip.SIPConstants; +import gov.nist.javax.sip.address.AddressImpl; +import gov.nist.javax.sip.address.GenericURI; +import gov.nist.javax.sip.address.SipUri; +import gov.nist.javax.sip.address.TelephoneNumber; +import gov.nist.javax.sip.header.*; +import gov.nist.javax.sip.message.SIPMessage; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import gov.nist.javax.sip.parser.*; +import lombok.extern.slf4j.Slf4j; + +import java.io.UnsupportedEncodingException; +import java.text.ParseException; + +@Slf4j +public class GBStringMsgParser implements MessageParser { + + protected static boolean computeContentLengthFromMessage = false; + + /** + * @since v0.9 + */ + public GBStringMsgParser() { + super(); + } + + /** + * Parse a buffer containing a single SIP Message where the body is an array + * of un-interpreted bytes. This is intended for parsing the message from a + * memory buffer when the buffer. Incorporates a bug fix for a bug that was + * noted by Will Sullin of Callcast + * + * @param msgBuffer + * a byte buffer containing the messages to be parsed. This can + * consist of multiple SIP Messages concatenated together. + * @return a SIPMessage[] structure (request or response) containing the + * parsed SIP message. + * @exception ParseException + * is thrown when an illegal message has been encountered + * (and the rest of the buffer is discarded). + * @see ParseExceptionListener + */ + public SIPMessage parseSIPMessage(byte[] msgBuffer, boolean readBody, boolean strict, ParseExceptionListener parseExceptionListener) throws ParseException { + if (msgBuffer == null || msgBuffer.length == 0) + return null; + + int i = 0; + + // Squeeze out any leading control character. + try { + while (msgBuffer[i] < 0x20) + i++; + } + catch (ArrayIndexOutOfBoundsException e) { + // Array contains only control char, return null. + if (log.isDebugEnabled()) { + log.debug("handled only control char so returning null"); + } + return null; + } + + // Iterate thru the request/status line and headers. + String currentLine = null; + String currentHeader = null; + boolean isFirstLine = true; + SIPMessage message = null; + do + { + int lineStart = i; + + // Find the length of the line. + try { + while (msgBuffer[i] != '\r' && msgBuffer[i] != '\n') + i++; + } + catch (ArrayIndexOutOfBoundsException e) { + // End of the message. + break; + } + int lineLength = i - lineStart; + + // Make it a String. + try { + currentLine = new String(msgBuffer, lineStart, lineLength, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new ParseException("Bad message encoding!", 0); + } + + currentLine = trimEndOfLine(currentLine); + + if (currentLine.length() == 0) { + // Last header line, process the previous buffered header. + if (currentHeader != null && message != null) { + processHeader(currentHeader, message, parseExceptionListener, msgBuffer); + } + + } + else { + if (isFirstLine) { + message = processFirstLine(currentLine, parseExceptionListener, msgBuffer); + } else { + char firstChar = currentLine.charAt(0); + if (firstChar == '\t' || firstChar == ' ') { + if (currentHeader == null) + throw new ParseException("Bad header continuation.", 0); + + // This is a continuation, append it to the previous line. + currentHeader += currentLine.substring(1); + } + else { + if (currentHeader != null && message != null) { + processHeader(currentHeader, message, parseExceptionListener, msgBuffer); + } + currentHeader = currentLine; + } + } + } + + if (msgBuffer[i] == '\r' && msgBuffer.length > i+1 && msgBuffer[i+1] == '\n') + i++; + + i++; + + isFirstLine = false; + } while (currentLine.length() > 0); // End do - while + + if (message == null) throw new ParseException("Bad message", 0); + message.setSize(i); + + // Check for content legth header + if (readBody && message.getContentLength() != null ) { + if ( message.getContentLength().getContentLength() != 0) { + int bodyLength = msgBuffer.length - i; + + byte[] body = new byte[bodyLength]; + System.arraycopy(msgBuffer, i, body, 0, bodyLength); + message.setMessageContent(body,!strict,computeContentLengthFromMessage,message.getContentLength().getContentLength()); + } else if (message.getCSeqHeader().getMethod().equalsIgnoreCase("MESSAGE")) { + int bodyLength = msgBuffer.length - i; + + byte[] body = new byte[bodyLength]; + System.arraycopy(msgBuffer, i, body, 0, bodyLength); + message.setMessageContent(body,!strict,computeContentLengthFromMessage,bodyLength); + }else if (!computeContentLengthFromMessage && strict) { + String last4Chars = new String(msgBuffer, msgBuffer.length - 4, 4); + if(!"\r\n\r\n".equals(last4Chars)) { + throw new ParseException("Extraneous characters at the end of the message ",i); + } + } + } + + return message; + } + + protected static String trimEndOfLine(String line) { + if (line == null) + return line; + + int i = line.length() - 1; + while (i >= 0 && line.charAt(i) <= 0x20) + i--; + + if (i == line.length() - 1) + return line; + + if (i == -1) + return ""; + + return line.substring(0, i+1); + } + + protected SIPMessage processFirstLine(String firstLine, ParseExceptionListener parseExceptionListener, byte[] msgBuffer) throws ParseException { + SIPMessage message; + if (!firstLine.startsWith(SIPConstants.SIP_VERSION_STRING)) { + message = new SIPRequest(); + try { + RequestLine requestLine = new RequestLineParser(firstLine + "\n") + .parse(); + ((SIPRequest) message).setRequestLine(requestLine); + } catch (ParseException ex) { + if (parseExceptionListener != null) + try { + parseExceptionListener.handleException(ex, message, + RequestLine.class, firstLine, new String(msgBuffer, "UTF-8")); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + else + throw ex; + + } + } else { + message = new SIPResponse(); + try { + StatusLine sl = new StatusLineParser(firstLine + "\n").parse(); + ((SIPResponse) message).setStatusLine(sl); + } catch (ParseException ex) { + if (parseExceptionListener != null) { + try { + parseExceptionListener.handleException(ex, message, + StatusLine.class, firstLine, new String(msgBuffer, "UTF-8")); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + } else + throw ex; + + } + } + return message; + } + + protected void processHeader(String header, SIPMessage message, ParseExceptionListener parseExceptionListener, byte[] rawMessage) throws ParseException { + if (header == null || header.length() == 0) + return; + + HeaderParser headerParser = null; + try { + headerParser = ParserFactory.createParser(header + "\n"); + } catch (ParseException ex) { + // https://java.net/jira/browse/JSIP-456 + if (parseExceptionListener != null) { + parseExceptionListener.handleException(ex, message, null, + header, null); + return; + } else { + throw ex; + } + } + + try { + SIPHeader sipHeader = headerParser.parse(); + message.attachHeader(sipHeader, false); + } catch (ParseException ex) { + if (parseExceptionListener != null) { + String headerName = Lexer.getHeaderName(header); + Class headerClass = NameMap.getClassFromName(headerName); + if (headerClass == null) { + headerClass = ExtensionHeaderImpl.class; + + } + try { + parseExceptionListener.handleException(ex, message, + headerClass, header, new String(rawMessage, "UTF-8")); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + } + } + } + + /** + * Parse an address (nameaddr or address spec) and return and address + * structure. + * + * @param address + * is a String containing the address to be parsed. + * @return a parsed address structure. + * @since v1.0 + * @exception ParseException + * when the address is badly formatted. + */ + public AddressImpl parseAddress(String address) throws ParseException { + AddressParser addressParser = new AddressParser(address); + return addressParser.address(true); + } + + /** + * Parse a host:port and return a parsed structure. + * + * @param hostport + * is a String containing the host:port to be parsed + * @return a parsed address structure. + * @since v1.0 + * @exception throws + * a ParseException when the address is badly formatted. + * + public HostPort parseHostPort(String hostport) throws ParseException { + Lexer lexer = new Lexer("charLexer", hostport); + return new HostNameParser(lexer).hostPort(); + + } + */ + + /** + * Parse a host name and return a parsed structure. + * + * @param host + * is a String containing the host name to be parsed + * @return a parsed address structure. + * @since v1.0 + * @exception ParseException + * a ParseException when the hostname is badly formatted. + */ + public Host parseHost(String host) throws ParseException { + Lexer lexer = new Lexer("charLexer", host); + return new HostNameParser(lexer).host(); + + } + + /** + * Parse a telephone number return a parsed structure. + * + * @param telephone_number + * is a String containing the telephone # to be parsed + * @return a parsed address structure. + * @since v1.0 + * @exception ParseException + * a ParseException when the address is badly formatted. + */ + public TelephoneNumber parseTelephoneNumber(String telephone_number) + throws ParseException { + // Bug fix contributed by Will Scullin + return new URLParser(telephone_number).parseTelephoneNumber(true); + + } + + /** + * Parse a SIP url from a string and return a URI structure for it. + * + * @param url + * a String containing the URI structure to be parsed. + * @return A parsed URI structure + * @exception ParseException + * if there was an error parsing the message. + */ + + public SipUri parseSIPUrl(String url) throws ParseException { + try { + return new URLParser(url).sipURL(true); + } catch (ClassCastException ex) { + throw new ParseException(url + " Not a SIP URL ", 0); + } + } + + /** + * Parse a uri from a string and return a URI structure for it. + * + * @param url + * a String containing the URI structure to be parsed. + * @return A parsed URI structure + * @exception ParseException + * if there was an error parsing the message. + */ + + public GenericURI parseUrl(String url) throws ParseException { + return new URLParser(url).parse(); + } + + /** + * Parse an individual SIP message header from a string. + * + * @param header + * String containing the SIP header. + * @return a SIPHeader structure. + * @exception ParseException + * if there was an error parsing the message. + */ + public static SIPHeader parseSIPHeader(String header) throws ParseException { + int start = 0; + int end = header.length() - 1; + try { + // Squeeze out any leading control character. + while (header.charAt(start) <= 0x20) + start++; + + // Squeeze out any trailing control character. + while (header.charAt(end) <= 0x20) + end--; + } + catch (ArrayIndexOutOfBoundsException e) { + // Array contains only control char. + throw new ParseException("Empty header.", 0); + } + + StringBuilder buffer = new StringBuilder(end + 1); + int i = start; + int lineStart = start; + boolean endOfLine = false; + while (i <= end) { + char c = header.charAt(i); + if (c == '\r' || c == '\n') { + if (!endOfLine) { + buffer.append(header.substring(lineStart, i)); + endOfLine = true; + } + } + else { + if (endOfLine) { + endOfLine = false; + if (c == ' ' || c == '\t') { + buffer.append(' '); + lineStart = i + 1; + } + else { + lineStart = i; + } + } + } + + i++; + } + buffer.append(header.substring(lineStart, i)); + buffer.append('\n'); + + HeaderParser hp = ParserFactory.createParser(buffer.toString()); + if (hp == null) + throw new ParseException("could not create parser", 0); + return hp.parse(); + } + + /** + * Parse the SIP Request Line + * + * @param requestLine + * a String containing the request line to be parsed. + * @return a RequestLine structure that has the parsed RequestLine + * @exception ParseException + * if there was an error parsing the requestLine. + */ + + public RequestLine parseSIPRequestLine(String requestLine) + throws ParseException { + requestLine += "\n"; + return new RequestLineParser(requestLine).parse(); + } + + /** + * Parse the SIP Response message status line + * + * @param statusLine + * a String containing the Status line to be parsed. + * @return StatusLine class corresponding to message + * @exception ParseException + * if there was an error parsing + * @see StatusLine + */ + + public StatusLine parseSIPStatusLine(String statusLine) + throws ParseException { + statusLine += "\n"; + return new StatusLineParser(statusLine).parse(); + } + + public static void setComputeContentLengthFromMessage( + boolean computeContentLengthFromMessage) { + GBStringMsgParser.computeContentLengthFromMessage = computeContentLengthFromMessage; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Gb28181Sdp.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Gb28181Sdp.java new file mode 100644 index 0000000..4b9e26a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Gb28181Sdp.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import javax.sdp.SessionDescription; + +/** + * 28181 的SDP解析器 + */ +public class Gb28181Sdp { + private SessionDescription baseSdb; + private String ssrc; + + private String mediaDescription; + + public static Gb28181Sdp getInstance(SessionDescription baseSdb, String ssrc, String mediaDescription) { + Gb28181Sdp gb28181Sdp = new Gb28181Sdp(); + gb28181Sdp.setBaseSdb(baseSdb); + gb28181Sdp.setSsrc(ssrc); + gb28181Sdp.setMediaDescription(mediaDescription); + return gb28181Sdp; + } + + + public SessionDescription getBaseSdb() { + return baseSdb; + } + + public void setBaseSdb(SessionDescription baseSdb) { + this.baseSdb = baseSdb; + } + + public String getSsrc() { + return ssrc; + } + + public void setSsrc(String ssrc) { + this.ssrc = ssrc; + } + + public String getMediaDescription() { + return mediaDescription; + } + + public void setMediaDescription(String mediaDescription) { + this.mediaDescription = mediaDescription; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbCode.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbCode.java new file mode 100644 index 0000000..bc5e508 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbCode.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 国标编码对象 + */ +@Data +@Schema(description = "国标编码对象") +public class GbCode { + + @Schema(description = "中心编码,由监控中心所在地的行政区划代码确定,符合GB/T2260—2007的要求") + private String centerCode; + + @Schema(description = "行业编码") + private String industryCode; + + @Schema(description = "类型编码") + private String typeCode; + + @Schema(description = "网络标识") + private String netCode; + + @Schema(description = "序号") + private String sn; + + /** + * 解析国标编号 + */ + public static GbCode decode(String code){ + if (code == null || code.trim().length() != 20) { + return null; + } + code = code.trim(); + GbCode gbCode = new GbCode(); + gbCode.setCenterCode(code.substring(0, 8)); + gbCode.setIndustryCode(code.substring(8, 10)); + gbCode.setTypeCode(code.substring(10, 13)); + gbCode.setNetCode(code.substring(13, 14)); + gbCode.setSn(code.substring(14)); + return gbCode; + } + + public String ecode(){ + return centerCode + industryCode + typeCode + netCode + sn; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbSipDate.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbSipDate.java new file mode 100644 index 0000000..e02954f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbSipDate.java @@ -0,0 +1,148 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import gov.nist.core.InternalErrorHandler; +import gov.nist.javax.sip.header.SIPDate; + +import java.io.Serial; +import java.util.*; + +/** + * 重写jain sip的SIPDate解决与国标时间格式不一致的问题 + */ +public class GbSipDate extends SIPDate { + + @Serial + private static final long serialVersionUID = 1L; + + private Calendar javaCal; + + public GbSipDate(long timeMillis) { + this.javaCal = new GregorianCalendar(TimeZone.getDefault(), Locale.getDefault()); + Date date = new Date(timeMillis); + this.javaCal.setTime(date); + this.wkday = this.javaCal.get(7); + switch(this.wkday) { + case 1: + this.sipWkDay = "Sun"; + break; + case 2: + this.sipWkDay = "Mon"; + break; + case 3: + this.sipWkDay = "Tue"; + break; + case 4: + this.sipWkDay = "Wed"; + break; + case 5: + this.sipWkDay = "Thu"; + break; + case 6: + this.sipWkDay = "Fri"; + break; + case 7: + this.sipWkDay = "Sat"; + break; + default: + InternalErrorHandler.handleException("No date map for wkday " + this.wkday); + } + + this.day = this.javaCal.get(5); + this.month = this.javaCal.get(2); + switch(this.month) { + case 0: + this.sipMonth = "Jan"; + break; + case 1: + this.sipMonth = "Feb"; + break; + case 2: + this.sipMonth = "Mar"; + break; + case 3: + this.sipMonth = "Apr"; + break; + case 4: + this.sipMonth = "May"; + break; + case 5: + this.sipMonth = "Jun"; + break; + case 6: + this.sipMonth = "Jul"; + break; + case 7: + this.sipMonth = "Aug"; + break; + case 8: + this.sipMonth = "Sep"; + break; + case 9: + this.sipMonth = "Oct"; + break; + case 10: + this.sipMonth = "Nov"; + break; + case 11: + this.sipMonth = "Dec"; + break; + default: + InternalErrorHandler.handleException("No date map for month " + this.month); + } + + this.year = this.javaCal.get(1); + this.hour = this.javaCal.get(11); + this.minute = this.javaCal.get(12); + this.second = this.javaCal.get(13); + } + + @Override + public StringBuilder encode(StringBuilder var1) { + String var2; + if (this.month < 9) { + var2 = "0" + (this.month + 1); + } else { + var2 = "" + (this.month + 1); + } + + String var3; + if (this.day < 10) { + var3 = "0" + this.day; + } else { + var3 = "" + this.day; + } + + String var4; + if (this.hour < 10) { + var4 = "0" + this.hour; + } else { + var4 = "" + this.hour; + } + + String var5; + if (this.minute < 10) { + var5 = "0" + this.minute; + } else { + var5 = "" + this.minute; + } + + String var6; + if (this.second < 10) { + var6 = "0" + this.second; + } else { + var6 = "" + this.second; + } + + int var8 = this.javaCal.get(14); + String var7; + if (var8 < 10) { + var7 = "00" + var8; + } else if (var8 < 100) { + var7 = "0" + var8; + } else { + var7 = "" + var8; + } + + return var1.append(this.year).append("-").append(var2).append("-").append(var3).append("T").append(var4).append(":").append(var5).append(":").append(var6).append(".").append(var7); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbSteamIdentification.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbSteamIdentification.java new file mode 100644 index 0000000..63c17a8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbSteamIdentification.java @@ -0,0 +1,44 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +/** + * 码流索引标识 + */ +public enum GbSteamIdentification { + /** + * 主码流 stream:0 + * 子码流 stream:1s + */ + streamMain("stream", new String[]{"0","1"}), + /** + * 国标28181-2022定义的方式 + * 主码流 streamnumber:0 + * 子码流 streamnumber:1 + */ + streamnumber("streamnumber", new String[]{"0","1"}), + /** + * 主码流 streamprofile:0 + * 子码流 streamprofile:1 + */ + streamprofile("streamprofile", new String[]{"0","1"}), + /** + * 适用的品牌: TP-LINK + */ + streamMode("streamMode", new String[]{"main","sub"}), + ; + + GbSteamIdentification(String value, String[] indexArray) { + this.value = value; + this.indexArray = indexArray; + } + + private String value; + private String[] indexArray; + + public String getValue() { + return value; + } + + public String[] getIndexArray() { + return indexArray; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbStream.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbStream.java new file mode 100755 index 0000000..67058f3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbStream.java @@ -0,0 +1,125 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * 直播流关联国标上级平台 + * @author lin + */ +@Schema(description = "直播流关联国标上级平台") +public class GbStream extends PlatformGbStream{ + + @Schema(description = "ID") + private int gbStreamId; + @Schema(description = "应用名") + private String app; + @Schema(description = "流ID") + private String stream; + @Schema(description = "国标ID") + private String gbId; + @Schema(description = "名称") + private String name; + @Schema(description = "流媒体ID") + private String mediaServerId; + @Schema(description = "经度") + private double longitude; + @Schema(description = "纬度") + private double latitude; + @Schema(description = "流类型(拉流/推流)") + private String streamType; + @Schema(description = "状态") + private boolean status; + + @Schema(description = "创建时间") + public String createTime; + + @Override + public Integer getGbStreamId() { + return gbStreamId; + } + + @Override + public void setGbStreamId(Integer gbStreamId) { + this.gbStreamId = gbStreamId; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getGbId() { + return gbId; + } + + public void setGbId(String gbId) { + this.gbId = gbId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getLongitude() { + return longitude; + } + + public void setLongitude(double longitude) { + this.longitude = longitude; + } + + public double getLatitude() { + return latitude; + } + + public void setLatitude(double latitude) { + this.latitude = latitude; + } + + public String getStreamType() { + return streamType; + } + + public void setStreamType(String streamType) { + this.streamType = streamType; + } + + public boolean isStatus() { + return status; + } + + public void setStatus(boolean status) { + this.status = status; + } + + public String getMediaServerId() { + return mediaServerId; + } + + public void setMediaServerId(String mediaServerId) { + this.mediaServerId = mediaServerId; + } + + public String getCreateTime() { + return createTime; + } + + public void setCreateTime(String createTime) { + this.createTime = createTime; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbStringMsgParserFactory.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbStringMsgParserFactory.java new file mode 100755 index 0000000..3a9a1d1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbStringMsgParserFactory.java @@ -0,0 +1,21 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import gov.nist.javax.sip.parser.MessageParser; +import gov.nist.javax.sip.parser.MessageParserFactory; +import gov.nist.javax.sip.stack.SIPTransactionStack; + +public class GbStringMsgParserFactory implements MessageParserFactory { + + /** + * msg parser is completely stateless, reuse isntance for the whole stack + * fixes https://github.com/RestComm/jain-sip/issues/92 + */ + private static GBStringMsgParser msgParser = new GBStringMsgParser(); + /* + * (non-Javadoc) + * @see gov.nist.javax.sip.parser.MessageParserFactory#createMessageParser(gov.nist.javax.sip.stack.SIPTransactionStack) + */ + public MessageParser createMessageParser(SIPTransactionStack stack) { + return msgParser; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Group.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Group.java new file mode 100644 index 0000000..0d6729d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Group.java @@ -0,0 +1,100 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.utils.DateUtil; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +/** + * 业务分组 + */ +@Data +@Schema(description = "业务分组") +public class Group implements Comparable{ + /** + * 数据库自增ID + */ + @Schema(description = "数据库自增ID") + private int id; + + /** + * 区域国标编号 + */ + @Schema(description = "区域国标编号") + private String deviceId; + + /** + * 区域名称 + */ + @Schema(description = "区域名称") + private String name; + + /** + * 父分组ID + */ + @Schema(description = "父分组ID") + private Integer parentId; + + /** + * 父区域国标ID + */ + @Schema(description = "父区域国标ID") + private String parentDeviceId; + + /** + * 所属的业务分组国标编号 + */ + @Schema(description = "所属的业务分组国标编号") + private String businessGroup; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + private String createTime; + + /** + * 更新时间 + */ + @Schema(description = "更新时间") + private String updateTime; + + /** + * 行政区划 + */ + @Schema(description = "行政区划") + private String civilCode; + + /** + * 别名 + */ + @Schema(description = "别名, 此别名为唯一值,可以对接第三方是存储对方的ID") + private String alias; + + public static Group getInstance(DeviceChannel channel) { + GbCode gbCode = GbCode.decode(channel.getDeviceId()); + if (gbCode == null || (!gbCode.getTypeCode().equals("215") && !gbCode.getTypeCode().equals("216"))) { + return null; + } + Group group = new Group(); + group.setName(channel.getName()); + group.setDeviceId(channel.getDeviceId()); + group.setCreateTime(DateUtil.getNow()); + group.setUpdateTime(DateUtil.getNow()); + if (gbCode.getTypeCode().equals("215")) { + group.setBusinessGroup(channel.getDeviceId()); + }else if (gbCode.getTypeCode().equals("216")) { + group.setBusinessGroup(channel.getBusinessGroupId()); + group.setParentDeviceId(channel.getParentId()); + } + if (group.getBusinessGroup() == null) { + return null; + } + return group; + } + + @Override + public int compareTo(@NotNull Group region) { + return Integer.compare(Integer.parseInt(this.deviceId), Integer.parseInt(region.getDeviceId())); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GroupTree.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GroupTree.java new file mode 100644 index 0000000..44789f0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GroupTree.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 业务分组 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Schema(description = "业务分组树") +public class GroupTree extends Group{ + + @Schema(description = "树节点ID") + private String treeId; + + @Schema(description = "是否有子节点") + private boolean isLeaf; + + @Schema(description = "类型, 行政区划:0 摄像头: 1") + private int type; + + @Schema(description = "在线状态") + private String status; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/HandlerCatchData.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/HandlerCatchData.java new file mode 100755 index 0000000..97da863 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/HandlerCatchData.java @@ -0,0 +1,44 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import org.dom4j.Element; + +import javax.sip.RequestEvent; + +/** + * @author lin + */ +public class HandlerCatchData { + private RequestEvent evt; + private Device device; + private Element rootElement; + + public HandlerCatchData(RequestEvent evt, Device device, Element rootElement) { + this.evt = evt; + this.device = device; + this.rootElement = rootElement; + } + + public RequestEvent getEvt() { + return evt; + } + + public void setEvt(RequestEvent evt) { + this.evt = evt; + } + + public Device getDevice() { + return device; + } + + public void setDevice(Device device) { + this.device = device; + } + + public Element getRootElement() { + return rootElement; + } + + public void setRootElement(Element rootElement) { + this.rootElement = rootElement; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/HomePositionRequest.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/HomePositionRequest.java new file mode 100755 index 0000000..2c20713 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/HomePositionRequest.java @@ -0,0 +1,94 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.gb28181.utils.MessageElement; + +/** + * 设备信息查询响应 + * + * @author Y.G + * @version 1.0 + * @date 2022/6/28 14:55 + */ +public class HomePositionRequest { + /** + * 序列号 + */ + @MessageElement("SN") + private String sn; + + @MessageElement("DeviceID") + private String deviceId; + + @MessageElement(value = "HomePosition") + private HomePosition homePosition; + + + /** + * 基本参数 + */ + public static class HomePosition { + /** + * 播放窗口长度像素值 + */ + @MessageElement("Enabled") + protected String enabled; + /** + * 播放窗口宽度像素值 + */ + @MessageElement("ResetTime") + protected String resetTime; + /** + * 拉框中心的横轴坐标像素值 + */ + @MessageElement("PresetIndex") + protected String presetIndex; + + public String getEnabled() { + return enabled; + } + + public void setEnabled(String enabled) { + this.enabled = enabled; + } + + public String getResetTime() { + return resetTime; + } + + public void setResetTime(String resetTime) { + this.resetTime = resetTime; + } + + public String getPresetIndex() { + return presetIndex; + } + + public void setPresetIndex(String presetIndex) { + this.presetIndex = presetIndex; + } + } + + public String getSn() { + return sn; + } + + public void setSn(String sn) { + this.sn = sn; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public HomePosition getHomePosition() { + return homePosition; + } + + public void setHomePosition(HomePosition homePosition) { + this.homePosition = homePosition; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Host.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Host.java new file mode 100755 index 0000000..1b14560 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Host.java @@ -0,0 +1,35 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + + +public class Host { + + private String ip; + private int port; + private String address; + + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IFrontEndControlCode.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IFrontEndControlCode.java new file mode 100644 index 0000000..8264e53 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IFrontEndControlCode.java @@ -0,0 +1,7 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public interface IFrontEndControlCode { + + FrontEndControlType getType(); + String encode(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IndustryCodeType.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IndustryCodeType.java new file mode 100644 index 0000000..d3414a2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IndustryCodeType.java @@ -0,0 +1,59 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import org.jetbrains.annotations.NotNull; + +public class IndustryCodeType implements Comparable{ + + /** + * 接入类型码 + */ + private String name; + + /** + * 名称 + */ + private String code; + + /** + * 备注 + */ + private String notes; + + public static IndustryCodeType getInstance(IndustryCodeTypeEnum typeEnum) { + IndustryCodeType industryCodeType = new IndustryCodeType(); + industryCodeType.setName(typeEnum.getName()); + industryCodeType.setCode(typeEnum.getCode()); + industryCodeType.setNotes(typeEnum.getNotes()); + return industryCodeType; + } + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } + + @Override + public int compareTo(@NotNull IndustryCodeType industryCodeType) { + return Integer.compare(Integer.parseInt(this.code), Integer.parseInt(industryCodeType.getCode())); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IndustryCodeTypeEnum.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IndustryCodeTypeEnum.java new file mode 100644 index 0000000..8d25fe9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IndustryCodeTypeEnum.java @@ -0,0 +1,55 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Getter; + +/** + * 收录行业编码 + */ +public enum IndustryCodeTypeEnum { + SOCIAL_SECURITY_ROAD("00", "社会治安路面接入", "包括城市路面、商业街、公共区域、重点区域"), + SOCIAL_SECURITY_COMMUNITY("01", "社会治安社区接入", "包括社区、楼宇、网吧等"), + SOCIAL_SECURITY__INTERNAL("02", "社会治安内部接入 ", "包括公安办公楼、留置室等"), + SOCIAL_SECURITY_OTHER("03", "社会治安其他接入", ""), + TRAFFIC_ROAD("04", "交通路面接入 ", "包括城市主要干道、国道、高速交通状况监视"), + TRAFFIC_BAYONET("05", "交通卡口接入", "包括交叉路口、“电子警察”、关口、收费站等"), + TRAFFIC_INTERNAL("06", "交通内部接入", "包括交管办公楼等"), + TRAFFIC_OTHER("07", "交通其他接入", ""), + CITY_MANAGEMENT("08", "城市管理接入", ""), + HEALTH_ENVIRONMENTAL_PROTECTION("09", "卫生环保接入", ""), + COMMODITY_INSPECTION_CUSTOMHOUSE("10", "商检海关接入", ""), + EDUCATION_SECTOR("11", "教育部门接入", ""), + CIVIL_AVIATION("12", "民航接入", ""), + RAILWAY("13", "铁路接入", ""), + SHIPPING("14", "航运接入", ""), + AGRICULTURE_FORESTRY_ANIMAL_HUSBANDRY_FISHING("40", "农、林、牧、渔业接入", ""), + MINING("41", "采矿业接入", ""), + MANUFACTURING_INDUSTRY("42", "制造业接入", ""), + ELECTRICITY_HEAT_GAS_AND_WATER_PRODUCTION_AND_SUPPLY("43", "电力、热力、燃气及水生产和供应业接入", ""), + CONSTRUCTION("44", "建筑业接入", ""), + WHOLESALE_AND_RETAIL("45", "批发和零售业接入", ""), + ; + + /** + * 接入类型码 + */ + @Getter + private String name; + + /** + * 名称 + */ + @Getter + private String code; + + /** + * 备注 + */ + @Getter + private String notes; + + IndustryCodeTypeEnum(String code, String name, String notes) { + this.name = name; + this.code = code; + this.notes = notes; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteDecodeException.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteDecodeException.java new file mode 100644 index 0000000..e9943f1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteDecodeException.java @@ -0,0 +1,14 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +@Data +public class InviteDecodeException extends RuntimeException{ + private int code; + private String msg; + + public InviteDecodeException(int code, String msg) { + this.code = code; + this.msg = msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteMessageInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteMessageInfo.java new file mode 100644 index 0000000..beadb69 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteMessageInfo.java @@ -0,0 +1,22 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +// 从INVITE消息中解析需要的信息 +@Data +public class InviteMessageInfo { + private String requesterId; + private String targetChannelId; + private String sourceChannelId; + private String sessionName; + private String ssrc; + private boolean tcp; + private boolean tcpActive; + private String callId; + private Long startTime; + private Long stopTime; + private String downloadSpeed; + private String ip; + private int port; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamCallback.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamCallback.java new file mode 100755 index 0000000..42a0519 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamCallback.java @@ -0,0 +1,5 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public interface InviteStreamCallback { + void call(InviteStreamInfo inviteStreamInfo); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamInfo.java new file mode 100755 index 0000000..e1925fa --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamInfo.java @@ -0,0 +1,61 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.media.bean.MediaServer; + +public class InviteStreamInfo { + + public InviteStreamInfo(MediaServer mediaServerItem, JSONObject response, String callId, String app, String stream) { + this.mediaServerItem = mediaServerItem; + this.response = response; + this.callId = callId; + this.app = app; + this.stream = stream; + } + + private MediaServer mediaServerItem; + private JSONObject response; + private String callId; + private String app; + private String stream; + + public MediaServer getMediaServerItem() { + return mediaServerItem; + } + + public void setMediaServerItem(MediaServer mediaServerItem) { + this.mediaServerItem = mediaServerItem; + } + + public JSONObject getResponse() { + return response; + } + + public void setResponse(JSONObject response) { + this.response = response; + } + + public String getCallId() { + return callId; + } + + public void setCallId(String callId) { + this.callId = callId; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamType.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamType.java new file mode 100755 index 0000000..4f62c66 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamType.java @@ -0,0 +1,8 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public enum InviteStreamType { + + PLAY,PLAYBACK,DOWNLOAD,PUSH,PROXY,CLOUD_RECORD_PUSH,CLOUD_RECORD_PROXY,BROADCAST,TALK + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/MessageResponseTask.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/MessageResponseTask.java new file mode 100644 index 0000000..cb3dfb0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/MessageResponseTask.java @@ -0,0 +1,43 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Getter; +import lombok.Setter; +import org.dom4j.Element; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +public class MessageResponseTask implements Delayed { + + @Getter + @Setter + private Element element; + + @Getter + @Setter + private List data; + + @Getter + @Setter + private String key; + + + /** + * 超时时间(单位: 毫秒) + */ + @Getter + @Setter + private long delayTime; + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/MobilePosition.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/MobilePosition.java new file mode 100755 index 0000000..39804a0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/MobilePosition.java @@ -0,0 +1,71 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +/** + * @description: 移动位置bean + * @author: lawrencehj + * @date: 2021年1月23日 + */ + +@Data +public class MobilePosition { + /** + * 设备Id + */ + private String deviceId; + + /** + * 通道Id + */ + private Integer channelId; + + /** + * 通道国标编号 + */ + private String channelDeviceId; + + /** + * 设备名称 + */ + private String deviceName; + + /** + * 通知时间 + */ + private String time; + + /** + * 经度 + */ + private double longitude; + + /** + * 纬度 + */ + private double latitude; + + /** + * 海拔高度 + */ + private double altitude; + + /** + * 速度 + */ + private double speed; + + /** + * 方向 + */ + private double direction; + + /** + * 位置信息上报来源(Mobile Position、GPS Alarm) + */ + private String reportSource; + /** + * 创建时间 + */ + private String createTime; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NetworkIdentificationType.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NetworkIdentificationType.java new file mode 100644 index 0000000..ed469e4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NetworkIdentificationType.java @@ -0,0 +1,45 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import org.jetbrains.annotations.NotNull; + +public class NetworkIdentificationType implements Comparable{ + + /** + * 接入类型码 + */ + private String name; + + /** + * 名称 + */ + private String code; + + public static NetworkIdentificationType getInstance(NetworkIdentificationTypeEnum typeEnum) { + NetworkIdentificationType industryCodeType = new NetworkIdentificationType(); + industryCodeType.setName(typeEnum.getName()); + industryCodeType.setCode(typeEnum.getCode()); + return industryCodeType; + } + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + @Override + public int compareTo(@NotNull NetworkIdentificationType networkIdentificationType) { + return Integer.compare(Integer.parseInt(this.code), Integer.parseInt(networkIdentificationType.getCode())); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NetworkIdentificationTypeEnum.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NetworkIdentificationTypeEnum.java new file mode 100644 index 0000000..1b0ed2a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NetworkIdentificationTypeEnum.java @@ -0,0 +1,51 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +/** + * 收录行业编码 + */ +public enum NetworkIdentificationTypeEnum { + PUBLIC_SECURITY_VIDEO_TRANSMISSION_NETWORK("0", "公安视频传输网"), + PUBLIC_SECURITY_VIDEO_TRANSMISSION_NETWORK2("1", "公安视频传输网"), + INDUSTRY_SPECIFIC_NETWORK("2", "行业专网"), + POLITICAL_AND_LEGAL_INFORMATION_NETWORK("3", "政法信息网"), + PUBLIC_SECURITY_MOBILE_INFORMATION_NETWORK("4", "公安移动信息网"), + PUBLIC_SECURITY_INFORMATION_NETWORK("5", "公安信息网"), + ELECTRONIC_GOVERNMENT_EXTRANET("6", "电子政务外网"), + PUBLIC_NETWORKS_SUCH_AS_THE_INTERNET("7", "互联网等公共网络"), + Dedicated_Line("8", "专线"), + RESERVE("9", "预留"), + ; + + /** + * 接入类型码 + */ + private String name; + + /** + * 名称 + */ + private String code; + + + NetworkIdentificationTypeEnum(String code, String name) { + this.name = name; + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NotifyCatalogChannel.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NotifyCatalogChannel.java new file mode 100644 index 0000000..8961677 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NotifyCatalogChannel.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +public class NotifyCatalogChannel { + + private Type type; + + private DeviceChannel channel; + + + public enum Type { + ADD, DELETE, UPDATE, STATUS_CHANGED + } + + + public static NotifyCatalogChannel getInstance(Type type, DeviceChannel channel) { + NotifyCatalogChannel notifyCatalogChannel = new NotifyCatalogChannel(); + notifyCatalogChannel.setType(type); + notifyCatalogChannel.setChannel(channel); + return notifyCatalogChannel; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public DeviceChannel getChannel() { + return channel; + } + + public void setChannel(DeviceChannel channel) { + this.channel = channel; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/OpenRTPServerResult.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/OpenRTPServerResult.java new file mode 100644 index 0000000..aa60444 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/OpenRTPServerResult.java @@ -0,0 +1,12 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.media.event.hook.HookData; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import lombok.Data; + +@Data +public class OpenRTPServerResult { + + private SSRCInfo ssrcInfo; + private HookData hookData; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Platform.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Platform.java new file mode 100755 index 0000000..13ec2c8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Platform.java @@ -0,0 +1,133 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @author lin + */ +@Data +@Schema(description = "平台信息") +public class Platform { + + @Schema(description = "ID(数据库中)") + private Integer id; + + @Schema(description = "是否启用") + private boolean enable; + + @Schema(description = "名称") + private String name; + + @Schema(description = "SIP服务国标编码") + private String serverGBId; + + @Schema(description = "SIP服务国标域") + private String serverGBDomain; + + @Schema(description = "SIP服务IP") + private String serverIp; + + @Schema(description = "SIP服务端口") + private int serverPort; + + @Schema(description = "设备国标编号") + private String deviceGBId; + + @Schema(description = "设备ip") + private String deviceIp; + + @Schema(description = "设备端口") + private int devicePort; + + @Schema(description = "SIP认证用户名(默认使用设备国标编号)") + private String username; + + @Schema(description = "SIP认证密码") + private String password; + + @Schema(description = "注册周期 (秒)") + private int expires; + + @Schema(description = "心跳周期(秒)") + private int keepTimeout; + + @Schema(description = "传输协议") + private String transport; + + @Schema(description = "字符集") + private String characterSet; + + @Schema(description = "允许云台控制") + private boolean ptz; + + @Schema(description = "RTCP流保活") + private boolean rtcp; + + @Schema(description = "在线状态") + private boolean status; + + @Schema(description = "通道数量") + private int channelCount; + + @Schema(description = "已被订阅目录信息") + private boolean catalogSubscribe; + + @Schema(description = "已被订阅报警信息") + private boolean alarmSubscribe; + + @Schema(description = "已被订阅移动位置信息") + private boolean mobilePositionSubscribe; + + @Schema(description = "目录分组-每次向上级发送通道信息时单个包携带的通道数量,取值1,2,4,8") + private int catalogGroup; + + @Schema(description = "更新时间") + private String updateTime; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "是否作为消息通道") + private boolean asMessageChannel; + + @Schema(description = "点播回复200OK使用的IP") + private String sendStreamIp; + + @Schema(description = "是否自动推送通道变化") + private Boolean autoPushChannel; + + @Schema(description = "目录信息包含平台信息, 0:关闭,1:打开") + private int catalogWithPlatform; + + @Schema(description = "目录信息包含分组信息, 0:关闭,1:打开") + private int catalogWithGroup; + + @Schema(description = "目录信息包含行政区划, 0:关闭,1:打开") + private int catalogWithRegion; + + @Schema(description = "行政区划") + private String civilCode; + + @Schema(description = "平台厂商") + private String manufacturer; + + @Schema(description = "平台型号") + private String model; + + @Schema(description = "平台安装地址") + private String address; + + @Schema(description = "注册方式(必选)缺省为1; " + + "1-符合IETF RFC 3261标准的认证注册模式;" + + "2-基于口令的双向认证注册模式;" + + "3-基于数字证书的双向认证注册模式(高安全级别要求);" + + "4-基于数字证书的单向认证注册模式(高安全级别要求)") + private int registerWay = 1; + + @Schema(description = "保密属性(必选)缺省为0;0-不涉密,1-涉密") + private int secrecy = 0; + + @Schema(description = "执行注册的服务ID") + private String serverId; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformCatalog.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformCatalog.java new file mode 100755 index 0000000..38ba2f0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformCatalog.java @@ -0,0 +1,116 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * 国标级联-目录 + * @author lin + */ +@Schema(description = "目录信息") +public class PlatformCatalog { + @Schema(description = "ID") + private String id; + + @Schema(description = "名称") + private String name; + + @Schema(description = "平台ID") + private String platformId; + + @Schema(description = "父级目录ID") + private String parentId; + + @Schema(description = "行政区划") + private String civilCode; + + @Schema(description = "目录分组") + private String businessGroupId; + + /** + * 子节点数 + */ + @Schema(description = "子节点数") + private int childrenCount; + + /** + * 0 目录, 1 国标通道, 2 直播流 + */ + @Schema(description = "类型:0 目录, 1 国标通道, 2 直播流") + private int type; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPlatformId() { + return platformId; + } + + public void setPlatformId(String platformId) { + this.platformId = platformId; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public int getChildrenCount() { + return childrenCount; + } + + public void setChildrenCount(int childrenCount) { + this.childrenCount = childrenCount; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public void setTypeForCatalog() { + this.type = 0; + } + + public void setTypeForGb() { + this.type = 1; + } + + public void setTypeForStream() { + this.type = 2; + } + + public String getCivilCode() { + return civilCode; + } + + public void setCivilCode(String civilCode) { + this.civilCode = civilCode; + } + + public String getBusinessGroupId() { + return businessGroupId; + } + + public void setBusinessGroupId(String businessGroupId) { + this.businessGroupId = businessGroupId; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformCatch.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformCatch.java new file mode 100755 index 0000000..db7ab98 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformCatch.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +@Data +public class PlatformCatch { + + private String id; + + /** + * 心跳未回复次数 + */ + private int keepAliveReply; + + // 注册未回复次数 + private int registerAliveReply; + + private String callId; + + private Platform platform; + + private SipTransactionInfo sipTransactionInfo; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformChannel.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformChannel.java new file mode 100644 index 0000000..a9517b3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformChannel.java @@ -0,0 +1,208 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class PlatformChannel extends CommonGBChannel { + + @Schema(description = "Id") + private int id; + + @Schema(description = "平台ID") + private int platformId; + + @Schema(description = "国标-编码") + private String customDeviceId; + + @Schema(description = "国标-名称") + private String customName; + + @Schema(description = "国标-设备厂商") + private String customManufacturer; + + @Schema(description = "国标-设备型号") + private String customModel; + + // 2016 + @Schema(description = "国标-设备归属") + private String customOwner; + + @Schema(description = "国标-行政区域") + private String customCivilCode; + + @Schema(description = "国标-警区") + private String customBlock; + + @Schema(description = "国标-安装地址") + private String customAddress; + + @Schema(description = "国标-是否有子设备") + private Integer customParental; + + @Schema(description = "国标-父节点ID") + private String customParentId; + + // 2016 + @Schema(description = "国标-信令安全模式") + private Integer customSafetyWay; + + @Schema(description = "国标-注册方式") + private Integer customRegisterWay; + + // 2016 + @Schema(description = "国标-证书序列号") + private Integer customCertNum; + + // 2016 + @Schema(description = "国标-证书有效标识") + private Integer customCertifiable; + + // 2016 + @Schema(description = "国标-无效原因码(有证书且证书无效的设备必选)") + private Integer customErrCode; + + // 2016 + @Schema(description = "国标-证书终止有效期(有证书且证书无效的设备必选)") + private Integer customEndTime; + + // 2022 + @Schema(description = "国标-摄像机安全能力等级代码") + private String customSecurityLevelCode; + + @Schema(description = "国标-保密属性(必选)缺省为0;0-不涉密,1-涉密") + private Integer customSecrecy; + + @Schema(description = "国标-设备/系统IPv4/IPv6地址") + private String customIpAddress; + + @Schema(description = "国标-设备/系统端口") + private Integer customPort; + + @Schema(description = "国标-设备口令") + private String customPassword; + + @Schema(description = "国标-设备状态") + private String customStatus; + + @Schema(description = "国标-经度 WGS-84坐标系") + private Double customLongitude; + + @Schema(description = "国标-纬度 WGS-84坐标系") + private Double customLatitude; + + @Schema(description = "国标-虚拟组织所属的业务分组ID") + private String customBusinessGroupId; + + @Schema(description = "国标-摄像机结构类型,标识摄像机类型: 1-球机; 2-半球; 3-固定枪机; 4-遥控枪机;5-遥控半球;6-多目设备的全景/拼接通道;7-多目设备的分割通道") + private Integer customPtzType; + + // 2016 + @Schema(description = "-摄像机位置类型扩展。1-省际检查站、2-党政机关、3-车站码头、4-中心广场、5-体育场馆、6-商业中心、7-宗教场所、" + + "8-校园周边、9-治安复杂区域、10-交通干线。当目录项为摄像机时可选。") + private Integer customPositionType; + + @Schema(description = "国标-摄像机光电成像类型。1-可见光成像;2-热成像;3-雷达成像;4-X光成像;5-深度光场成像;9-其他。可多值,") + private String customPhotoelectricImagingTyp; + + @Schema(description = "国标-摄像机采集部位类型") + private String customCapturePositionType; + + @Schema(description = "国标-摄像机安装位置室外、室内属性。1-室外、2-室内。") + private Integer customRoomType; + + // 2016 + @Schema(description = "国标-用途属性") + private Integer customUseType; + + @Schema(description = "国标-摄像机补光属性。1-无补光;2-红外补光;3-白光补光;4-激光补光;9-其他") + private Integer customSupplyLightType; + + @Schema(description = "国标-摄像机监视方位(光轴方向)属性。1-东(西向东)、2-西(东向西)、3-南(北向南)、4-北(南向北)、" + + "5-东南(西北到东南)、6-东北(西南到东北)、7-西南(东北到西南)、8-西北(东南到西北)") + private Integer customDirectionType; + + @Schema(description = "国标-摄像机支持的分辨率,可多值") + private String customResolution; + + // 2022 + @Schema(description = "国标-摄像机支持的码流编号列表,用于实时点播时指定码流编号(可选)") + private String customStreamNumberList; + + @Schema(description = "国标-下载倍速(可选),可多值") + private String customDownloadSpeed; + + @Schema(description = "国标-空域编码能力,取值0-不支持;1-1级增强(1个增强层);2-2级增强(2个增强层);3-3级增强(3个增强层)") + private Integer customSvcSpaceSupportMod; + + @Schema(description = "国标-时域编码能力,取值0-不支持;1-1级增强;2-2级增强;3-3级增强(可选)") + private Integer customSvcTimeSupportMode; + + // 2022 + @Schema(description = "国标- SSVC增强层与基本层比例能力 ") + private String customSsvcRatioSupportList; + + // 2022 + @Schema(description = "国标-移动采集设备类型(仅移动采集设备适用,必选);1-移动机器人载摄像机;2-执法记录仪;3-移动单兵设备;" + + "4-车载视频记录设备;5-无人机载摄像机;9-其他") + private Integer customMobileDeviceType; + + // 2022 + @Schema(description = "国标-摄像机水平视场角(可选),取值范围大于0度小于等于360度") + private Double customHorizontalFieldAngle; + + // 2022 + @Schema(description = "国标-摄像机竖直视场角(可选),取值范围大于0度小于等于360度 ") + private Double customVerticalFieldAngle; + + // 2022 + @Schema(description = "国标-摄像机可视距离(可选),单位:米") + private Double customMaxViewDistance; + + // 2022 + @Schema(description = "国标-基层组织编码(必选,非基层建设时为“000000”)") + private String customGrassrootsCode; + + // 2022 + @Schema(description = "国标-监控点位类型(当为摄像机时必选),1-一类视频监控点;2-二类视频监控点;3-三类视频监控点;9-其他点位。") + private Integer customPoType; + + // 2022 + @Schema(description = "国标-点位俗称") + private String customPoCommonName; + + // 2022 + @Schema(description = "国标-设备MAC地址(可选),用“XX-XX-XX-XX-XX-XX”格式表达") + private String customMac; + + // 2022 + @Schema(description = "国标-摄像机卡口功能类型,01-人脸卡口;02-人员卡口;03-机动车卡口;04-非机动车卡口;05-物品卡口;99-其他") + private String customFunctionType; + + // 2022 + @Schema(description = "国标-摄像机视频编码格式") + private String customEncodeType; + + // 2022 + @Schema(description = "国标-摄像机安装使用时间") + private String customInstallTime; + + // 2022 + @Schema(description = "国标-摄像机所属管理单位名称") + private String customManagementUnit; + + // 2022 + @Schema(description = "国标-摄像机所属管理单位联系人的联系方式(电话号码,可多值,用英文半角“/”分割)") + private String customContactInfo; + + // 2022 + @Schema(description = "国标-录像保存天数(可选)") + private Integer customRecordSaveDays; + + // 2022 + @Schema(description = "国标-国民经济行业分类代码(可选)") + private String customIndustrialClassification; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformGbStream.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformGbStream.java new file mode 100755 index 0000000..f7402b6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformGbStream.java @@ -0,0 +1,39 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; + +public class PlatformGbStream { + + @Schema(description = "ID") + private int gbStreamId; + + @Schema(description = "平台ID") + private String platformId; + + @Schema(description = "目录ID") + private String catalogId; + + public Integer getGbStreamId() { + return gbStreamId; + } + + public void setGbStreamId(Integer gbStreamId) { + this.gbStreamId = gbStreamId; + } + + public String getPlatformId() { + return platformId; + } + + public void setPlatformId(String platformId) { + this.platformId = platformId; + } + + public String getCatalogId() { + return catalogId; + } + + public void setCatalogId(String catalogId) { + this.catalogId = catalogId; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformKeepaliveCallback.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformKeepaliveCallback.java new file mode 100644 index 0000000..2e98fa8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformKeepaliveCallback.java @@ -0,0 +1,5 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public interface PlatformKeepaliveCallback { + public void run(String platformServerGbId, int failCount); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformRegister.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformRegister.java new file mode 100755 index 0000000..4a45862 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformRegister.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public class PlatformRegister { + + // 未回复次数 + private int reply; + + public int getReply() { + return reply; + } + + public void setReply(int reply) { + this.reply = reply; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlayException.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlayException.java new file mode 100644 index 0000000..8934394 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlayException.java @@ -0,0 +1,14 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +@Data +public class PlayException extends RuntimeException{ + private int code; + private String msg; + + public PlayException(int code, String msg) { + this.code = code; + this.msg = msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Preset.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Preset.java new file mode 100755 index 0000000..0bb8eec --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Preset.java @@ -0,0 +1,12 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Data; + +@Data +public class Preset { + + private String presetId; + + private String presetName; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordInfo.java new file mode 100755 index 0000000..130b768 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordInfo.java @@ -0,0 +1,43 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.time.Instant; +import java.util.List; + +/** + * @description:设备录像信息bean + * @author: swwheihei + * @date: 2020年5月8日 下午2:05:56 + */ +@Setter +@Getter +@Schema(description = "设备录像查询结果信息") +public class RecordInfo { + + @Schema(description = "设备编号") + private String deviceId; + + @Schema(description = "通道编号") + private String channelId; + + @Schema(description = "命令序列号") + private String sn; + + @Schema(description = "设备名称") + private String name; + + @Schema(description = "列表总数") + private int sumNum; + + private int count; + + private Instant lastTime; + + @Schema(description = "") + private List recordList; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordItem.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordItem.java new file mode 100755 index 0000000..bc7630b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordItem.java @@ -0,0 +1,68 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import com.genersoft.iot.vmp.utils.DateUtil; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; + +import java.time.Instant; +import java.time.temporal.TemporalAccessor; + +/** + * @description:设备录像bean + * @author: swwheihei + * @date: 2020年5月8日 下午2:06:54 + */ +@Setter +@Getter +@Schema(description = "设备录像详情") +public class RecordItem implements Comparable{ + + @Schema(description = "设备编号") + private String deviceId; + + @Schema(description = "名称") + private String name; + + @Schema(description = "文件路径名 (可选)") + private String filePath; + + @Schema(description = "录像文件大小,单位:Byte(可选)") + private String fileSize; + + @Schema(description = "录像地址(可选)") + private String address; + + @Schema(description = "录像开始时间(可选)") + private String startTime; + + @Schema(description = "录像结束时间(可选)") + private String endTime; + + @Schema(description = "保密属性(必选)缺省为0;0:不涉密,1:涉密") + private int secrecy; + + @Schema(description = "录像产生类型(可选)time或alarm 或 manual") + private String type; + + @Schema(description = "录像触发者ID(可选)") + private String recorderId; + + @Override + public int compareTo(@NotNull RecordItem recordItem) { + TemporalAccessor startTimeNow = DateUtil.formatter.parse(startTime); + TemporalAccessor startTimeParam = DateUtil.formatter.parse(recordItem.getStartTime()); + Instant startTimeParamInstant = Instant.from(startTimeParam); + Instant startTimeNowInstant = Instant.from(startTimeNow); + if (startTimeNowInstant.equals(startTimeParamInstant)) { + return 0; + }else if (Instant.from(startTimeParam).isAfter(Instant.from(startTimeNow)) ) { + return -1; + }else { + return 1; + } + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RedisGroupMessage.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RedisGroupMessage.java new file mode 100644 index 0000000..2691dc1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RedisGroupMessage.java @@ -0,0 +1,67 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +@Data +public class RedisGroupMessage { + + /** + * 分组国标ID + */ + private String groupGbId; + + /** + * 分组别名 + */ + private String groupAlias; + + /** + * 分组名称 + */ + private String groupName; + + /** + * 分组所属的行政区划 + */ + private String groupCivilCode; + + /** + * 分组所属父分组国标ID + */ + private String parentGroupGbId; + + /** + * 分组所属父分组别名 + */ + private String parentGAlias; + + /** + * 分组所属业务分组国标ID + */ + private String topGroupGbId; + + /** + * 分组所属业务分组别名 + */ + private String topGroupGAlias; + + /** + * 分组变化消息中的消息类型,取值为 add update delete + */ + private String messageType; + + + @Override + public String toString() { + return "RedisGroupMessage{" + + "groupGbId='" + groupGbId + '\'' + + ", groupAlias='" + groupAlias + '\'' + + ", groupName='" + groupName + '\'' + + ", groupCivilCode='" + groupCivilCode + '\'' + + ", parentGroupGbId='" + parentGroupGbId + '\'' + + ", parentGAlias='" + parentGAlias + '\'' + + ", topGroupGbId='" + topGroupGbId + '\'' + + ", topGroupGAlias='" + topGroupGAlias + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Region.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Region.java new file mode 100644 index 0000000..bb4e1fd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Region.java @@ -0,0 +1,122 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.common.CivilCodePo; +import com.genersoft.iot.vmp.utils.CivilCodeUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +/** + * 区域 + */ +@Data +@Schema(description = "区域") +public class Region implements Comparable{ + /** + * 数据库自增ID + */ + @Schema(description = "数据库自增ID") + private int id; + + /** + * 区域国标编号 + */ + @Schema(description = "区域国标编号") + private String deviceId; + + /** + * 区域名称 + */ + @Schema(description = "区域名称") + private String name; + + /** + * 父区域国标ID + */ + @Schema(description = "父区域ID") + private Integer parentId; + + /** + * 父区域国标ID + */ + @Schema(description = "父区域国标ID") + private String parentDeviceId; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + private String createTime; + + /** + * 更新时间 + */ + @Schema(description = "更新时间") + private String updateTime; + + public static Region getInstance(String commonRegionDeviceId, String commonRegionName, String commonRegionParentId) { + Region region = new Region(); + region.setDeviceId(commonRegionDeviceId); + region.setName(commonRegionName); + region.setParentDeviceId(commonRegionParentId); + region.setCreateTime(DateUtil.getNow()); + region.setUpdateTime(DateUtil.getNow()); + return region; + } + + public static Region getInstance(CivilCodePo civilCodePo) { + Region region = new Region(); + region.setName(civilCodePo.getName()); + region.setDeviceId(civilCodePo.getCode()); + if (civilCodePo.getCode().length() > 2) { + region.setParentDeviceId(civilCodePo.getParentCode()); + } + region.setCreateTime(DateUtil.getNow()); + region.setUpdateTime(DateUtil.getNow()); + return region; + } + + public static Region getInstance(DeviceChannel channel) { + Region region = new Region(); + region.setName(channel.getName()); + region.setDeviceId(channel.getDeviceId()); + CivilCodePo parentCode = CivilCodeUtil.INSTANCE.getParentCode(channel.getDeviceId()); + if (parentCode != null) { + region.setParentDeviceId(parentCode.getCode()); + } + region.setCreateTime(DateUtil.getNow()); + region.setUpdateTime(DateUtil.getNow()); + return region; + } + + @Override + public int compareTo(@NotNull Region region) { + return Integer.compare(Integer.parseInt(this.deviceId), Integer.parseInt(region.getDeviceId())); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) + return false; + if (this == obj) + return true; + if (obj instanceof Region) { + Region region = (Region) obj; + + // 比较每个属性的值一致时才返回true + if (region.getId() == this.id) { + return true; + } + } + return false; + } + + /** + * 重写hashcode方法,返回的hashCode一样才再去比较每个属性的值 + */ + @Override + public int hashCode() { + return id; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RegionTree.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RegionTree.java new file mode 100644 index 0000000..10ef1ae --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RegionTree.java @@ -0,0 +1,26 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 区域 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Schema(description = "区域树") +public class RegionTree extends Region { + + @Schema(description = "树节点ID") + private String treeId; + + @Schema(description = "是否有子节点") + private boolean isLeaf; + + @Schema(description = "类型, 行政区划:0 摄像头: 1") + private int type; + + @Schema(description = "在线状态") + private String status; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SDPInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SDPInfo.java new file mode 100755 index 0000000..39225b5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SDPInfo.java @@ -0,0 +1,14 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import javax.sdp.SessionDescription; + +public class SDPInfo { + private byte[] source; + private SessionDescription sdpSource; + private String sessionName; + private Long startTime; + private Long stopTime; + private String username; + private String address; + private String ssrc; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SendRtpInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SendRtpInfo.java new file mode 100755 index 0000000..0cfc21c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SendRtpInfo.java @@ -0,0 +1,249 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.RequestPushStreamMsg; +import lombok.Data; + +@Data +public class SendRtpInfo { + + /** + * 推流ip + */ + private String ip; + + /** + * 推流端口 + */ + private int port; + + /** + * 推流标识 + */ + private String ssrc; + + /** + * 目标平台或设备的编号 + */ + private String targetId; + + /** + * 目标平台或设备的名称 + */ + private String targetName; + + /** + * 是否是发送给上级平台 + */ + private boolean sendToPlatform; + + /** + * 直播流的应用名 + */ + private String app; + + /** + * 通道id + */ + private Integer channelId; + + /** + * 推流状态 + * 0 等待设备推流上来 + * 1 等待上级平台回复ack + * 2 推流中 + */ + private int status = 0; + + + /** + * 设备推流的streamId + */ + private String stream; + + /** + * 是否为tcp + */ + private boolean tcp; + + /** + * 是否为tcp主动模式 + */ + private boolean tcpActive; + + /** + * 自己推流使用的IP + */ + private String localIp; + + /** + * 自己推流使用的端口 + */ + private int localPort; + + /** + * 使用的流媒体 + */ + private String mediaServerId; + + /** + * 使用的服务的ID + */ + private String serverId; + + /** + * invite 的 callId + */ + private String callId; + + /** + * invite 的 fromTag + */ + private String fromTag; + + /** + * invite 的 toTag + */ + private String toTag; + + /** + * 发送时,rtp的pt(uint8_t),不传时默认为96 + */ + private int pt = 96; + + /** + * 发送时,rtp的负载类型。为true时,负载为ps;为false时,为es; + */ + private boolean usePs = true; + + /** + * 当usePs 为false时,有效。为1时,发送音频;为0时,发送视频;不传时默认为0 + */ + private boolean onlyAudio = false; + + /** + * 是否开启rtcp保活 + */ + private boolean rtcp = false; + + + /** + * 播放类型 + */ + private InviteStreamType playType; + + /** + * 发流的同时收流 + */ + private String receiveStream; + + /** + * 上级的点播类型 + */ + private String sessionName; + + public static SendRtpInfo getInstance(RequestPushStreamMsg requestPushStreamMsg) { + SendRtpInfo sendRtpItem = new SendRtpInfo(); + sendRtpItem.setMediaServerId(requestPushStreamMsg.getMediaServerId()); + sendRtpItem.setApp(requestPushStreamMsg.getApp()); + sendRtpItem.setStream(requestPushStreamMsg.getStream()); + sendRtpItem.setIp(requestPushStreamMsg.getIp()); + sendRtpItem.setPort(requestPushStreamMsg.getPort()); + sendRtpItem.setSsrc(requestPushStreamMsg.getSsrc()); + sendRtpItem.setTcp(requestPushStreamMsg.isTcp()); + sendRtpItem.setLocalPort(requestPushStreamMsg.getSrcPort()); + sendRtpItem.setPt(requestPushStreamMsg.getPt()); + sendRtpItem.setUsePs(requestPushStreamMsg.isPs()); + sendRtpItem.setOnlyAudio(requestPushStreamMsg.isOnlyAudio()); + return sendRtpItem; + + } + + public static SendRtpInfo getInstance(String app, String stream, String ssrc, String dstIp, Integer dstPort, boolean tcp, int sendLocalPort, Integer pt) { + SendRtpInfo sendRtpItem = new SendRtpInfo(); + sendRtpItem.setApp(app); + sendRtpItem.setStream(stream); + sendRtpItem.setSsrc(ssrc); + sendRtpItem.setTcp(tcp); + sendRtpItem.setLocalPort(sendLocalPort); + sendRtpItem.setIp(dstIp); + sendRtpItem.setPort(dstPort); + if (pt != null) { + sendRtpItem.setPt(pt); + } + + return sendRtpItem; + } + + public static SendRtpInfo getInstance(Integer localPort, MediaServer mediaServer, String ip, Integer port, String ssrc, + String deviceId, String platformId, Integer channelId, Boolean isTcp, Boolean rtcp, + String serverId) { + if (localPort == 0) { + return null; + } + SendRtpInfo sendRtpItem = new SendRtpInfo(); + sendRtpItem.setIp(ip); + if(port != null) { + sendRtpItem.setPort(port); + } + + sendRtpItem.setSsrc(ssrc); + if (deviceId != null) { + sendRtpItem.setTargetId(deviceId); + sendRtpItem.setSendToPlatform(false); + }else { + sendRtpItem.setTargetId(platformId); + sendRtpItem.setSendToPlatform(true); + } + sendRtpItem.setChannelId(channelId); + sendRtpItem.setTcp(isTcp); + sendRtpItem.setRtcp(rtcp); + sendRtpItem.setApp("rtp"); + sendRtpItem.setLocalPort(localPort); + sendRtpItem.setServerId(serverId); + sendRtpItem.setMediaServerId(mediaServer.getId()); + return sendRtpItem; + } + + @Override + public String toString() { + return "SendRtpItem{" + + "ip='" + ip + '\'' + + ", port=" + port + + ", ssrc='" + ssrc + '\'' + + ", targetId='" + targetId + '\'' + + ", app='" + app + '\'' + + ", channelId='" + channelId + '\'' + + ", status=" + status + + ", stream='" + stream + '\'' + + ", tcp=" + tcp + + ", tcpActive=" + tcpActive + + ", localIp='" + localIp + '\'' + + ", localPort=" + localPort + + ", mediaServerId='" + mediaServerId + '\'' + + ", serverId='" + serverId + '\'' + + ", CallId='" + callId + '\'' + + ", fromTag='" + fromTag + '\'' + + ", toTag='" + toTag + '\'' + + ", pt=" + pt + + ", usePs=" + usePs + + ", onlyAudio=" + onlyAudio + + ", rtcp=" + rtcp + + ", playType=" + playType + + ", receiveStream='" + receiveStream + '\'' + + ", sessionName='" + sessionName + '\'' + + '}'; + } + + + public void setPlayTypeByChannelDataType(Integer dataType, String sessionName) { + if (dataType == ChannelDataType.STREAM_PUSH) { + this.setPlayType(InviteStreamType.PUSH); + }else if (dataType == ChannelDataType.STREAM_PROXY){ + this.setPlayType(InviteStreamType.PROXY); + }else { + this.setPlayType("Play".equalsIgnoreCase(sessionName) ? InviteStreamType.PLAY : InviteStreamType.PLAYBACK); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipMsgInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipMsgInfo.java new file mode 100755 index 0000000..06d43c8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipMsgInfo.java @@ -0,0 +1,56 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import org.dom4j.Element; + +import javax.sip.RequestEvent; + +public class SipMsgInfo { + private RequestEvent evt; + private Device device; + private Platform platform; + private Element rootElement; + + public SipMsgInfo(RequestEvent evt, Device device, Element rootElement) { + this.evt = evt; + this.device = device; + this.rootElement = rootElement; + } + + public SipMsgInfo(RequestEvent evt, Platform platform, Element rootElement) { + this.evt = evt; + this.platform = platform; + this.rootElement = rootElement; + } + + public RequestEvent getEvt() { + return evt; + } + + public void setEvt(RequestEvent evt) { + this.evt = evt; + } + + public Device getDevice() { + return device; + } + + public void setDevice(Device device) { + this.device = device; + } + + public Platform getPlatform() { + return platform; + } + + public void setPlatform(Platform platform) { + this.platform = platform; + } + + public Element getRootElement() { + return rootElement; + } + + public void setRootElement(Element rootElement) { + this.rootElement = rootElement; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipSendFailEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipSendFailEvent.java new file mode 100644 index 0000000..2e2c54e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipSendFailEvent.java @@ -0,0 +1,19 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import lombok.Data; + +@Data +public class SipSendFailEvent extends SipSubscribe.EventResult { + + private String callId; + + private String msg; + + public static SipSendFailEvent getInstance(String callId, String msg){ + SipSendFailEvent sipSendFailEvent = new SipSendFailEvent(); + sipSendFailEvent.setMsg(msg); + sipSendFailEvent.setCallId(callId); + return sipSendFailEvent; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipTransactionInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipTransactionInfo.java new file mode 100755 index 0000000..3e2c40b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipTransactionInfo.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import gov.nist.javax.sip.message.SIPResponse; +import lombok.Data; + +import javax.sip.header.EventHeader; + +@Data +public class SipTransactionInfo { + + private String callId; + private String fromTag; + private String toTag; + private String viaBranch; + private int expires; + private String user; + private String eventId; + + // 自己是否媒体流发送者 + private boolean asSender; + + public SipTransactionInfo(SIPResponse response, boolean asSender) { + this.callId = response.getCallIdHeader().getCallId(); + this.fromTag = response.getFromTag(); + this.toTag = response.getToTag(); + this.viaBranch = response.getTopmostViaHeader().getBranch(); + this.asSender = asSender; + EventHeader header = (EventHeader)response.getHeader(EventHeader.NAME); + if (header != null) { + this.eventId = header.getEventId(); + } + } + + public SipTransactionInfo(SIPResponse response) { + this.callId = response.getCallIdHeader().getCallId(); + this.fromTag = response.getFromTag(); + this.toTag = response.getToTag(); + this.viaBranch = response.getTopmostViaHeader().getBranch(); + this.asSender = false; + EventHeader header = (EventHeader)response.getHeader(EventHeader.NAME); + if (header != null) { + this.eventId = header.getEventId(); + } + } + + public SipTransactionInfo() { + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SsrcTransaction.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SsrcTransaction.java new file mode 100755 index 0000000..2a051a7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SsrcTransaction.java @@ -0,0 +1,91 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.Data; + +@Data +public class SsrcTransaction { + + /** + * 设备编号 + */ + private String deviceId; + + /** + * 上级平台的编号 + */ + private String platformId; + + /** + * 通道的数据库ID + */ + private Integer channelId; + + /** + * 会话的CALL ID + */ + private String callId; + + /** + * 关联的流应用名 + */ + private String app; + + /** + * 关联的流ID + */ + private String stream; + + /** + * 使用的流媒体 + */ + private String mediaServerId; + + /** + * 使用的SSRC + */ + private String ssrc; + + /** + * 事务信息 + */ + private SipTransactionInfo sipTransactionInfo; + + /** + * 类型 + */ + private InviteSessionType type; + + public static SsrcTransaction buildForDevice(String deviceId, Integer channelId, String callId, String app, String stream, + String ssrc, String mediaServerId, SIPResponse response, InviteSessionType type) { + SsrcTransaction ssrcTransaction = new SsrcTransaction(); + ssrcTransaction.setDeviceId(deviceId); + ssrcTransaction.setChannelId(channelId); + ssrcTransaction.setCallId(callId); + ssrcTransaction.setApp(app); + ssrcTransaction.setStream(stream); + ssrcTransaction.setMediaServerId(mediaServerId); + ssrcTransaction.setSsrc(ssrc); + ssrcTransaction.setSipTransactionInfo(new SipTransactionInfo(response)); + ssrcTransaction.setType(type); + return ssrcTransaction; + } + public static SsrcTransaction buildForPlatform(String platformId, Integer channelId, String callId, String app,String stream, + String ssrc, String mediaServerId, SIPResponse response, InviteSessionType type) { + SsrcTransaction ssrcTransaction = new SsrcTransaction(); + ssrcTransaction.setPlatformId(platformId); + ssrcTransaction.setChannelId(channelId); + ssrcTransaction.setCallId(callId); + ssrcTransaction.setStream(stream); + ssrcTransaction.setApp(app); + ssrcTransaction.setMediaServerId(mediaServerId); + ssrcTransaction.setSsrc(ssrc); + ssrcTransaction.setSipTransactionInfo(new SipTransactionInfo(response)); + ssrcTransaction.setType(type); + return ssrcTransaction; + } + + public SsrcTransaction() { + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeHolder.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeHolder.java new file mode 100755 index 0000000..17a5284 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeHolder.java @@ -0,0 +1,120 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +/** + * @author lin + */ +@Slf4j +@Component +public class SubscribeHolder { + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + private final String prefix = "VMP_SUBSCRIBE_OVERDUE"; + + public void putCatalogSubscribe(String platformId, SubscribeInfo subscribeInfo) { + log.info("[国标级联] 添加目录订阅,平台: {}, 有效期: {}", platformId, subscribeInfo.getExpires()); + + String key = String.format("%s_%s_%s_%s", prefix, userSetting.getServerId(), "catalog", platformId); + if (subscribeInfo.getExpires() > 0) { + Duration duration = Duration.ofSeconds(subscribeInfo.getExpires()); + redisTemplate.opsForValue().set(key, subscribeInfo, duration); + }else { + redisTemplate.opsForValue().set(key, subscribeInfo); + } + } + + public SubscribeInfo getCatalogSubscribe(String platformId) { + String key = String.format("%s_%s_%s_%s", prefix, userSetting.getServerId(), "catalog", platformId); + return (SubscribeInfo)redisTemplate.opsForValue().get(key); + } + + public void removeCatalogSubscribe(String platformId) { + String key = String.format("%s_%s_%s_%s", prefix, userSetting.getServerId(), "catalog", platformId); + redisTemplate.delete(key); + } + + public void putMobilePositionSubscribe(String platformId, SubscribeInfo subscribeInfo, Runnable gpsTask) { + log.info("[国标级联] 添加移动位置订阅,平台: {}, 有效期: {}s", platformId, subscribeInfo.getExpires()); + String key = String.format("%s_%s_%s_%s", prefix, userSetting.getServerId(), "mobilePosition", platformId); + if (subscribeInfo.getExpires() > 0) { + Duration duration = Duration.ofSeconds(subscribeInfo.getExpires()); + redisTemplate.opsForValue().set(key, subscribeInfo, duration); + }else { + redisTemplate.opsForValue().set(key, subscribeInfo); + } + int cycleForCatalog; + if (subscribeInfo.getGpsInterval() <= 0) { + cycleForCatalog = 5; + }else { + cycleForCatalog = subscribeInfo.getGpsInterval(); + } + dynamicTask.startCron( + key, + () -> { + SubscribeInfo subscribe = getMobilePositionSubscribe(platformId); + if (subscribe != null) { + gpsTask.run(); + }else { + dynamicTask.stop(key); + } + }, + cycleForCatalog * 1000); + + } + + public SubscribeInfo getMobilePositionSubscribe(String platformId) { + String key = String.format("%s_%s_%s_%s", prefix, userSetting.getServerId(), "mobilePosition", platformId); + return (SubscribeInfo)redisTemplate.opsForValue().get(key); + } + + public void removeMobilePositionSubscribe(String platformId) { + String key = String.format("%s_%s_%s_%s", prefix, userSetting.getServerId(), "mobilePosition", platformId); + redisTemplate.delete(key); + } + + public List getAllCatalogSubscribePlatform(List platformList) { + if (platformList == null || platformList.isEmpty()) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + for (Platform platform : platformList) { + String key = String.format("%s_%s_%s_%s", prefix, userSetting.getServerId(), "catalog", platform.getServerGBId()); + if (redisTemplate.hasKey(key)) { + result.add(platform.getServerGBId()); + } + } + return result; + } + + public List getAllMobilePositionSubscribePlatform(List platformList) { + if (platformList == null || platformList.isEmpty()) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + for (Platform platform : platformList) { + String key = String.format("%s_%s_%s_%s", prefix, userSetting.getServerId(), "mobilePosition", platform.getServerGBId()); + if (redisTemplate.hasKey(key)) { + result.add(platform.getServerGBId()); + } + } + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeInfo.java new file mode 100755 index 0000000..5820cb6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeInfo.java @@ -0,0 +1,64 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import gov.nist.javax.sip.message.SIPResponse; +import lombok.Data; + +import javax.sip.header.EventHeader; +import java.util.UUID; + +@Data +public class SubscribeInfo { + + private String id; + private int expires; + private String eventId; + private String eventType; + private SipTransactionInfo transactionInfo; + + /** + * 以下为可选字段 + */ + private String sn; + + private int gpsInterval; + + /** + * 模拟的FromTag + */ + private String simulatedFromTag; + + /** + * 模拟的ToTag + */ + private String simulatedToTag; + + /** + * 模拟的CallID + */ + private String simulatedCallId; + + + public static SubscribeInfo getInstance(SIPResponse response, String id, int expires, EventHeader eventHeader){ + SubscribeInfo subscribeInfo = new SubscribeInfo(); + subscribeInfo.id = id; + subscribeInfo.transactionInfo = new SipTransactionInfo(response); + + subscribeInfo.expires = expires; + subscribeInfo.eventId = eventHeader.getEventId(); + subscribeInfo.eventType = eventHeader.getEventType(); + return subscribeInfo; + } + public static SubscribeInfo buildSimulated(String platFormServerId, String platFormServerIp){ + SubscribeInfo subscribeInfo = new SubscribeInfo(); + subscribeInfo.setId(platFormServerId); + subscribeInfo.setExpires(-1); + subscribeInfo.setEventType("Catalog"); + int random = (int) Math.floor(Math.random() * 10000); + subscribeInfo.setEventId(random + ""); + subscribeInfo.setSimulatedCallId(UUID.randomUUID().toString().replace("-", "") + "@" + platFormServerIp); + subscribeInfo.setSimulatedFromTag(UUID.randomUUID().toString().replace("-", "")); + subscribeInfo.setSimulatedToTag(UUID.randomUUID().toString().replace("-", "")); + return subscribeInfo; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SyncStatus.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SyncStatus.java new file mode 100755 index 0000000..074a7a1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SyncStatus.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.Instant; + +/** + * 摄像机同步状态 + * @author lin + */ +@Data +@Schema(description = "摄像机同步状态") +public class SyncStatus { + + @Schema(description = "总数") + private Integer total; + + @Schema(description = "当前更新多少") + private Integer current; + + @Schema(description = "错误描述") + private String errorMsg; + + @Schema(description = "是否同步中") + private Boolean syncIng; + + @Schema(description = "时间") + private Instant time; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/VectorTileSource.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/VectorTileSource.java new file mode 100644 index 0000000..770a3a5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/VectorTileSource.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +@Getter +@Setter +public class VectorTileSource implements Delayed { + + /** + * 抽稀的图层数据 + */ + private Map vectorTileMap = new ConcurrentHashMap<>(); + + /** + * 抽稀的原始数据 + */ + private List channelList = new ArrayList<>(); + + private String id; + + /** + * 创建时间, 大于6小时后删除 + */ + private long time; + + public VectorTileSource() { + this.time = System.currentTimeMillis(); + } + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(time + 6 * 60 * 60 * 1000 - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/conf/DefaultProperties.java b/src/main/java/com/genersoft/iot/vmp/gb28181/conf/DefaultProperties.java new file mode 100755 index 0000000..73e271d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/conf/DefaultProperties.java @@ -0,0 +1,70 @@ +package com.genersoft.iot.vmp.gb28181.conf; + +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd.AlarmNotifyMessageHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Properties; + +/** + * 获取sip默认配置 + * @author lin + */ +public class DefaultProperties { + + public static Properties getProperties(String name, boolean sipLog, boolean sipCacheServerConnections) { + Properties properties = new Properties(); + properties.setProperty("javax.sip.STACK_NAME", name); +// properties.setProperty("javax.sip.IP_ADDRESS", ip); + // 关闭自动会话 + properties.setProperty("javax.sip.AUTOMATIC_DIALOG_SUPPORT", "off"); + + /** + * 完整配置参考 gov.nist.javax.sip.SipStackImpl,需要下载源码 + * gov/nist/javax/sip/SipStackImpl.class + * sip消息的解析在 gov.nist.javax.sip.stack.UDPMessageChannel的processIncomingDataPacket方法 + */ + + // 接收所有notify请求,即使没有订阅 + properties.setProperty("gov.nist.javax.sip.DELIVER_UNSOLICITED_NOTIFY", "true"); + properties.setProperty("gov.nist.javax.sip.AUTOMATIC_DIALOG_ERROR_HANDLING", "false"); + properties.setProperty("gov.nist.javax.sip.CANCEL_CLIENT_TRANSACTION_CHECKED", "true"); + // 为_NULL _对话框传递_终止的_事件 + properties.setProperty("gov.nist.javax.sip.DELIVER_TERMINATED_EVENT_FOR_NULL_DIALOG", "true"); + // 是否自动计算content length的实际长度,默认不计算 + properties.setProperty("gov.nist.javax.sip.COMPUTE_CONTENT_LENGTH_FROM_MESSAGE_BODY", "true"); + // 会话清理策略 + properties.setProperty("gov.nist.javax.sip.RELEASE_REFERENCES_STRATEGY", "Normal"); + // 处理由该服务器处理的基于底层TCP的保持生存超时 + properties.setProperty("gov.nist.javax.sip.RELIABLE_CONNECTION_KEEP_ALIVE_TIMEOUT", "60"); + // 获取实际内容长度,不使用header中的长度信息 + properties.setProperty("gov.nist.javax.sip.COMPUTE_CONTENT_LENGTH_FROM_MESSAGE_BODY", "true"); + // 线程可重入 + properties.setProperty("gov.nist.javax.sip.REENTRANT_LISTENER", "true"); + // 定义应用程序打算多久审计一次 SIP 堆栈,了解其内部线程的健康状况(该属性指定连续审计之间的时间(以毫秒为单位)) + properties.setProperty("gov.nist.javax.sip.THREAD_AUDIT_INTERVAL_IN_MILLISECS", "30000"); + + // 部分设备会在短时间内发送大量注册, 导致协议栈内存溢出, 开启此项可以防止这部分设备注册, 避免服务崩溃,但是会降低系统性能, 描述如下 + // 默认值为 true。 + // 将此设置为 false 会使 Stack 在 Server Transaction 进入 TERMINATED 状态后关闭服务器套接字。 + // 这允许服务器防止客户端发起的基于 TCP 的拒绝服务攻击(即发起数百个客户端事务)。 + // 如果为 true(默认作),则堆栈将保持套接字打开,以便以牺牲线程和内存资源为代价来最大化性能 - 使自身容易受到 DOS 攻击。 + properties.setProperty("gov.nist.javax.sip.CACHE_SERVER_CONNECTIONS", String.valueOf(sipCacheServerConnections)); + + properties.setProperty("gov.nist.javax.sip.MESSAGE_PROCESSOR_FACTORY", "gov.nist.javax.sip.stack.NioMessageProcessorFactory"); + + /** + * sip_server_log.log 和 sip_debug_log.log ERROR, INFO, WARNING, OFF, DEBUG, TRACE + */ + Logger log = LoggerFactory.getLogger(AlarmNotifyMessageHandler.class); + if (sipLog) { + properties.setProperty("gov.nist.javax.sip.STACK_LOGGER", "com.genersoft.iot.vmp.gb28181.conf.StackLoggerImpl"); + properties.setProperty("gov.nist.javax.sip.SERVER_LOGGER", "com.genersoft.iot.vmp.gb28181.conf.ServerLoggerImpl"); + properties.setProperty("gov.nist.javax.sip.LOG_MESSAGE_CONTENT", "true"); + log.info("[SIP日志]已开启"); + }else { + log.info("[SIP日志]已关闭"); + } + return properties; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/conf/ServerLoggerImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/conf/ServerLoggerImpl.java new file mode 100755 index 0000000..c7b1f6e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/conf/ServerLoggerImpl.java @@ -0,0 +1,91 @@ +package com.genersoft.iot.vmp.gb28181.conf; + +import gov.nist.core.CommonLogger; +import gov.nist.core.ServerLogger; +import gov.nist.core.StackLogger; +import gov.nist.javax.sip.message.SIPMessage; +import gov.nist.javax.sip.stack.SIPTransactionStack; + +import javax.sip.SipStack; +import java.util.Properties; + +public class ServerLoggerImpl implements ServerLogger { + + private boolean showLog = true; + + private SIPTransactionStack sipStack; + + protected StackLogger stackLogger; + + @Override + public void closeLogFile() { + + } + + @Override + public void logMessage(SIPMessage message, String from, String to, boolean sender, long time) { + if (!showLog) { + return; + } + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(sender? "发送:目标--->" + from:"接收:来自--->" + to) + .append("\r\n") + .append(message); + this.stackLogger.logInfo(stringBuilder.toString()); + + } + + @Override + public void logMessage(SIPMessage message, String from, String to, String status, boolean sender, long time) { + if (!showLog) { + return; + } + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(sender? "发送: 目标->" + from :"接收:来自->" + to) + .append("\r\n") + .append(message); + this.stackLogger.logInfo(stringBuilder.toString()); + } + + @Override + public void logMessage(SIPMessage message, String from, String to, String status, boolean sender) { + if (!showLog) { + return; + } + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(sender? "发送: 目标->" + from :"接收:来自->" + to) + .append("\r\n") + .append(message); + this.stackLogger.logInfo(stringBuilder.toString()); + } + + @Override + public void logException(Exception ex) { + if (!showLog) { + return; + } + this.stackLogger.logException(ex); + } + + @Override + public void setStackProperties(Properties stackProperties) { + if (!showLog) { + return; + } + String TRACE_LEVEL = stackProperties.getProperty("gov.nist.javax.sip.TRACE_LEVEL"); + if (TRACE_LEVEL != null) { + showLog = true; + } + } + + @Override + public void setSipStack(SipStack sipStack) { + if (!showLog) { + return; + } + if(sipStack instanceof SIPTransactionStack) { + this.sipStack = (SIPTransactionStack)sipStack; + this.stackLogger = CommonLogger.getLogger(SIPTransactionStack.class); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/conf/StackLoggerImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/conf/StackLoggerImpl.java new file mode 100755 index 0000000..bab0285 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/conf/StackLoggerImpl.java @@ -0,0 +1,141 @@ +package com.genersoft.iot.vmp.gb28181.conf; + +import gov.nist.core.StackLogger; +import org.slf4j.LoggerFactory; +import org.slf4j.spi.LocationAwareLogger; +import org.springframework.stereotype.Component; + +import java.util.Properties; + +@Component +public class StackLoggerImpl implements StackLogger { + + /** + * 完全限定类名(Fully Qualified Class Name),用于定位日志位置 + */ + private static final String FQCN = StackLoggerImpl.class.getName(); + + /** + * 获取栈中类信息(以便底层日志记录系统能够提取正确的位置信息(方法名、行号)) + * @return LocationAwareLogger + */ + private static LocationAwareLogger getLocationAwareLogger() { + return (LocationAwareLogger) LoggerFactory.getLogger(new Throwable().getStackTrace()[4].getClassName()); + } + + + /** + * 封装打印日志的位置信息 + * @param level 日志级别 + * @param message 日志事件的消息 + */ + private static void log(int level, String message) { + LocationAwareLogger locationAwareLogger = getLocationAwareLogger(); + locationAwareLogger.log(null, FQCN, level, message, null, null); + } + + /** + * 封装打印日志的位置信息 + * @param level 日志级别 + * @param message 日志事件的消息 + */ + private static void log(int level, String message, Throwable throwable) { + LocationAwareLogger locationAwareLogger = getLocationAwareLogger(); + locationAwareLogger.log(null, FQCN, level, message, null, throwable); + } + + @Override + public void logStackTrace() { + + } + + @Override + public void logStackTrace(int traceLevel) { + System.out.println("traceLevel: " + traceLevel); + } + + @Override + public int getLineCount() { + return 0; + } + + @Override + public void logException(Throwable ex) { + + } + + @Override + public void logDebug(String message) { + log(LocationAwareLogger.INFO_INT, message); + } + + @Override + public void logDebug(String message, Exception ex) { + log(LocationAwareLogger.INFO_INT, message, ex); + } + + @Override + public void logTrace(String message) { + log(LocationAwareLogger.INFO_INT, message); + } + + @Override + public void logFatalError(String message) { + log(LocationAwareLogger.INFO_INT, message); + } + + @Override + public void logError(String message) { + log(LocationAwareLogger.INFO_INT, message); + } + + @Override + public boolean isLoggingEnabled() { + return true; + } + + @Override + public boolean isLoggingEnabled(int logLevel) { + return true; + } + + @Override + public void logError(String message, Exception ex) { + log(LocationAwareLogger.INFO_INT, message, ex); + } + + @Override + public void logWarning(String message) { + log(LocationAwareLogger.INFO_INT, message); + } + + @Override + public void logInfo(String message) { + log(LocationAwareLogger.INFO_INT, message); + } + + @Override + public void disableLogging() { + + } + + @Override + public void enableLogging() { + + } + + @Override + public void setBuildTimeStamp(String buildTimeStamp) { + + } + + @Override + public void setStackProperties(Properties stackProperties) { + + } + + @Override + public String getLoggerName() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/AlarmController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/AlarmController.java new file mode 100755 index 0000000..c219d96 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/AlarmController.java @@ -0,0 +1,194 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.IDeviceAlarmService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.Arrays; +import java.util.List; + +@Tag(name = "报警信息管理") +@Slf4j +@RestController +@RequestMapping("/api/alarm") +public class AlarmController { + + @Autowired + private IDeviceAlarmService deviceAlarmService; + + @Autowired + private ISIPCommander commander; + + @Autowired + private ISIPCommanderForPlatform commanderForPlatform; + + @Autowired + private IPlatformService platformService; + + @Autowired + private IDeviceService deviceService; + + + /** + * 删除报警 + * + * @param id 报警id + * @param deviceIds 多个设备id,逗号分隔 + * @param time 结束时间(这个时间之前的报警会被删除) + * @return + */ + @DeleteMapping("/delete") + @Operation(summary = "删除报警", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "ID") + @Parameter(name = "deviceIds", description = "多个设备id,逗号分隔") + @Parameter(name = "time", description = "结束时间") + public Integer delete( + @RequestParam(required = false) Integer id, + @RequestParam(required = false) String deviceIds, + @RequestParam(required = false) String time + ) { + if (ObjectUtils.isEmpty(id)) { + id = null; + } + if (ObjectUtils.isEmpty(deviceIds)) { + deviceIds = null; + } + + if (ObjectUtils.isEmpty(time)) { + time = null; + }else if (!DateUtil.verification(time, DateUtil.formatter) ){ + throw new ControllerException(ErrorCode.ERROR400.getCode(), "time格式为" + DateUtil.PATTERN); + } + List deviceIdList = null; + if (deviceIds != null) { + String[] deviceIdArray = deviceIds.split(","); + deviceIdList = Arrays.asList(deviceIdArray); + } + + return deviceAlarmService.clearAlarmBeforeTime(id, deviceIdList, time); + } + + /** + * 测试向上级/设备发送模拟报警通知 + * + * @param deviceId 报警id + * @return + */ + @GetMapping("/test/notify/alarm") + @Operation(summary = "测试向上级/设备发送模拟报警通知", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号") + public void delete(@RequestParam String deviceId) { + Device device = deviceService.getDeviceByDeviceId(deviceId); + Platform platform = platformService.queryPlatformByServerGBId(deviceId); + DeviceAlarm deviceAlarm = new DeviceAlarm(); + deviceAlarm.setChannelId(deviceId); + deviceAlarm.setAlarmDescription("test"); + deviceAlarm.setAlarmMethod("1"); + deviceAlarm.setAlarmPriority("1"); + deviceAlarm.setAlarmTime(DateUtil.getNow()); + deviceAlarm.setAlarmType("1"); + deviceAlarm.setLongitude(115.33333); + deviceAlarm.setLatitude(39.33333); + + if (device != null && platform == null) { + + try { + commander.sendAlarmMessage(device, deviceAlarm); + } catch (InvalidArgumentException | SipException | ParseException e) { + + } + }else if (device == null && platform != null){ + try { + commanderForPlatform.sendAlarmMessage(platform, deviceAlarm); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + }else { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"无法确定" + deviceId + "是平台还是设备"); + } + + } + + /** + * 分页查询报警 + * + * @param deviceId 设备id + * @param page 当前页 + * @param count 每页查询数量 + * @param alarmPriority 报警级别 + * @param alarmMethod 报警方式 + * @param alarmType 报警类型 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return + */ + @Operation(summary = "分页查询报警", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page",description = "当前页",required = true) + @Parameter(name = "count",description = "每页查询数量",required = true) + @Parameter(name = "deviceId",description = "设备id") + @Parameter(name = "channelId",description = "通道id") + @Parameter(name = "alarmPriority",description = "查询内容") + @Parameter(name = "alarmMethod",description = "查询内容") + @Parameter(name = "alarmType",description = "每页查询数量") + @Parameter(name = "startTime",description = "开始时间") + @Parameter(name = "endTime",description = "结束时间") + @GetMapping("/all") + public PageInfo getAll( + @RequestParam int page, + @RequestParam int count, + @RequestParam(required = false) String deviceId, + @RequestParam(required = false) String channelId, + @RequestParam(required = false) String alarmPriority, + @RequestParam(required = false) String alarmMethod, + @RequestParam(required = false) String alarmType, + @RequestParam(required = false) String startTime, + @RequestParam(required = false) String endTime + ) { + if (ObjectUtils.isEmpty(alarmPriority)) { + alarmPriority = null; + } + if (ObjectUtils.isEmpty(alarmMethod)) { + alarmMethod = null; + } + if (ObjectUtils.isEmpty(alarmType)) { + alarmType = null; + } + + if (ObjectUtils.isEmpty(startTime)) { + startTime = null; + }else if (!DateUtil.verification(startTime, DateUtil.formatter) ){ + throw new ControllerException(ErrorCode.ERROR400.getCode(), "startTime格式为" + DateUtil.PATTERN); + } + + if (ObjectUtils.isEmpty(endTime)) { + endTime = null; + }else if (!DateUtil.verification(endTime, DateUtil.formatter) ){ + throw new ControllerException(ErrorCode.ERROR400.getCode(), "endTime格式为" + DateUtil.PATTERN); + } + + return deviceAlarmService.getAllAlarm(page, count, deviceId, channelId, alarmPriority, alarmMethod, + alarmType, startTime, endTime); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/ChannelController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/ChannelController.java new file mode 100755 index 0000000..20a92a8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/ChannelController.java @@ -0,0 +1,593 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.controller.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.utils.VectorTileCatch; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import java.beans.PropertyDescriptor; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.concurrent.TimeUnit; + + +@Tag(name = "全局通道管理") +@RestController +@Slf4j +@RequestMapping(value = "/api/common/channel") +public class ChannelController { + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IGbChannelPlayService channelPlayService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private VectorTileCatch vectorTileCatch; + + + @Operation(summary = "查询通道信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "通道的数据库自增Id", required = true) + @GetMapping(value = "/one") + public CommonGBChannel getOne(int id){ + return channelService.getOne(id); + } + + @Operation(summary = "获取行业编码列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping("/industry/list") + public List getIndustryCodeList(){ + return channelService.getIndustryCodeList(); + } + + @Operation(summary = "获取编码列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping("/type/list") + public List getDeviceTypeList(){ + return channelService.getDeviceTypeList(); + } + + @Operation(summary = "获取编码列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping("/network/identification/list") + public List getNetworkIdentificationTypeList(){ + return channelService.getNetworkIdentificationTypeList(); + } + + @Operation(summary = "更新通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/update") + public void update(@RequestBody CommonGBChannel channel){ + BeanWrapperImpl wrapper = new BeanWrapperImpl(channel); + int count = 0; + for (PropertyDescriptor pd : wrapper.getPropertyDescriptors()) { + String name = pd.getName(); + if ("class".equals(name)) continue; + if (pd.getReadMethod() == null) continue; + Object val = wrapper.getPropertyValue(name); + if (val != null) count++; + } + Assert.isTrue(count > 1, "未进行任何修改"); + channelService.update(channel); + } + + + @Operation(summary = "重置国标通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/reset") + public void reset(@RequestBody ResetParam param){ + Assert.notNull(param.getId(), "通道ID不能为空"); + Assert.notEmpty(param.getChanelFields(), "待重置字段不可以空"); + channelService.reset(param.getId(), param.getChanelFields()); + } + + @Operation(summary = "增加通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/add") + public CommonGBChannel add(@RequestBody CommonGBChannel channel){ + channelService.add(channel); + return channel; + } + + @Operation(summary = "获取通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "hasRecordPlan", description = "是否已设置录制计划") + @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理") + @Parameter(name = "civilCode", description = "行政区划") + @Parameter(name = "parentDeviceId", description = "父节点编码") + @GetMapping("/list") + public PageInfo queryList(int page, int count, + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Boolean hasRecordPlan, + @RequestParam(required = false) Integer channelType, + @RequestParam(required = false) String civilCode, + @RequestParam(required = false) String parentDeviceId){ + if (ObjectUtils.isEmpty(query)){ + query = null; + } + if (ObjectUtils.isEmpty(civilCode)){ + civilCode = null; + } + if (ObjectUtils.isEmpty(parentDeviceId)){ + parentDeviceId = null; + } + return channelService.queryList(page, count, query, online, hasRecordPlan, channelType, civilCode, parentDeviceId); + } + + @Operation(summary = "获取关联行政区划通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理") + @Parameter(name = "civilCode", description = "行政区划") + @GetMapping("/civilcode/list") + public PageInfo queryListByCivilCode(int page, int count, + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Integer channelType, + @RequestParam(required = false) String civilCode){ + if (ObjectUtils.isEmpty(query)){ + query = null; + } + return channelService.queryListByCivilCode(page, count, query, online, channelType, civilCode); + } + + + @Operation(summary = "存在行政区划但无法挂载的通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理") + @GetMapping("/civilCode/unusual/list") + public PageInfo queryListByCivilCodeForUnusual(int page, int count, + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Integer channelType){ + if (ObjectUtils.isEmpty(query)){ + query = null; + } + return channelService.queryListByCivilCodeForUnusual(page, count, query, online, channelType); + } + + + @Operation(summary = "存在父节点编号但无法挂载的通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理") + @GetMapping("/parent/unusual/list") + public PageInfo queryListByParentForUnusual(int page, int count, + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Integer channelType){ + if (ObjectUtils.isEmpty(query)){ + query = null; + } + return channelService.queryListByParentForUnusual(page, count, query, online, channelType); + } + + @Operation(summary = "清除存在行政区划但无法挂载的通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "param", description = "清理参数, all为true清理所有异常数据。 否则按照传入的设备Id清理", required = true) + @PostMapping("/civilCode/unusual/clear") + public void clearChannelCivilCode(@RequestBody ChannelToRegionParam param){ + channelService.clearChannelCivilCode(param.getAll(), param.getChannelIds()); + } + + @Operation(summary = "清除存在分组节点但无法挂载的通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "param", description = "清理参数, all为true清理所有异常数据。 否则按照传入的设备Id清理", required = true) + @PostMapping("/parent/unusual/clear") + public void clearChannelParent(@RequestBody ChannelToRegionParam param){ + channelService.clearChannelParent(param.getAll(), param.getChannelIds()); + } + + @Operation(summary = "获取关联业务分组通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理") + @Parameter(name = "groupDeviceId", description = "业务分组下的父节点ID") + @GetMapping("/parent/list") + public PageInfo queryListByParentId(int page, int count, + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Integer channelType, + @RequestParam(required = false) String groupDeviceId){ + if (ObjectUtils.isEmpty(query)){ + query = null; + } + return channelService.queryListByParentId(page, count, query, online, channelType, groupDeviceId); + } + + @Operation(summary = "通道设置行政区划", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/region/add") + public void addChannelToRegion(@RequestBody ChannelToRegionParam param){ + Assert.notEmpty(param.getChannelIds(),"通道ID不可为空"); + Assert.hasLength(param.getCivilCode(),"未添加行政区划"); + channelService.addChannelToRegion(param.getCivilCode(), param.getChannelIds()); + } + + @Operation(summary = "通道删除行政区划", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/region/delete") + public void deleteChannelToRegion(@RequestBody ChannelToRegionParam param){ + Assert.isTrue(!param.getChannelIds().isEmpty() || !ObjectUtils.isEmpty(param.getCivilCode()),"参数异常"); + channelService.deleteChannelToRegion(param.getCivilCode(), param.getChannelIds()); + } + + @Operation(summary = "通道设置行政区划-根据国标设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/region/device/add") + public void addChannelToRegionByGbDevice(@RequestBody ChannelToRegionByGbDeviceParam param){ + Assert.notEmpty(param.getDeviceIds(),"参数异常"); + Assert.hasLength(param.getCivilCode(),"未添加行政区划"); + channelService.addChannelToRegionByGbDevice(param.getCivilCode(), param.getDeviceIds()); + } + + @Operation(summary = "通道删除行政区划-根据国标设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/region/device/delete") + public void deleteChannelToRegionByGbDevice(@RequestBody ChannelToRegionByGbDeviceParam param){ + Assert.notEmpty(param.getDeviceIds(),"参数异常"); + channelService.deleteChannelToRegionByGbDevice(param.getDeviceIds()); + } + + @Operation(summary = "通道设置业务分组", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/group/add") + public void addChannelToGroup(@RequestBody ChannelToGroupParam param){ + Assert.notEmpty(param.getChannelIds(),"通道ID不可为空"); + Assert.hasLength(param.getParentId(),"未添加上级分组编号"); + Assert.hasLength(param.getBusinessGroup(),"未添加业务分组"); + channelService.addChannelToGroup(param.getParentId(), param.getBusinessGroup(), param.getChannelIds()); + } + + @Operation(summary = "通道删除业务分组", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/group/delete") + public void deleteChannelToGroup(@RequestBody ChannelToGroupParam param){ + Assert.isTrue(!param.getChannelIds().isEmpty() + || (!ObjectUtils.isEmpty(param.getParentId()) && !ObjectUtils.isEmpty(param.getBusinessGroup())), + "参数异常"); + channelService.deleteChannelToGroup(param.getParentId(), param.getBusinessGroup(), param.getChannelIds()); + } + + @Operation(summary = "通道设置业务分组-根据国标设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/group/device/add") + public void addChannelToGroupByGbDevice(@RequestBody ChannelToGroupByGbDeviceParam param){ + Assert.notEmpty(param.getDeviceIds(),"参数异常"); + Assert.hasLength(param.getParentId(),"未添加上级分组编号"); + Assert.hasLength(param.getBusinessGroup(),"未添加业务分组"); + channelService.addChannelToGroupByGbDevice(param.getParentId(), param.getBusinessGroup(), param.getDeviceIds()); + } + + @Operation(summary = "通道删除业务分组-根据国标设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/group/device/delete") + public void deleteChannelToGroupByGbDevice(@RequestBody ChannelToGroupByGbDeviceParam param){ + Assert.notEmpty(param.getDeviceIds(),"参数异常"); + channelService.deleteChannelToGroupByGbDevice(param.getDeviceIds()); + } + + @Operation(summary = "播放通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping("/play") + public DeferredResult> play(HttpServletRequest request, Integer channelId){ + Assert.notNull(channelId,"参数异常"); + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + + ErrorCallback callback = (code, msg, streamInfo) -> { + if (code == InviteErrorCode.SUCCESS.getCode()) { + WVPResult wvpResult = WVPResult.success(); + if (streamInfo != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + if (!ObjectUtils.isEmpty(streamInfo.getMediaServer().getTranscodeSuffix()) + && !"null".equalsIgnoreCase(streamInfo.getMediaServer().getTranscodeSuffix())) { + streamInfo.setStream(streamInfo.getStream() + "_" + streamInfo.getMediaServer().getTranscodeSuffix()); + } + wvpResult.setData(new StreamContent(streamInfo)); + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + result.setResult(wvpResult); + }else { + result.setResult(WVPResult.fail(code, msg)); + } + }; + channelPlayService.play(channel, null, userSetting.getRecordSip(), callback); + return result; + } + + @Operation(summary = "停止播放通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping("/play/stop") + public void stopPlay(Integer channelId){ + Assert.notNull(channelId,"参数异常"); + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + channelPlayService.stopPlay(channel); + } + + @Operation(summary = "录像查询", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道ID", required = true) + @Parameter(name = "startTime", description = "开始时间", required = true) + @Parameter(name = "endTime", description = "结束时间", required = true) + @GetMapping("/playback/query") + public DeferredResult>> queryRecord(Integer channelId, String startTime, String endTime){ + + DeferredResult>> result = new DeferredResult<>(Long.valueOf(userSetting.getRecordInfoTimeout()), TimeUnit.MILLISECONDS); + if (!DateUtil.verification(startTime, DateUtil.formatter)){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), "startTime格式为" + DateUtil.PATTERN); + } + if (!DateUtil.verification(endTime, DateUtil.formatter)){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), "endTime格式为" + DateUtil.PATTERN); + } + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + channelPlayService.queryRecord(channel, startTime, endTime, (code, msg, data) -> { + WVPResult> wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }); + result.onTimeout(()->{ + WVPResult> wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("timeout"); + result.setResult(wvpResult); + }); + return result; + } + + @Operation(summary = "录像回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道ID", required = true) + @Parameter(name = "startTime", description = "开始时间", required = true) + @Parameter(name = "endTime", description = "结束时间", required = true) + @GetMapping("/playback") + public DeferredResult> playback(HttpServletRequest request, Integer channelId, String startTime, String endTime){ + Assert.notNull(channelId,"参数异常"); + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + + ErrorCallback callback = (code, msg, streamInfo) -> { + if (code == InviteErrorCode.SUCCESS.getCode()) { + WVPResult wvpResult = WVPResult.success(); + if (streamInfo != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + if (!ObjectUtils.isEmpty(streamInfo.getMediaServer().getTranscodeSuffix()) + && !"null".equalsIgnoreCase(streamInfo.getMediaServer().getTranscodeSuffix())) { + streamInfo.setStream(streamInfo.getStream() + "_" + streamInfo.getMediaServer().getTranscodeSuffix()); + } + wvpResult.setData(new StreamContent(streamInfo)); + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + + result.setResult(wvpResult); + }else { + result.setResult(WVPResult.fail(code, msg)); + } + }; + channelPlayService.playback(channel, DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime), + DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime), callback); + return result; + } + + @Operation(summary = "停止录像回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道ID", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @GetMapping("/playback/stop") + public void stopPlayback(Integer channelId, String stream){ + Assert.notNull(channelId,"参数异常"); + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + channelPlayService.stopPlayback(channel, stream); + } + + @Operation(summary = "暂停录像回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道ID", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @GetMapping("/playback/pause") + public void pausePlayback(Integer channelId, String stream){ + Assert.notNull(channelId,"参数异常"); + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + channelPlayService.playbackPause(channel, stream); + } + + @Operation(summary = "恢复录像回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道ID", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @GetMapping("/playback/resume") + public void resumePlayback(Integer channelId, String stream){ + Assert.notNull(channelId,"参数异常"); + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + channelPlayService.playbackResume(channel, stream); + } + + @Operation(summary = "拖动录像回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道ID", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @Parameter(name = "seekTime", description = "将要播放的时间", required = true) + @GetMapping("/playback/seek") + public void seekPlayback(Integer channelId, String stream, Long seekTime){ + Assert.notNull(channelId,"参数异常"); + Assert.notNull(seekTime,"参数异常"); + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + channelPlayService.playbackSeek(channel, stream, seekTime); + } + + @Operation(summary = "拖动录像回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道ID", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @Parameter(name = "speed", description = "倍速", required = true) + @GetMapping("/playback/speed") + public void seekPlayback(Integer channelId, String stream, Double speed){ + Assert.notNull(channelId,"参数异常"); + Assert.notNull(speed,"参数异常"); + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + channelPlayService.playbackSpeed(channel, stream, speed); + } + + @Operation(summary = "为地图获取通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "hasRecordPlan", description = "是否已设置录制计划") + @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理") + @Parameter(name = "geoCoordSys", description = "地理坐标系, WGS84/GCJ02") + @GetMapping("/map/list") + public List queryListForMap( + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Boolean hasRecordPlan, + @RequestParam(required = false) Integer channelType){ + if (ObjectUtils.isEmpty(query)){ + query = null; + } + return channelService.queryListForMap(query, online, hasRecordPlan, channelType); + } + + @Operation(summary = "为地图去除抽稀结果", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/map/reset-level") + public void resetLevel(){ + channelService.resetLevel(); + } + + @Operation(summary = "执行抽稀", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/map/thin/draw") + public String drawThin(@RequestBody DrawThinParam param){ + if(param == null || param.getZoomParam() == null || param.getZoomParam().isEmpty()) { + throw new ControllerException(ErrorCode.ERROR400); + } + return channelService.drawThin(param.getZoomParam(), param.getExtent(), param.getGeoCoordSys()); + } + + @Operation(summary = "清除未保存的抽稀结果", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "抽稀ID", required = true) + @GetMapping("/map/thin/clear") + public void clearThin(String id){ + vectorTileCatch.remove(id); + } + + @Operation(summary = "保存的抽稀结果", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "抽稀ID", required = true) + @GetMapping("/map/thin/save") + public void saveThin(String id){ + channelService.saveThin(id); + } + + @Operation(summary = "获取抽稀执行的进度", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "抽稀ID", required = true) + @GetMapping("/map/thin/progress") + public DrawThinProcess thinProgress(String id){ + return channelService.thinProgress(id); + } + + @Operation(summary = "为地图提供标准mvt图层", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping(value = "/map/tile/{z}/{x}/{y}", produces = "application/x-protobuf") + @Parameter(name = "geoCoordSys", description = "地理坐标系, WGS84/GCJ02") + public ResponseEntity getTile(@PathVariable int z, @PathVariable int x, @PathVariable int y, String geoCoordSys){ + + try { + byte[] mvt = channelService.getTile(z, x, y, geoCoordSys); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType("application/x-protobuf")); + if (mvt == null) { + headers.setContentLength(0); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); + } + headers.setContentLength(mvt.length); + return new ResponseEntity<>(mvt, headers, HttpStatus.OK); + } catch (Exception e) { + log.error("构建矢量瓦片失败: z: {}, x: {}, y:{}", z, x, y, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); + } + } + + @Operation(summary = "为地图提供经过抽稀的标准mvt图层", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping(value = "/map/thin/tile/{z}/{x}/{y}", produces = "application/x-protobuf") + @Parameter(name = "geoCoordSys", description = "地理坐标系, WGS84/GCJ02") + @Parameter(name = "thinId", description = "抽稀结果ID") + public ResponseEntity getThinTile(@PathVariable int z, @PathVariable int x, @PathVariable int y, + String geoCoordSys, @RequestParam(required = false) String thinId){ + + if (ObjectUtils.isEmpty(thinId)) { + thinId = "DEFAULT"; + } + String catchKey = z + "_" + x + "_" + y + "_" + geoCoordSys.toUpperCase(); + byte[] mvt = vectorTileCatch.getVectorTile(thinId, catchKey); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType("application/x-protobuf")); + if (mvt == null) { + headers.setContentLength(0); + return ResponseEntity.status(HttpStatus.OK).body(null); + } + + headers.setContentLength(mvt.length); + return new ResponseEntity<>(mvt, headers, HttpStatus.OK); + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/ChannelFrontEndController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/ChannelFrontEndController.java new file mode 100755 index 0000000..8659cf2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/ChannelFrontEndController.java @@ -0,0 +1,601 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelControlService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +import java.util.List; + + +@Tag(name = "全局通道前端控制") +@RestController +@Slf4j +@RequestMapping(value = "/api/common/channel/front-end") +public class ChannelFrontEndController { + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IGbChannelControlService channelControlService; + + + @Operation(summary = "云台控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道ID", required = true) + @Parameter(name = "command", description = "控制指令,允许值: left, right, up, down, upleft, upright, downleft, downright, zoomin, zoomout, stop", required = true) + @Parameter(name = "panSpeed", description = "水平速度(0-100)", required = true) + @Parameter(name = "tiltSpeed", description = "垂直速度(0-100)", required = true) + @Parameter(name = "zoomSpeed", description = "缩放速度(0-100)", required = true) + @GetMapping("/ptz") + public DeferredResult> ptz(Integer channelId, String command, Integer panSpeed, Integer tiltSpeed, Integer zoomSpeed){ + + if (log.isDebugEnabled()) { + log.debug("[通用通道]云台控制 API调用,channelId:{} ,command:{} ,panSpeed:{} ,tiltSpeed:{} ,zoomSpeed:{}",channelId, command, panSpeed, tiltSpeed, zoomSpeed); + } + + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + if (panSpeed == null) { + panSpeed = 50; + }else if (panSpeed < 0 || panSpeed > 100) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "panSpeed 为 0-100的数字"); + } + if (tiltSpeed == null) { + tiltSpeed = 50; + }else if (tiltSpeed < 0 || tiltSpeed > 100) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "tiltSpeed 为 0-100的数字"); + } + if (zoomSpeed == null) { + zoomSpeed = 50; + }else if (zoomSpeed < 0 || zoomSpeed > 100) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "zoomSpeed 为 0-100的数字"); + } + + FrontEndControlCodeForPTZ controlCode = new FrontEndControlCodeForPTZ(); + controlCode.setPanSpeed(panSpeed); + controlCode.setTiltSpeed(tiltSpeed); + controlCode.setZoomSpeed(zoomSpeed); + switch (command){ + case "left": + controlCode.setPan(0); + break; + case "right": + controlCode.setPan(1); + break; + case "up": + controlCode.setTilt(0); + break; + case "down": + controlCode.setTilt(1); + break; + case "upleft": + controlCode.setPan(0); + controlCode.setTilt(0); + break; + case "upright": + controlCode.setTilt(0); + controlCode.setPan(1); + break; + case "downleft": + controlCode.setPan(0); + controlCode.setTilt(1); + break; + case "downright": + controlCode.setTilt(1); + controlCode.setPan(1); + break; + case "zoomin": + controlCode.setZoom(1); + break; + case "zoomout": + controlCode.setZoom(0); + break; + default: + break; + } + + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + channelControlService.ptz(channel, controlCode, (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }); + return result; + } + + + @Operation(summary = "光圈控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: in, out, stop", required = true) + @Parameter(name = "speed", description = "光圈速度(0-100)", required = true) + @GetMapping("/fi/iris") + public DeferredResult> iris(Integer channelId, String command, Integer speed){ + + if (log.isDebugEnabled()) { + log.debug("[通用通道]光圈控制 API调用,channelId:{} ,command:{} ,speed:{} ",channelId, command, speed); + } + + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + if (speed == null) { + speed = 50; + }else if (speed < 0 || speed > 100) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "speed 为 0-100的数字"); + } + + FrontEndControlCodeForFI controlCode = new FrontEndControlCodeForFI(); + controlCode.setIrisSpeed(speed); + + switch (command){ + case "in": + controlCode.setIris(1); + break; + case "out": + controlCode.setIris(0); + break; + default: + break; + } + + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + ErrorCallback callback = (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }; + + channelControlService.fi(channel, controlCode, callback); + + return result; + } + + @Operation(summary = "聚焦控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: near, far, stop", required = true) + @Parameter(name = "speed", description = "聚焦速度(0-100)", required = true) + @GetMapping("/fi/focus") + public DeferredResult> focus(Integer channelId, String command, Integer speed){ + + if (log.isDebugEnabled()) { + log.debug("[通用通道]聚焦控制 API调用,channelId:{} ,command:{} ,speed:{} ", channelId, command, speed); + } + + if (speed == null) { + speed = 50; + }else if (speed < 0 || speed > 100) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "speed 为 0-100的数字"); + } + + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + FrontEndControlCodeForFI controlCode = new FrontEndControlCodeForFI(); + controlCode.setFocusSpeed(speed); + switch (command){ + case "near": + controlCode.setFocus(0); + break; + case "far": + controlCode.setFocus(1); + break; + default: + break; + } + + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + ErrorCallback callback = (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }; + + channelControlService.fi(channel, controlCode, callback); + return result; + } + + @Operation(summary = "查询预置位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @GetMapping("/preset/query") + public DeferredResult>> queryPreset(Integer channelId) { + if (log.isDebugEnabled()) { + log.debug("[通用通道] 预置位查询API调用, {}", channelId); + } + + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + DeferredResult>> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult> wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + ErrorCallback> callback = (code, msg, data) -> { + WVPResult> wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }; + + channelControlService.queryPreset(channel, callback); + + return result; + } + + private DeferredResult> controlPreset(Integer channelId, FrontEndControlCodeForPreset controlCode) { + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + ErrorCallback callback = (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }; + + channelControlService.preset(channel, controlCode, callback); + return result; + } + + @Operation(summary = "预置位指令-设置预置位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "presetId", description = "预置位编号", required = true) + @Parameter(name = "presetName", description = "预置位名称", required = true) + @GetMapping("/preset/add") + public DeferredResult> addPreset(Integer channelId, Integer presetId, String presetName) { + FrontEndControlCodeForPreset controlCode = new FrontEndControlCodeForPreset(); + controlCode.setCode(1); + controlCode.setPresetId(presetId); + controlCode.setPresetName(presetName); + + return controlPreset(channelId, controlCode); + } + + @Operation(summary = "预置位指令-调用预置位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "presetId", description = "预置位编号(1-100)", required = true) + @GetMapping("/preset/call") + public DeferredResult> callPreset(Integer channelId, Integer presetId) { + FrontEndControlCodeForPreset controlCode = new FrontEndControlCodeForPreset(); + controlCode.setCode(2); + controlCode.setPresetId(presetId); + + return controlPreset(channelId, controlCode); + } + + @Operation(summary = "预置位指令-删除预置位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "presetId", description = "预置位编号(1-100)", required = true) + @GetMapping("/preset/delete") + public DeferredResult> deletePreset(Integer channelId, Integer presetId) { + + FrontEndControlCodeForPreset controlCode = new FrontEndControlCodeForPreset(); + controlCode.setCode(3); + controlCode.setPresetId(presetId); + + return controlPreset(channelId, controlCode); + } + + private DeferredResult> tourControl(Integer channelId, FrontEndControlCodeForTour controlCode) { + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + ErrorCallback callback = (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }; + + channelControlService.tour(channel, controlCode, callback); + return result; + } + + @Operation(summary = "巡航指令-加入巡航点", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "tourId", description = "巡航组号", required = true) + @Parameter(name = "presetId", description = "预置位编号", required = true) + @GetMapping("/tour/point/add") + public DeferredResult> addTourPoint(Integer channelId, Integer tourId, Integer presetId) { + + FrontEndControlCodeForTour controlCode = new FrontEndControlCodeForTour(); + controlCode.setCode(1); + controlCode.setPresetId(presetId); + controlCode.setTourId(tourId); + + return tourControl(channelId, controlCode); + } + + @Operation(summary = "巡航指令-删除一个巡航点", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "tourId", description = "巡航组号(1-100)", required = true) + @Parameter(name = "presetId", description = "预置位编号(0-100, 为0时删除整个巡航)", required = true) + @GetMapping("/tour/point/delete") + public DeferredResult> deleteCruisePoint(Integer channelId, Integer tourId, Integer presetId) { + FrontEndControlCodeForTour controlCode = new FrontEndControlCodeForTour(); + controlCode.setCode(2); + controlCode.setPresetId(presetId); + controlCode.setTourId(tourId); + + return tourControl(channelId, controlCode); + } + + @Operation(summary = "巡航指令-设置巡航速度", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "tourId", description = "巡航组号(0-100)", required = true) + @Parameter(name = "speed", description = "巡航速度(1-4095)", required = true) + @Parameter(name = "presetId", description = "预置位编号", required = true) + @GetMapping("/tour/speed") + public DeferredResult> setCruiseSpeed(Integer channelId, Integer tourId, Integer speed, Integer presetId) { + FrontEndControlCodeForTour controlCode = new FrontEndControlCodeForTour(); + controlCode.setCode(3); + controlCode.setTourSpeed(speed); + controlCode.setTourId(tourId); + controlCode.setPresetId(presetId); + return tourControl(channelId, controlCode); + } + + @Operation(summary = "巡航指令-设置巡航停留时间", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "tourId", description = "巡航组号", required = true) + @Parameter(name = "time", description = "巡航停留时间(1-4095)", required = true) + @Parameter(name = "presetId", description = "预置位编号", required = true) + @GetMapping("/tour/time") + public DeferredResult> setCruiseTime(Integer channelId, Integer tourId, Integer time, Integer presetId) { + FrontEndControlCodeForTour controlCode = new FrontEndControlCodeForTour(); + controlCode.setCode(4); + controlCode.setTourTime(time); + controlCode.setTourId(tourId); + controlCode.setPresetId(presetId); + return tourControl(channelId, controlCode); + } + + @Operation(summary = "巡航指令-开始巡航", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "tourId", description = "巡航组号)", required = true) + @GetMapping("/tour/start") + public DeferredResult> startCruise(Integer channelId, Integer tourId) { + FrontEndControlCodeForTour controlCode = new FrontEndControlCodeForTour(); + controlCode.setCode(5); + controlCode.setTourId(tourId); + return tourControl(channelId, controlCode); + } + + @Operation(summary = "巡航指令-停止巡航", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "tourId", description = "巡航组号", required = true) + @GetMapping("/tour/stop") + public DeferredResult> stopCruise(Integer channelId, Integer tourId) { + FrontEndControlCodeForTour controlCode = new FrontEndControlCodeForTour(); + controlCode.setCode(6); + controlCode.setTourId(tourId); + return tourControl(channelId, controlCode); + } + + private DeferredResult> scanControl(Integer channelId, FrontEndControlCodeForScan controlCode) { + + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + ErrorCallback callback = (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }; + channelControlService.scan(channel, controlCode, callback); + + return result; + + } + + @Operation(summary = "扫描指令-开始自动扫描", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-100)", required = true) + @GetMapping("/scan/start") + public DeferredResult> startScan(Integer channelId, Integer scanId) { + FrontEndControlCodeForScan controlCode = new FrontEndControlCodeForScan(); + controlCode.setCode(1); + controlCode.setScanId(scanId); + return scanControl(channelId, controlCode); + + } + + @Operation(summary = "扫描指令-停止自动扫描", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-100)", required = true) + @GetMapping("/scan/stop") + public DeferredResult> stopScan(Integer channelId, Integer scanId) { + FrontEndControlCodeForScan controlCode = new FrontEndControlCodeForScan(); + controlCode.setCode(5); + controlCode.setScanId(scanId); + return scanControl(channelId, controlCode); + } + + @Operation(summary = "扫描指令-设置自动扫描左边界", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-100)", required = true) + @GetMapping("/scan/set/left") + public DeferredResult> setScanLeft(Integer channelId, Integer scanId) { + FrontEndControlCodeForScan controlCode = new FrontEndControlCodeForScan(); + controlCode.setCode(2); + controlCode.setScanId(scanId); + return scanControl(channelId, controlCode); + } + + @Operation(summary = "扫描指令-设置自动扫描右边界", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-100)", required = true) + @GetMapping("/scan/set/right") + public DeferredResult> setScanRight(Integer channelId, Integer scanId) { + FrontEndControlCodeForScan controlCode = new FrontEndControlCodeForScan(); + controlCode.setCode(3); + controlCode.setScanId(scanId); + return scanControl(channelId, controlCode); + } + + + @Operation(summary = "扫描指令-设置自动扫描速度", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-100)", required = true) + @Parameter(name = "speed", description = "自动扫描速度(1-4095)", required = true) + @GetMapping("/scan/set/speed") + public DeferredResult> setScanSpeed(Integer channelId, Integer scanId, Integer speed) { + FrontEndControlCodeForScan controlCode = new FrontEndControlCodeForScan(); + controlCode.setCode(4); + controlCode.setScanId(scanId); + controlCode.setScanSpeed(speed); + return scanControl(channelId, controlCode); + } + + + @Operation(summary = "辅助开关控制指令-雨刷控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: on, off", required = true) + @GetMapping("/wiper") + public DeferredResult> wiper(Integer channelId, String command){ + + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + FrontEndControlCodeForWiper controlCode = new FrontEndControlCodeForWiper(); + + switch (command){ + case "on": + controlCode.setCode(1); + break; + case "off": + controlCode.setCode(2); + break; + default: + break; + } + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + ErrorCallback callback = (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }; + + channelControlService.wiper(channel, controlCode, callback); + + return result; + } + + @Operation(summary = "辅助开关控制指令", security = @SecurityRequirement(name = JwtUtils.HEADER)) + + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: on, off", required = true) + @Parameter(name = "auxiliaryId", description = "开关编号", required = true) + @GetMapping("/auxiliary") + public DeferredResult> auxiliarySwitch(Integer channelId, String command, Integer auxiliaryId){ + + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + FrontEndControlCodeForAuxiliary controlCode = new FrontEndControlCodeForAuxiliary(); + controlCode.setAuxiliaryId(auxiliaryId); + switch (command){ + case "on": + controlCode.setCode(1); + break; + case "off": + controlCode.setCode(2); + break; + default: + break; + } + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + ErrorCallback callback = (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }; + channelControlService.auxiliary(channel, controlCode, callback); + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceConfig.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceConfig.java new file mode 100755 index 0000000..f2402cf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceConfig.java @@ -0,0 +1,91 @@ +/** + * 设备设置命令API接口 + * + * @author lawrencehj + * @date 2021年2月2日 + */ + +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.BasicParam; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +@Slf4j +@Tag(name = "国标设备配置") +@RestController +@RequestMapping("/api/device/config") +public class DeviceConfig { + + @Autowired + private IDeviceService deviceService; + + @GetMapping("/basicParam") + @Operation(summary = "基本配置设置命令", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "basicParam", description = "基础配置参数", required = true) + public DeferredResult> homePositionApi(BasicParam basicParam) { + if (log.isDebugEnabled()) { + log.debug("基本配置设置命令API调用"); + } + Assert.notNull(basicParam.getDeviceId(), "设备ID必须存在"); + + Device device = deviceService.getDeviceByDeviceId(basicParam.getDeviceId()); + Assert.notNull(device, "设备不存在"); + + DeferredResult> deferredResult = new DeferredResult<>(); + deviceService.deviceBasicConfig(device, basicParam, (code, msg, data) -> { + deferredResult.setResult(new WVPResult<>(code, msg, data)); + }); + + deferredResult.onTimeout(() -> { + log.warn("[设备配置] 超时, {}", device.getDeviceId()); + deferredResult.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "超时")); + }); + return deferredResult; + + } + + @Operation(summary = "设备配置查询", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "configType", description = "配置类型, 可选值," + + "基本参数配置:BasicParam," + + "视频参数范围:VideoParamOpt, " + + "SVAC编码配置:SVACEncodeConfig, " + + "SVAC解码配置:SVACDecodeConfig。" + + "可同时查询多个配置类型,各类型以“/”分隔,") + @GetMapping("/query") + public DeferredResult> configDownloadApi(String deviceId,String configType, + @RequestParam(required = false) String channelId) { + if (log.isDebugEnabled()) { + log.debug("设备配置查询请求API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + + DeferredResult> deferredResult = new DeferredResult<>(); + + deviceService.deviceConfigQuery(device, channelId, configType, (code, msg, data) -> { + deferredResult.setResult(new WVPResult<>(code, msg, data)); + }); + + deferredResult.onTimeout(() -> { + log.warn("[获取设备配置] 超时, {}", device.getDeviceId()); + deferredResult.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "超时")); + }); + return deferredResult; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceControl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceControl.java new file mode 100755 index 0000000..6170cb1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceControl.java @@ -0,0 +1,234 @@ +/** + * 设备控制命令API接口 + * + * @author lawrencehj + * @date 2021年2月1日 + */ + +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +@Tag(name = "国标设备控制") +@Slf4j +@RestController +@RequestMapping("/api/device/control") +public class DeviceControl { + + @Autowired + private IDeviceService deviceService; + + + @Operation(summary = "远程启动", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @GetMapping("/teleboot/{deviceId}") + public void teleBootApi(@PathVariable String deviceId) { + if (log.isDebugEnabled()) { + log.debug("设备远程启动API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + deviceService.teleboot(device); + } + + + @Operation(summary = "录像控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "recordCmdStr", description = "命令, 可选值:Record(手动录像),StopRecord(停止手动录像)", required = true) + @GetMapping("/record") + public DeferredResult> recordApi(String deviceId, String recordCmdStr, String channelId) { + if (log.isDebugEnabled()) { + log.debug("开始/停止录像API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> deferredResult = new DeferredResult<>(); + + deviceService.record(device, channelId, recordCmdStr, (code, msg, data) -> { + deferredResult.setResult(new WVPResult<>(code, msg, data)); + }); + deferredResult.onTimeout(() -> { + log.warn("[开始/停止录像] 操作超时, 设备未返回应答指令, {}", deviceId); + deferredResult.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return deferredResult; + } + + @Operation(summary = "布防/撤防", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "guardCmd", description = "命令, 可选值:SetGuard(布防),ResetGuard(撤防)", required = true) + @GetMapping("/guard") + public DeferredResult> guardApi(String deviceId, String guardCmd) { + if (log.isDebugEnabled()) { + log.debug("布防/撤防API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> result = new DeferredResult<>(); + deviceService.guard(device, guardCmd, (code, msg, data) -> { + result.setResult(new WVPResult<>(code, msg, data)); + }); + result.onTimeout(() -> { + log.warn("[布防/撤防] 操作超时, 设备未返回应答指令, {}", deviceId); + result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return result; + } + + @Operation(summary = "报警复位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "alarmMethod", description = "报警方式, 报警方式条件(可选),取值0为全部,1为电话报警,2为设备报警,3为短信报警,4为\n" + + "GPS报警,5为视频报警,6为设备故障报警,7其他报警;可以为直接组合如12为电话报警或设备报警") + @Parameter(name = "alarmType", description = "报警类型, " + + "报警类型。" + + "报警方式为2时,不携带 AlarmType为默认的报警设备报警," + + "携带 AlarmType取值及对应报警类型如下:" + + "1-视频丢失报警;2-设备防拆报警;3-存储设备磁盘满报警;4-设备高温报警;5-设备低温报警。" + + "报警方式为5时,取值如下:" + + "1-人工视频报警;2-运动目标检测报警;3-遗留物检测报警;4-物体移除检测报警;5-绊线检测报警;" + + "6-入侵检测报警;7-逆行检测报警;8-徘徊检测报警;9-流量统计报警;10-密度检测报警;" + + "11-视频异常检测报警;12-快速移动报警。" + + "报警方式为6时,取值如下:" + + "1-存储设备磁盘故障报警;2-存储设备风扇故障报警") + @GetMapping("/reset_alarm") + public DeferredResult> resetAlarm(String deviceId, String channelId, + @RequestParam(required = false) String alarmMethod, + @RequestParam(required = false) String alarmType) { + if (log.isDebugEnabled()) { + log.debug("报警复位API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> result = new DeferredResult<>(); + deviceService.resetAlarm(device, channelId, alarmMethod, alarmType, (code, msg, data) -> { + result.setResult(new WVPResult<>(code, msg, data)); + }); + result.onTimeout(() -> { + log.warn("[布防/撤防] 操作超时, 设备未返回应答指令, {}", deviceId); + result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return result; + } + + @Operation(summary = "强制关键帧", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号") + @GetMapping("/i_frame") + public void iFrame(String deviceId, @RequestParam(required = false) String channelId) { + if (log.isDebugEnabled()) { + log.debug("强制关键帧API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + deviceService.iFrame(device, channelId); + } + + @Operation(summary = "看守位控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "enabled", description = "是否开启看守位", required = true) + @Parameter(name = "presetIndex", description = "调用预置位编号") + @Parameter(name = "resetTime", description = "自动归位时间间隔 单位:秒") + @GetMapping("/home_position") + public DeferredResult> homePositionApi(String deviceId, String channelId, Boolean enabled, + @RequestParam(required = false) Integer resetTime, + @RequestParam(required = false) Integer presetIndex) { + if (log.isDebugEnabled()) { + log.debug("看守位控制API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> result = new DeferredResult<>(); + deviceService.homePosition(device, channelId, enabled, resetTime, presetIndex, (code, msg, data) -> { + result.setResult(new WVPResult<>(code, msg, data)); + }); + result.onTimeout(() -> { + log.warn("[看守位控制] 操作超时, 设备未返回应答指令, {}", deviceId); + result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return result; + } + + @Operation(summary = "拉框放大", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "length", description = "播放窗口长度像素值", required = true) + @Parameter(name = "width", description = "播放窗口宽度像素值", required = true) + @Parameter(name = "midpointx", description = "拉框中心的横轴坐标像素值", required = true) + @Parameter(name = "midpointy", description = "拉框中心的纵轴坐标像素值", required = true) + @Parameter(name = "lengthx", description = "拉框长度像素值", required = true) + @Parameter(name = "lengthy", description = "拉框宽度像素值", required = true) + @GetMapping("drag_zoom/zoom_in") + public DeferredResult> dragZoomIn(@RequestParam String deviceId, String channelId, + @RequestParam int length, + @RequestParam int width, + @RequestParam int midpointx, + @RequestParam int midpointy, + @RequestParam int lengthx, + @RequestParam int lengthy) { + if (log.isDebugEnabled()) { + log.debug(String.format("设备拉框放大 API调用,deviceId:%s ,channelId:%s ,length:%d ,width:%d ,midpointx:%d ,midpointy:%d ,lengthx:%d ,lengthy:%d",deviceId, channelId, length, width, midpointx, midpointy,lengthx, lengthy)); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> result = new DeferredResult<>(); + deviceService.dragZoomIn(device, channelId, length, width, midpointx, midpointy, lengthx,lengthy, (code, msg, data) -> { + result.setResult(new WVPResult<>(code, msg, data)); + }); + result.onTimeout(() -> { + log.warn("[设备拉框放大] 操作超时, 设备未返回应答指令, {}", deviceId); + result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return result; + } + + @Operation(summary = "拉框缩小", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号") + @Parameter(name = "length", description = "播放窗口长度像素值", required = true) + @Parameter(name = "width", description = "拉框中心的横轴坐标像素值", required = true) + @Parameter(name = "midpointx", description = "拉框中心的横轴坐标像素值", required = true) + @Parameter(name = "midpointy", description = "拉框中心的纵轴坐标像素值", required = true) + @Parameter(name = "lengthx", description = "拉框长度像素值", required = true) + @Parameter(name = "lengthy", description = "拉框宽度像素值", required = true) + @GetMapping("/drag_zoom/zoom_out") + public DeferredResult> dragZoomOut(@RequestParam String deviceId, + @RequestParam(required = false) String channelId, + @RequestParam int length, + @RequestParam int width, + @RequestParam int midpointx, + @RequestParam int midpointy, + @RequestParam int lengthx, + @RequestParam int lengthy){ + + if (log.isDebugEnabled()) { + log.debug(String.format("设备拉框缩小 API调用,deviceId:%s ,channelId:%s ,length:%d ,width:%d ,midpointx:%d ,midpointy:%d ,lengthx:%d ,lengthy:%d",deviceId, channelId, length, width, midpointx, midpointy,lengthx, lengthy)); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> result = new DeferredResult<>(); + deviceService.dragZoomOut(device, channelId, length, width, midpointx, midpointy, lengthx,lengthy, (code, msg, data) -> { + result.setResult(new WVPResult<>(code, msg, data)); + }); + result.onTimeout(() -> { + log.warn("[设备拉框放大] 操作超时, 设备未返回应答指令, {}", deviceId); + result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceQuery.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceQuery.java new file mode 100755 index 0000000..6954101 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceQuery.java @@ -0,0 +1,401 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.bean.SyncStatus; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.compress.utils.IOUtils; +import org.apache.ibatis.annotations.Options; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; + +@Tag(name = "国标设备查询", description = "国标设备查询") +@SuppressWarnings("rawtypes") +@Slf4j +@RestController +@RequestMapping("/api/device/query") +public class DeviceQuery { + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private ISIPCommander cmder; + + @Autowired + private DeferredResultHolder resultHolder; + + @Autowired + private UserSetting userSetting; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private IRedisRpcService redisRpcService; + + @Operation(summary = "查询国标设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @GetMapping("/devices/{deviceId}") + public Device devices(@PathVariable String deviceId){ + + return deviceService.getDeviceByDeviceId(deviceId); + } + + + @Operation(summary = "分页查询国标设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "搜索", required = false) + @Parameter(name = "status", description = "状态", required = false) + @GetMapping("/devices") + @Options() + public PageInfo devices(int page, int count, String query, Boolean status){ + if (ObjectUtils.isEmpty(query)){ + query = null; + } + return deviceService.getAll(page, count, query, status); + } + + + @GetMapping("/devices/{deviceId}/channels") + @Operation(summary = "分页查询通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "channelType", description = "设备/子目录-> false/true") + public PageInfo channels(@PathVariable String deviceId, + int page, int count, + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Boolean channelType) { + if (ObjectUtils.isEmpty(query)) { + query = null; + } + + return deviceChannelService.queryChannelsByDeviceId(deviceId, query, channelType, online, page, count); + } + + @GetMapping("/streams") + @Operation(summary = "分页查询存在流的通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + public PageInfo streamChannels(int page, int count, + @RequestParam(required = false) String query) { + if (ObjectUtils.isEmpty(query)) { + query = null; + } + + return deviceChannelService.queryChannels(query, true, null, null, true, page, count); + } + + @Operation(summary = "同步设备通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @GetMapping("/devices/{deviceId}/sync") + public WVPResult devicesSync(@PathVariable String deviceId){ + + if (log.isDebugEnabled()) { + log.debug("设备通道信息同步API调用,deviceId:" + deviceId); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device.getRegisterTime() == null) { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("设备尚未注册过"); + return wvpResult; + } + return deviceService.devicesSync(device); + + } + + @Operation(summary = "移除设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @DeleteMapping("/devices/{deviceId}/delete") + public String delete(@PathVariable String deviceId){ + + if (log.isDebugEnabled()) { + log.debug("设备信息删除API调用,deviceId:" + deviceId); + } + + // 清除redis记录 + deviceService.delete(deviceId); + JSONObject json = new JSONObject(); + json.put("deviceId", deviceId); + return json.toString(); + } + + @Operation(summary = "分页查询子目录通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "channelType", description = "设备/子目录-> false/true") + @GetMapping("/sub_channels/{deviceId}/{channelId}/channels") + public PageInfo subChannels(@PathVariable String deviceId, + @PathVariable String channelId, + int page, + int count, + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Boolean channelType){ + + DeviceChannel deviceChannel = deviceChannelService.getOne(deviceId,channelId); + if (deviceChannel == null) { + PageInfo deviceChannelPageResult = new PageInfo<>(); + return deviceChannelPageResult; + } + + return deviceChannelService.getSubChannels(deviceChannel.getDataDeviceId(), channelId, query, channelType, online, page, count); + } + + @Operation(summary = "开启/关闭通道的音频", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道的数据库ID", required = true) + @Parameter(name = "audio", description = "开启/关闭音频", required = true) + @PostMapping("/channel/audio") + public void changeAudio(Integer channelId, Boolean audio){ + Assert.notNull(channelId, "通道的数据库ID不可为NULL"); + Assert.notNull(audio, "开启/关闭音频不可为NULL"); + deviceChannelService.changeAudio(channelId, audio); + } + + @Operation(summary = "修改通道的码流类型", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/channel/stream/identification/update/") + public void updateChannelStreamIdentification(DeviceChannel channel){ + deviceChannelService.updateChannelStreamIdentification(channel); + } + @Operation(summary = "获取单个通道详情", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备的国标编码", required = true) + @Parameter(name = "channelDeviceId", description = "通道的国标编码", required = true) + @GetMapping("/channel/one") + public DeviceChannel getChannel(String deviceId, String channelDeviceId){ + return deviceChannelService.getOne(deviceId, channelDeviceId); + } + + + @Operation(summary = "修改数据流传输模式", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "streamMode", description = "数据流传输模式, 取值:" + + "UDP(udp传输),TCP-ACTIVE(tcp主动模式),TCP-PASSIVE(tcp被动模式)", required = true) + @PostMapping("/transport/{deviceId}/{streamMode}") + public void updateTransport(@PathVariable String deviceId, @PathVariable String streamMode){ + Assert.isTrue(streamMode.equalsIgnoreCase("UDP") + || streamMode.equalsIgnoreCase("TCP-ACTIVE") + || streamMode.equalsIgnoreCase("TCP-PASSIVE"), "数据流传输模式, 取值:UDP/TCP-ACTIVE/TCP-PASSIVE"); + Device device = deviceService.getDeviceByDeviceId(deviceId); + device.setStreamMode(streamMode.toUpperCase()); + deviceService.updateCustomDevice(device); + } + + + @Operation(summary = "添加设备信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "device", description = "设备", required = true) + @PostMapping("/device/add") + public void addDevice(@RequestBody Device device){ + + if (device == null || device.getDeviceId() == null) { + throw new ControllerException(ErrorCode.ERROR400); + } + + // 查看deviceId是否存在 + boolean exist = deviceService.isExist(device.getDeviceId()); + if (exist) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备编号已存在"); + } + deviceService.addCustomDevice(device); + } + + + @Operation(summary = "更新设备信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "device", description = "设备", required = true) + @PostMapping("/device/update") + public void updateDevice(@RequestBody Device device){ + if (device == null || device.getDeviceId() == null || device.getId() <= 0) { + throw new ControllerException(ErrorCode.ERROR400); + } + deviceService.updateCustomDevice(device); + } + + @Operation(summary = "设备状态查询", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @GetMapping("/devices/{deviceId}/status") + public DeferredResult> deviceStatusApi(@PathVariable String deviceId) { + if (log.isDebugEnabled()) { + log.debug("设备状态查询API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> result = new DeferredResult<>(); + deviceService.deviceStatus(device, (code, msg, data) -> { + result.setResult(new WVPResult<>(code, msg, data)); + }); + result.onTimeout(() -> { + log.warn("[设备状态查询] 操作超时, 设备未返回应答指令, {}", deviceId); + result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return result; + } + + @Operation(summary = "设备报警查询", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "startPriority", description = "报警起始级别, 0为全部,1为一级警情,2为二级警情,3为三级警情,4为四级警情") + @Parameter(name = "endPriority", description = "报警终止级别, ,0为全部,1为一级警情,2为二级警情,3为三级警情,4为四级警情") + @Parameter(name = "alarmMethod", description = "报警方式条件,取值0为全部,1为电话报警,2为设备报警,3为短信报警,4为GPS报警," + + "5为视频报警,6为设备故障报警,7其他报警;可以为直接组合如12为电话报警或设备报警") + @Parameter(name = "alarmType", description = "报警类型") + @Parameter(name = "startTime", description = "报警发生起始时间") + @Parameter(name = "endTime", description = "报警发生终止时间") + @GetMapping("/alarm") + public DeferredResult> alarmApi(String deviceId, + @RequestParam(required = false) String startPriority, + @RequestParam(required = false) String endPriority, + @RequestParam(required = false) String alarmMethod, + @RequestParam(required = false) String alarmType, + @RequestParam(required = false) String startTime, + @RequestParam(required = false) String endTime) { + if (log.isDebugEnabled()) { + log.debug("设备报警查询API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> result = new DeferredResult<>(); + deviceService.alarm(device, startPriority,endPriority ,alarmMethod ,alarmType ,startTime ,endTime, (code, msg, data) -> { + result.setResult(new WVPResult<>(code, msg, data)); + }); + result.onTimeout(() -> { + log.warn("[设备报警查询] 操作超时, 设备未返回应答指令, {}", deviceId); + result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return result; + } + + @Operation(summary = "设备信息查询", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @GetMapping("/info") + public DeferredResult> deviceInfo(String deviceId) { + if (log.isDebugEnabled()) { + log.debug("设备信息查询API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> result = new DeferredResult<>(); + deviceService.deviceInfo(device, (code, msg, data) -> { + result.setResult(new WVPResult<>(code, msg, data)); + }); + result.onTimeout(() -> { + log.warn("[设备信息查询] 操作超时, 设备未返回应答指令, {}", deviceId); + result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return result; + } + + + @GetMapping("/{deviceId}/sync_status") + @Operation(summary = "获取通道同步进度", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + public WVPResult getSyncStatus(@PathVariable String deviceId) { + SyncStatus channelSyncStatus = deviceService.getChannelSyncStatus(deviceId); + WVPResult wvpResult = new WVPResult<>(); + if (channelSyncStatus == null) { + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("同步不存在"); + }else if (channelSyncStatus.getErrorMsg() != null) { + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg(channelSyncStatus.getErrorMsg()); + }else if (channelSyncStatus.getTotal() == null){ + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg("等待通道信息..."); + }else if (channelSyncStatus.getTotal() == 0){ + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); + wvpResult.setData(channelSyncStatus); + }else { + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); + wvpResult.setData(channelSyncStatus); + } + return wvpResult; + } + + @GetMapping("/snap/{deviceId}/{channelId}") + @Operation(summary = "请求截图") + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "mark", description = "标识", required = false) + public void getSnap(HttpServletResponse resp, @PathVariable String deviceId, @PathVariable String channelId, @RequestParam(required = false) String mark) { + + try { + final InputStream in = Files.newInputStream(new File("snap" + File.separator + deviceId + "_" + channelId + (mark == null? ".jpg": ("_" + mark + ".jpg"))).toPath()); + resp.setContentType(MediaType.IMAGE_PNG_VALUE); + ServletOutputStream outputStream = resp.getOutputStream(); + IOUtils.copy(in, resp.getOutputStream()); + in.close(); + outputStream.close(); + } catch (IOException e) { + resp.setStatus(HttpServletResponse.SC_NO_CONTENT); + } + } + + @GetMapping("/channel/raw") + @Operation(summary = "国标通道编辑时的数据回显") + @Parameter(name = "id", description = "通道的Id", required = true) + public DeviceChannel getRawChannel(int id) { + return deviceChannelService.getRawChannel(id); + } + + @GetMapping("/subscribe/catalog") + @Operation(summary = "开启/关闭目录订阅") + @Parameter(name = "id", description = "通道的Id", required = true) + @Parameter(name = "cycle", description = "订阅周期", required = true) + public void subscribeCatalog(int id, int cycle) { + deviceService.subscribeCatalog(id, cycle); + } + + @GetMapping("/subscribe/mobile-position") + @Operation(summary = "开启/关闭移动位置订阅") + @Parameter(name = "id", description = "通道的Id", required = true) + @Parameter(name = "cycle", description = "订阅周期", required = true) + @Parameter(name = "interval", description = "报送间隔", required = true) + public void subscribeMobilePosition(int id, int cycle, int interval) { + deviceService.subscribeMobilePosition(id, cycle, interval); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GBRecordController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GBRecordController.java new file mode 100755 index 0000000..368538f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GBRecordController.java @@ -0,0 +1,214 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.bean.RecordInfo; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@Tag(name = "国标录像") +@Slf4j +@RestController +@RequestMapping("/api/gb_record") +public class GBRecordController { + + @Autowired + private SIPCommander cmder; + + @Autowired + private DeferredResultHolder resultHolder; + + @Autowired + private IPlayService playService; + + @Autowired + private IDeviceChannelService channelService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private UserSetting userSetting; + + @Operation(summary = "录像查询", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "startTime", description = "开始时间", required = true) + @Parameter(name = "endTime", description = "结束时间", required = true) + @GetMapping("/query/{deviceId}/{channelId}") + public DeferredResult> recordinfo(@PathVariable String deviceId, @PathVariable String channelId, String startTime, String endTime){ + + if (log.isDebugEnabled()) { + log.debug(String.format("录像信息查询 API调用,deviceId:%s ,startTime:%s, endTime:%s",deviceId, startTime, endTime)); + } + DeferredResult> result = new DeferredResult<>(Long.valueOf(userSetting.getRecordInfoTimeout()), TimeUnit.MILLISECONDS); + if (!DateUtil.verification(startTime, DateUtil.formatter)){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), "startTime格式为" + DateUtil.PATTERN); + } + if (!DateUtil.verification(endTime, DateUtil.formatter)){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), "endTime格式为" + DateUtil.PATTERN); + } + + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), deviceId + " 不存在"); + } + DeviceChannel channel = channelService.getOneForSource(device.getId(), channelId); + if (channel == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), channelId + " 不存在"); + } + channelService.queryRecordInfo(device, channel, startTime, endTime, (code, msg, data)->{ + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }); + result.onTimeout(()->{ + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("timeout"); + result.setResult(wvpResult); + }); + return result; + } + + + @Operation(summary = "开始历史媒体下载", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "startTime", description = "开始时间", required = true) + @Parameter(name = "endTime", description = "结束时间", required = true) + @Parameter(name = "downloadSpeed", description = "下载倍速", required = true) + @GetMapping("/download/start/{deviceId}/{channelId}") + public DeferredResult> download(HttpServletRequest request, @PathVariable String deviceId, @PathVariable String channelId, + String startTime, String endTime, String downloadSpeed) { + + if (log.isDebugEnabled()) { + log.debug(String.format("历史媒体下载 API调用,deviceId:%s,channelId:%s,downloadSpeed:%s", deviceId, channelId, downloadSpeed)); + } + + String uuid = UUID.randomUUID().toString(); + String key = DeferredResultHolder.CALLBACK_CMD_DOWNLOAD + deviceId + channelId; + DeferredResult> result = new DeferredResult<>(30000L); + resultHolder.put(key, uuid, result); + RequestMessage requestMessage = new RequestMessage(); + requestMessage.setId(uuid); + requestMessage.setKey(key); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device == null) { + log.warn("[开始历史媒体下载] 未找到设备 deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId); + } + + DeviceChannel channel = channelService.getOne(deviceId, channelId); + if (channel == null) { + log.warn("[开始历史媒体下载] 未找到通道 deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道:" + channelId); + } + playService.download(device, channel, startTime, endTime, Integer.parseInt(downloadSpeed), + (code, msg, data)->{ + + WVPResult wvpResult = new WVPResult<>(); + if (code == InviteErrorCode.SUCCESS.getCode()) { + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); + + if (data != null) { + StreamInfo streamInfo = (StreamInfo)data; + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo.changeStreamIp(request.getLocalAddr()); + } + wvpResult.setData(new StreamContent(streamInfo)); + } + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + requestMessage.setData(wvpResult); + resultHolder.invokeResult(requestMessage); + }); + + return result; + } + + @Operation(summary = "停止历史媒体下载", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @GetMapping("/download/stop/{deviceId}/{channelId}/{stream}") + public void downloadStop(@PathVariable String deviceId, @PathVariable String channelId, @PathVariable String stream) { + + if (log.isDebugEnabled()) { + log.debug(String.format("设备历史媒体下载停止 API调用,deviceId/channelId:%s_%s", deviceId, channelId)); + } + + if (deviceId == null || channelId == null) { + throw new ControllerException(ErrorCode.ERROR400); + } + + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "设备:" + deviceId + " 未找到"); + } + DeviceChannel deviceChannel = channelService.getOneForSource(deviceId, channelId); + if (deviceChannel == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "通道:" + channelId + " 未找到"); + } + playService.stop(InviteSessionType.DOWNLOAD, device, deviceChannel, stream); + } + + @Operation(summary = "获取历史媒体下载进度", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @GetMapping("/download/progress/{deviceId}/{channelId}/{stream}") + public StreamContent getProgress(@PathVariable String deviceId, @PathVariable String channelId, @PathVariable String stream) { + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device == null) { + log.warn("[获取历史媒体下载进度] 未找到设备 deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId); + } + + DeviceChannel channel = channelService.getOne(deviceId, channelId); + if (channel == null) { + log.warn("[获取历史媒体下载进度] 未找到通道 deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道:" + channelId); + } + StreamInfo downLoadInfo = playService.getDownLoadInfo(device, channel, stream); + if (downLoadInfo == null) { + throw new ControllerException(ErrorCode.ERROR404); + } + return new StreamContent(downLoadInfo); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GroupController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GroupController.java new file mode 100644 index 0000000..7a4578a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GroupController.java @@ -0,0 +1,116 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.Group; +import com.genersoft.iot.vmp.gb28181.bean.GroupTree; +import com.genersoft.iot.vmp.gb28181.service.IGroupService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Slf4j +@Tag(name = "分组管理") +@RestController +@RequestMapping("/api/group") +public class GroupController { + + @Autowired + private IGroupService groupService; + + @Operation(summary = "添加分组") + @Parameter(name = "group", description = "group", required = true) + @ResponseBody + @PostMapping("/add") + public void add(@RequestBody Group group){ + groupService.add(group); + } + + @Operation(summary = "查询分组节点") + @Parameter(name = "query", description = "要搜索的内容", required = true) + @Parameter(name = "parent", description = "所属分组编号", required = true) + @ResponseBody + @GetMapping("/tree/list") + public List queryForTree( + @RequestParam(required = false) String query, + @RequestParam(required = false) Integer parent, + @RequestParam(required = false) Boolean hasChannel + ){ + if (ObjectUtils.isEmpty(query)) { + query = null; + } + return groupService.queryForTree(query, parent, hasChannel); + } + + @Operation(summary = "查询分组") + @Parameter(name = "query", description = "要搜索的内容", required = true) + @Parameter(name = "channel", description = "true为查询通道,false为查询节点", required = true) + @ResponseBody + @GetMapping("/tree/query") + public PageInfo queryTree(Integer page, Integer count, + @RequestParam(required = true) String query + ){ + return groupService.queryList(page, count, query); + } + + @Operation(summary = "更新分组") + @Parameter(name = "group", description = "Group", required = true) + @ResponseBody + @PostMapping("/update") + public void update(@RequestBody Group group){ + groupService.update(group); + } + + @Operation(summary = "删除分组") + @Parameter(name = "id", description = "分组id", required = true) + @ResponseBody + @DeleteMapping("/delete") + public void delete(Integer id){ + Assert.notNull(id, "分组id(deviceId)不需要存在"); + boolean result = groupService.delete(id); + if (!result) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "移除失败"); + } + } + + @Operation(summary = "获取所属的行政区划下的行政区划") + @Parameter(name = "deviceId", description = "当前的行政区划", required = false) + @ResponseBody + @GetMapping("/path") + public List getPath(String deviceId, String businessGroup){ + return groupService.getPath(deviceId, businessGroup); + } + + @Operation(summary = "从第三方同步组织结构") + @ResponseBody + @GetMapping("/sync") + public void sync(){ + groupService.sync(); + } + +// @Operation(summary = "根据分组Id查询分组") +// @Parameter(name = "groupDeviceId", description = "分组节点编号", required = true) +// @ResponseBody +// @GetMapping("/one") +// public Group queryGroupByDeviceId( +// @RequestParam(required = true) String deviceId +// ){ +// Assert.hasLength(deviceId, ""); +// return groupService.queryGroupByDeviceId(deviceId); +// } + +// @Operation(summary = "从通道中同步分组") +// @ResponseBody +// @GetMapping("/sync") +// public void sync(){ +// groupService.syncFromChannel(); +// } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/MediaController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/MediaController.java new file mode 100755 index 0000000..ef07950 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/MediaController.java @@ -0,0 +1,155 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.conf.security.SecurityUtils; +import com.genersoft.iot.vmp.conf.security.dto.LoginUser; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import java.net.MalformedURLException; +import java.net.URL; + + +@Tag(name = "媒体流相关") +@RestController +@Slf4j +@RequestMapping(value = "/api/media") +public class MediaController { + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IStreamProxyService streamProxyService; + + @Autowired + private IMediaServerService mediaServerService; + + + /** + * 根据应用名和流id获取播放地址 + * @param app 应用名 + * @param stream 流id + * @return + */ + @Operation(summary = "根据应用名和流id获取播放地址", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流id", required = true) + @Parameter(name = "mediaServerId", description = "媒体服务器id") + @Parameter(name = "callId", description = "推流时携带的自定义鉴权ID") + @Parameter(name = "useSourceIpAsStreamIp", description = "是否使用请求IP作为返回的地址IP") + @GetMapping(value = "/stream_info_by_app_and_stream") + @ResponseBody + public DeferredResult> getStreamInfoByAppAndStream(HttpServletRequest request, @RequestParam String app, + @RequestParam String stream, + @RequestParam(required = false) String mediaServerId, + @RequestParam(required = false) String callId, + @RequestParam(required = false) Boolean useSourceIpAsStreamIp){ + DeferredResult> result = new DeferredResult<>(); + boolean authority = false; + if (callId != null) { + // 权限校验 + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(app, stream); + if (streamAuthorityInfo != null + && streamAuthorityInfo.getCallId() != null + && streamAuthorityInfo.getCallId().equals(callId)) { + authority = true; + }else { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "获取播放地址鉴权失败"); + } + }else { + // 是否登陆用户, 登陆用户返回完整信息 + LoginUser userInfo = SecurityUtils.getUserInfo(); + if (userInfo!= null) { + authority = true; + } + } + StreamInfo streamInfo; + if (useSourceIpAsStreamIp != null && useSourceIpAsStreamIp) { + String host = request.getHeader("Host"); + String localAddr = host.split(":")[0]; + log.info("使用{}作为返回流的ip", localAddr); + streamInfo = mediaServerService.getStreamInfoByAppAndStreamWithCheck(app, stream, mediaServerId, localAddr, authority); + }else { + streamInfo = mediaServerService.getStreamInfoByAppAndStreamWithCheck(app, stream, mediaServerId, authority); + } + + if (streamInfo != null){ + WVPResult wvpResult = WVPResult.success(); + wvpResult.setData(new StreamContent(streamInfo)); + result.setResult(wvpResult); + }else { + ErrorCallback callback = (code, msg, streamInfoStoStart) -> { + if (code == InviteErrorCode.SUCCESS.getCode()) { + WVPResult wvpResult = WVPResult.success(); + if (useSourceIpAsStreamIp != null && useSourceIpAsStreamIp) { + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfoStoStart.changeStreamIp(host); + } + if (!ObjectUtils.isEmpty(streamInfoStoStart.getMediaServer().getTranscodeSuffix()) + && !"null".equalsIgnoreCase(streamInfoStoStart.getMediaServer().getTranscodeSuffix())) { + streamInfoStoStart.setStream(streamInfoStoStart.getStream() + "_" + streamInfoStoStart.getMediaServer().getTranscodeSuffix()); + } + wvpResult.setData(new StreamContent(streamInfoStoStart)); + result.setResult(wvpResult); + }else { + result.setResult(WVPResult.fail(code, msg)); + } + }; + //获取流失败,重启拉流后重试一次 + streamProxyService.startByAppAndStream(app, stream, callback); + } + return result; + } + /** + * 获取推流播放地址 + * @param app 应用名 + * @param stream 流id + * @return + */ + @GetMapping(value = "/getPlayUrl") + @ResponseBody + @Operation(summary = "获取推流播放地址", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流id", required = true) + @Parameter(name = "mediaServerId", description = "媒体服务器id") + public StreamContent getPlayUrl(@RequestParam String app, @RequestParam String stream, + @RequestParam(required = false) String mediaServerId){ + boolean authority = false; + // 是否登陆用户, 登陆用户返回完整信息 + LoginUser userInfo = SecurityUtils.getUserInfo(); + if (userInfo!= null) { + authority = true; + } + StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStreamWithCheck(app, stream, mediaServerId, authority); + if (streamInfo == null){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), "获取播放地址失败"); + } + return new StreamContent(streamInfo); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/MobilePositionController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/MobilePositionController.java new file mode 100755 index 0000000..7ef9635 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/MobilePositionController.java @@ -0,0 +1,152 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.service.IMobilePositionService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.util.StringUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.List; +import java.util.UUID; + +/** + * 位置信息管理 + */ +@Tag(name = "位置信息管理") +@Slf4j +@RestController +@RequestMapping("/api/position") +public class MobilePositionController { + + @Autowired + private IMobilePositionService mobilePositionService; + + @Autowired + private ISIPCommander cmder; + + @Autowired + private DeferredResultHolder resultHolder; + + @Autowired + private IDeviceService deviceService; + + + /** + * 查询历史轨迹 + * @param deviceId 设备ID + * @param start 开始时间 + * @param end 结束时间 + * @return + */ + @Operation(summary = "查询历史轨迹", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号") + @Parameter(name = "start", description = "开始时间") + @Parameter(name = "end", description = "结束时间") + @GetMapping("/history/{deviceId}") + public List positions(@PathVariable String deviceId, + @RequestParam(required = false) String channelId, + @RequestParam(required = false) String start, + @RequestParam(required = false) String end) { + + if (StringUtil.isEmpty(start)) { + start = null; + } + if (StringUtil.isEmpty(end)) { + end = null; + } + return mobilePositionService.queryMobilePositions(deviceId, channelId, start, end); + } + + /** + * 查询设备最新位置 + * @param deviceId 设备ID + * @return + */ + @Operation(summary = "查询设备最新位置", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @GetMapping("/latest/{deviceId}") + public MobilePosition latestPosition(@PathVariable String deviceId) { + return mobilePositionService.queryLatestPosition(deviceId); + } + + /** + * 获取移动位置信息 + * @param deviceId 设备ID + * @return + */ + @Operation(summary = "获取移动位置信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @GetMapping("/realtime/{deviceId}") + public DeferredResult realTimePosition(@PathVariable String deviceId) { + Device device = deviceService.getDeviceByDeviceId(deviceId); + String uuid = UUID.randomUUID().toString(); + String key = DeferredResultHolder.CALLBACK_CMD_MOBILE_POSITION + deviceId; + try { + cmder.mobilePostitionQuery(device, event -> { + RequestMessage msg = new RequestMessage(); + msg.setId(uuid); + msg.setKey(key); + msg.setData(String.format("获取移动位置信息失败,错误码: %s, %s", event.statusCode, event.msg)); + resultHolder.invokeResult(msg); + }); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 获取移动位置信息: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + DeferredResult result = new DeferredResult(5*1000L); + result.onTimeout(()->{ + log.warn(String.format("获取移动位置信息超时")); + // 释放rtpserver + RequestMessage msg = new RequestMessage(); + msg.setId(uuid); + msg.setKey(key); + msg.setData("Timeout"); + resultHolder.invokeResult(msg); + }); + resultHolder.put(key, uuid, result); + return result; + } + + /** + * 订阅位置信息 + * @param deviceId 设备ID + * @param expires 订阅超时时间 + * @param interval 上报时间间隔 + * @return true = 命令发送成功 + */ + @Operation(summary = "订阅位置信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "expires", description = "订阅超时时间", required = true) + @Parameter(name = "interval", description = "上报时间间隔", required = true) + @GetMapping("/subscribe/{deviceId}") + public void positionSubscribe(@PathVariable String deviceId, + @RequestParam String expires, + @RequestParam String interval) { + + if (StringUtil.isEmpty(interval)) { + interval = "5"; + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + device.setSubscribeCycleForMobilePosition(Integer.parseInt(expires)); + device.setMobilePositionSubmissionInterval(Integer.parseInt(interval)); + deviceService.updateCustomDevice(device); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlatformController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlatformController.java new file mode 100755 index 0000000..cba750a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlatformController.java @@ -0,0 +1,285 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.PlatformChannel; +import com.genersoft.iot.vmp.gb28181.bean.SubscribeHolder; +import com.genersoft.iot.vmp.gb28181.controller.bean.UpdateChannelParam; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +/** + * 级联平台管理 + */ +@Tag(name = "级联平台管理") +@Slf4j +@RestController +@RequestMapping("/api/platform") +public class PlatformController { + + @Autowired + private IPlatformChannelService platformChannelService; + + @Autowired + private SubscribeHolder subscribeHolder; + + @Autowired + private SipConfig sipConfig; + + @Autowired + private IPlatformService platformService; + + + @Operation(summary = "获取国标服务的配置", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping("/server_config") + public JSONObject serverConfig() { + JSONObject result = new JSONObject(); + result.put("deviceIp", sipConfig.getShowIp()); + result.put("devicePort", sipConfig.getPort()); + result.put("username", sipConfig.getId()); + result.put("password", sipConfig.getPassword()); + return result; + } + + @Operation(summary = "获取级联服务器信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "平台国标编号", required = true) + @GetMapping("/info/{id}") + public Platform getPlatform(@PathVariable String id) { + Platform parentPlatform = platformService.queryPlatformByServerGBId(id); + if (parentPlatform != null) { + return parentPlatform; + } else { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未查询到此平台"); + } + } + + @GetMapping("/query") + @Operation(summary = "分页查询级联平台", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页") + @Parameter(name = "count", description = "每页查询数量") + @Parameter(name = "query", description = "查询内容") + public PageInfo platforms(int page, int count, + @RequestParam(required = false) String query) { + + PageInfo parentPlatformPageInfo = platformService.queryPlatformList(page, count, query); + if (parentPlatformPageInfo != null && !parentPlatformPageInfo.getList().isEmpty()) { + for (Platform platform : parentPlatformPageInfo.getList()) { + platform.setMobilePositionSubscribe(subscribeHolder.getMobilePositionSubscribe(platform.getServerGBId()) != null); + platform.setCatalogSubscribe(subscribeHolder.getCatalogSubscribe(platform.getServerGBId()) != null); + } + } + return parentPlatformPageInfo; + } + + @Operation(summary = "添加上级平台信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/add") + @ResponseBody + public void add(@RequestBody Platform platform) { + + Assert.notNull(platform.getName(), "平台名称不可为空"); + Assert.notNull(platform.getServerGBId(), "上级平台国标编号不可为空"); + Assert.notNull(platform.getServerIp(), "上级平台IP不可为空"); + Assert.isTrue(platform.getServerPort() > 0 && platform.getServerPort() < 65535, "上级平台端口异常"); + Assert.notNull(platform.getDeviceGBId(), "本平台国标编号不可为空"); + + if (ObjectUtils.isEmpty(platform.getServerGBDomain())) { + platform.setServerGBDomain(platform.getServerGBId().substring(0, 6)); + } + + if (platform.getExpires() <= 0) { + platform.setExpires(3600); + } + + if (platform.getKeepTimeout() <= 0) { + platform.setKeepTimeout(60); + } + + if (ObjectUtils.isEmpty(platform.getTransport())) { + platform.setTransport("UDP"); + } + + if (ObjectUtils.isEmpty(platform.getCharacterSet())) { + platform.setCharacterSet("GB2312"); + } + + Platform parentPlatformOld = platformService.queryPlatformByServerGBId(platform.getServerGBId()); + if (parentPlatformOld != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "平台 " + platform.getServerGBId() + " 已存在"); + } + platform.setCreateTime(DateUtil.getNow()); + platform.setUpdateTime(DateUtil.getNow()); + boolean updateResult = platformService.add(platform); + + if (!updateResult) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @Operation(summary = "更新上级平台信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/update") + @ResponseBody + public void updatePlatform(@RequestBody Platform parentPlatform) { + + if (ObjectUtils.isEmpty(parentPlatform.getName()) + || ObjectUtils.isEmpty(parentPlatform.getServerGBId()) + || ObjectUtils.isEmpty(parentPlatform.getServerGBDomain()) + || ObjectUtils.isEmpty(parentPlatform.getServerIp()) + || ObjectUtils.isEmpty(parentPlatform.getServerPort()) + || ObjectUtils.isEmpty(parentPlatform.getDeviceGBId()) + || ObjectUtils.isEmpty(parentPlatform.getExpires()) + || ObjectUtils.isEmpty(parentPlatform.getKeepTimeout()) + || ObjectUtils.isEmpty(parentPlatform.getTransport()) + || ObjectUtils.isEmpty(parentPlatform.getCharacterSet()) + ) { + throw new ControllerException(ErrorCode.ERROR400); + } + platformService.update(parentPlatform); + } + + @Operation(summary = "删除上级平台", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "上级平台ID") + @DeleteMapping("/delete") + @ResponseBody + public DeferredResult> deletePlatform(Integer id) { + + if (log.isDebugEnabled()) { + log.debug("删除上级平台API调用"); + } + DeferredResult> deferredResult = new DeferredResult<>(); + + platformService.delete(id, (object)-> deferredResult.setResult(WVPResult.success())); + return deferredResult; + } + + @Operation(summary = "查询上级平台是否存在", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "serverGBId", description = "上级平台的国标编号") + @GetMapping("/exit/{serverGBId}") + @ResponseBody + public Boolean exitPlatform(@PathVariable String serverGBId) { + Platform platform = platformService.queryPlatformByServerGBId(serverGBId); + return platform != null; + } + + @Operation(summary = "分页查询级联平台的所有所有通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页条数", required = true) + @Parameter(name = "platformId", description = "上级平台的数据ID") + @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理") + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "hasShare", description = "是否已经共享") + @GetMapping("/channel/list") + @ResponseBody + public PageInfo queryChannelList(int page, int count, + @RequestParam(required = false) Integer platformId, + @RequestParam(required = false) String query, + @RequestParam(required = false) Integer channelType, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Boolean hasShare) { + + Assert.notNull(platformId, "上级平台的数据ID不可为NULL"); + if (ObjectUtils.isEmpty(query)) { + query = null; + } + + return platformChannelService.queryChannelList(page, count, query, channelType, online, platformId, hasShare); + } + + @Operation(summary = "向上级平台添加国标通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/channel/add") + @ResponseBody + public void addChannel(@RequestBody UpdateChannelParam param) { + + if (log.isDebugEnabled()) { + log.debug("给上级平台添加国标通道API调用"); + } + int result = 0; + if (param.getChannelIds() == null || param.getChannelIds().isEmpty()) { + if (param.isAll()) { + log.info("[国标级联]添加所有通道到上级平台, {}", param.getPlatformId()); + result = platformChannelService.addAllChannel(param.getPlatformId()); + } + }else { + result = platformChannelService.addChannels(param.getPlatformId(), param.getChannelIds()); + } + if (result <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @Operation(summary = "从上级平台移除国标通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @DeleteMapping("/channel/remove") + @ResponseBody + public void delChannelForGB(@RequestBody UpdateChannelParam param) { + + if (log.isDebugEnabled()) { + log.debug("给上级平台删除国标通道API调用"); + } + int result = 0; + if (param.getChannelIds() == null || param.getChannelIds().isEmpty()) { + if (param.isAll()) { + log.info("[国标级联]移除所有通道,上级平台, {}", param.getPlatformId()); + result = platformChannelService.removeAllChannel(param.getPlatformId()); + } + }else { + result = platformChannelService.removeChannels(param.getPlatformId(), param.getChannelIds()); + } + if (result <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @Operation(summary = "推送通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "平台ID", required = true) + @GetMapping("/channel/push") + @ResponseBody + public void pushChannel(Integer id) { + Assert.notNull(id, "平台ID不可为空"); + platformChannelService.pushChannel(id); + } + + @Operation(summary = "添加通道-通过设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/channel/device/add") + @ResponseBody + public void addChannelByDevice(@RequestBody UpdateChannelParam param) { + Assert.notNull(param.getPlatformId(), "平台ID不可为空"); + Assert.notNull(param.getDeviceIds(), "设备ID不可为空"); + Assert.notEmpty(param.getDeviceIds(), "设备ID不可为空"); + platformChannelService.addChannelByDevice(param.getPlatformId(), param.getDeviceIds()); + } + + @Operation(summary = "移除通道-通过设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/channel/device/remove") + @ResponseBody + public void removeChannelByDevice(@RequestBody UpdateChannelParam param) { + Assert.notNull(param.getPlatformId(), "平台ID不可为空"); + Assert.notNull(param.getDeviceIds(), "设备ID不可为空"); + Assert.notEmpty(param.getDeviceIds(), "设备ID不可为空"); + platformChannelService.removeChannelByDevice(param.getPlatformId(), param.getDeviceIds()); + } + + @Operation(summary = "自定义共享通道信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/channel/custom/update") + @ResponseBody + public void updateCustomChannel(@RequestBody PlatformChannel channel) { + Assert.isTrue(channel.getId() > 0, "共享通道ID必须存在"); + platformChannelService.updateCustomChannel(channel); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java new file mode 100755 index 0000000..d4b75bf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java @@ -0,0 +1,276 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import jakarta.servlet.http.HttpServletRequest; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.UUID; + + +/** + * @author lin + */ +@Tag(name = "国标设备点播") +@Slf4j +@RestController +@RequestMapping("/api/play") +public class PlayController { + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private DeferredResultHolder resultHolder; + + @Autowired + private IPlayService playService; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Operation(summary = "开始点播", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @GetMapping("/start/{deviceId}/{channelId}") + public DeferredResult> play(HttpServletRequest request, @PathVariable String deviceId, + @PathVariable String channelId) { + + log.info("[开始点播] deviceId:{}, channelId:{}, ", deviceId, channelId); + Assert.notNull(deviceId, "设备国标编号不可为NULL"); + Assert.notNull(channelId, "通道国标编号不可为NULL"); + // 获取可用的zlm + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(deviceId, "设备不存在"); + DeviceChannel channel = deviceChannelService.getOne(deviceId, channelId); + Assert.notNull(channel, "通道不存在"); + + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + + result.onTimeout(()->{ + log.info("[点播等待超时] deviceId:{}, channelId:{}, ", deviceId, channelId); + // 释放rtpserver + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("点播超时"); + result.setResult(wvpResult); + + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + deviceChannelService.stopPlay(channel.getId()); + }); + + ErrorCallback callback = (code, msg, streamInfo) -> { + WVPResult wvpResult = new WVPResult<>(); + if (code == InviteErrorCode.SUCCESS.getCode()) { + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); + + if (streamInfo != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + if (!ObjectUtils.isEmpty(streamInfo.getMediaServer().getTranscodeSuffix()) && !"null".equalsIgnoreCase(streamInfo.getMediaServer().getTranscodeSuffix())) { + streamInfo.setStream(streamInfo.getStream() + "_" + streamInfo.getMediaServer().getTranscodeSuffix()); + } + wvpResult.setData(new StreamContent(streamInfo)); + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + result.setResult(wvpResult); + }; + playService.play(device, channel, callback); + return result; + } + + @Operation(summary = "停止点播", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @GetMapping("/stop/{deviceId}/{channelId}") + public JSONObject playStop(@PathVariable String deviceId, @PathVariable String channelId) { + + log.debug(String.format("设备预览/回放停止API调用,streamId:%s_%s", deviceId, channelId )); + + if (deviceId == null || channelId == null) { + throw new ControllerException(ErrorCode.ERROR400); + } + + Device device = deviceService.getDeviceByDeviceId(deviceId); + DeviceChannel channel = deviceChannelService.getOneForSource(deviceId, channelId); + Assert.notNull(device, "设备不存在"); + Assert.notNull(channel, "通道不存在"); + String streamId = String.format("%s_%s", device.getDeviceId(), channel.getDeviceId()); + playService.stop(InviteSessionType.PLAY, device, channel, streamId); + JSONObject json = new JSONObject(); + json.put("deviceId", deviceId); + json.put("channelId", channelId); + return json; + } + /** + * 结束转码 + */ + @Operation(summary = "结束转码", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "key", description = "视频流key", required = true) + @Parameter(name = "mediaServerId", description = "流媒体服务ID", required = true) + @PostMapping("/convertStop/{key}") + public void playConvertStop(@PathVariable String key, String mediaServerId) { + if (mediaServerId == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "流媒体:" + mediaServerId + "不存在" ); + } + MediaServer mediaInfo = mediaServerService.getOne(mediaServerId); + if (mediaInfo == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "使用的流媒体已经停止运行" ); + }else { + Boolean deleted = mediaServerService.delFFmpegSource(mediaInfo, key); + if (!deleted) { + throw new ControllerException(ErrorCode.ERROR100 ); + } + } + } + + @Operation(summary = "语音广播命令", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "deviceId", description = "通道国标编号", required = true) + @Parameter(name = "timeout", description = "推流超时时间(秒)", required = true) + @GetMapping("/broadcast/{deviceId}/{channelId}") + @PostMapping("/broadcast/{deviceId}/{channelId}") + public AudioBroadcastResult broadcastApi(@PathVariable String deviceId, @PathVariable String channelId, Integer timeout, Boolean broadcastMode) { + if (log.isDebugEnabled()) { + log.debug("语音广播API调用"); + } + + return playService.audioBroadcast(deviceId, channelId, broadcastMode); + + } + + @Operation(summary = "停止语音广播") + @Parameter(name = "deviceId", description = "设备Id", required = true) + @Parameter(name = "channelId", description = "通道Id", required = true) + @GetMapping("/broadcast/stop/{deviceId}/{channelId}") + @PostMapping("/broadcast/stop/{deviceId}/{channelId}") + public void stopBroadcast(@PathVariable String deviceId, @PathVariable String channelId) { + if (log.isDebugEnabled()) { + log.debug("停止语音广播API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeviceChannel channel = deviceChannelService.getOne(deviceId, channelId); + Assert.notNull(channel, "通道不存在"); + playService.stopAudioBroadcast(device, channel); + } + + @Operation(summary = "获取所有的ssrc", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping("/ssrc") + public JSONObject getSSRC() { + if (log.isDebugEnabled()) { + log.debug("获取所有的ssrc"); + } + JSONArray objects = new JSONArray(); + List allSsrc = sessionManager.getAll(); + for (SsrcTransaction transaction : allSsrc) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("deviceId", transaction.getDeviceId()); + jsonObject.put("channelId", transaction.getChannelId()); + jsonObject.put("ssrc", transaction.getSsrc()); + jsonObject.put("streamId", transaction.getStream()); + objects.add(jsonObject); + } + + JSONObject jsonObject = new JSONObject(); + jsonObject.put("data", objects); + jsonObject.put("count", objects.size()); + return jsonObject; + } + + @Operation(summary = "获取截图", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @GetMapping("/snap") + public DeferredResult getSnap(String deviceId, String channelId) { + if (log.isDebugEnabled()) { + log.debug("获取截图: {}/{}", deviceId, channelId); + } + + DeferredResult result = new DeferredResult<>(3 * 1000L); + String key = DeferredResultHolder.CALLBACK_CMD_SNAP + deviceId; + String uuid = UUID.randomUUID().toString(); + resultHolder.put(key, uuid, result); + + RequestMessage message = new RequestMessage(); + message.setKey(key); + message.setId(uuid); + + String fileName = deviceId + "_" + channelId + "_" + DateUtil.getNowForUrl() + ".jpg"; + playService.getSnap(deviceId, channelId, fileName, (code, msg, data) -> { + if (code == InviteErrorCode.SUCCESS.getCode()) { + message.setData(data); + }else { + message.setData(WVPResult.fail(code, msg)); + } + resultHolder.invokeResult(message); + }); + return result; + } + +} + diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlaybackController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlaybackController.java new file mode 100755 index 0000000..d5e47b5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlaybackController.java @@ -0,0 +1,220 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.exception.ServiceException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +import jakarta.servlet.http.HttpServletRequest; +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.net.MalformedURLException; +import java.net.URL; +import java.text.ParseException; +import java.util.UUID; + +/** + * @author lin + */ +@Tag(name = "视频回放") +@Slf4j +@RestController +@RequestMapping("/api/playback") +public class PlaybackController { + + @Autowired + private SIPCommander cmder; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private IPlayService playService; + + @Autowired + private DeferredResultHolder resultHolder; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService channelService; + + @Operation(summary = "开始视频回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "startTime", description = "开始时间", required = true) + @Parameter(name = "endTime", description = "结束时间", required = true) + @GetMapping("/start/{deviceId}/{channelId}") + public DeferredResult> start(HttpServletRequest request, @PathVariable String deviceId, @PathVariable String channelId, + String startTime, String endTime) { + + if (log.isDebugEnabled()) { + log.debug(String.format("设备回放 API调用,deviceId:%s ,channelId:%s", deviceId, channelId)); + } + + String uuid = UUID.randomUUID().toString(); + String key = DeferredResultHolder.CALLBACK_CMD_PLAYBACK + deviceId + channelId; + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + resultHolder.put(key, uuid, result); + + RequestMessage requestMessage = new RequestMessage(); + requestMessage.setKey(key); + requestMessage.setId(uuid); + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device == null) { + log.warn("[录像回放] 未找到设备 deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId); + } + + DeviceChannel channel = channelService.getOne(deviceId, channelId); + if (channel == null) { + log.warn("[录像回放] 未找到通道 deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道:" + channelId); + } + playService.playBack(device, channel, startTime, endTime, + (code, msg, data)->{ + + WVPResult wvpResult = new WVPResult<>(); + if (code == InviteErrorCode.SUCCESS.getCode()) { + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); + + if (data != null) { + StreamInfo streamInfo = (StreamInfo)data; + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + wvpResult.setData(new StreamContent(streamInfo)); + } + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + requestMessage.setData(wvpResult); + resultHolder.invokeResult(requestMessage); + }); + + return result; + } + + + @Operation(summary = "停止视频回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @GetMapping("/stop/{deviceId}/{channelId}/{stream}") + public void playStop( + @PathVariable String deviceId, + @PathVariable String channelId, + @PathVariable String stream) { + if (ObjectUtils.isEmpty(deviceId) || ObjectUtils.isEmpty(channelId) || ObjectUtils.isEmpty(stream)) { + throw new ControllerException(ErrorCode.ERROR400); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "设备:" + deviceId + " 未找到"); + } + DeviceChannel deviceChannel = channelService.getOneForSource(deviceId, channelId); + if (deviceChannel == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "通道:" + channelId + " 未找到"); + } + playService.stop(InviteSessionType.PLAYBACK, device, deviceChannel, stream); + } + + + @Operation(summary = "回放暂停", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "streamId", description = "回放流ID", required = true) + @GetMapping("/pause/{streamId}") + public void playbackPause(@PathVariable String streamId) { + log.info("[回放暂停] streamId: {}", streamId); + try { + playService.playbackPause(streamId); + } catch (ServiceException e) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), e.getMessage()); + } catch (InvalidArgumentException | ParseException | SipException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + } + + + @Operation(summary = "回放恢复", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "streamId", description = "回放流ID", required = true) + @GetMapping("/resume/{streamId}") + public void playResume(@PathVariable String streamId) { + log.info("playResume: "+streamId); + try { + playService.playbackResume(streamId); + } catch (ServiceException e) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), e.getMessage()); + } catch (InvalidArgumentException | ParseException | SipException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + } + + @Operation(summary = "回放拖动播放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "streamId", description = "回放流ID", required = true) + @Parameter(name = "seekTime", description = "拖动偏移量,单位s", required = true) + @GetMapping("/seek/{streamId}/{seekTime}") + public void playbackSeek(@PathVariable String streamId, @PathVariable long seekTime) { + log.info("playSeek: "+streamId+", "+seekTime); + try { + playService.playbackSeek(streamId, seekTime); + } catch (InvalidArgumentException | ParseException | SipException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + } + + @Operation(summary = "回放倍速播放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "streamId", description = "回放流ID", required = true) + @Parameter(name = "speed", description = "倍速0.25 0.5 1、2、4、8", required = true) + @GetMapping("/speed/{streamId}/{speed}") + public void playSpeed(@PathVariable String streamId, @PathVariable Double speed) { + Assert.notNull(speed, "倍速不存在"); + log.info("playSpeed: "+streamId+", "+speed); + try { + playService.playbackSpeed(streamId, speed); + } catch (InvalidArgumentException | ParseException | SipException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PtzController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PtzController.java new file mode 100755 index 0000000..2f3b3a9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PtzController.java @@ -0,0 +1,483 @@ +package com.genersoft.iot.vmp.gb28181.controller; + + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IPTZService; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +@Tag(name = "前端设备控制") +@Slf4j +@RestController +@RequestMapping("/api/front-end") +public class PtzController { + + @Autowired + private SIPCommander cmder; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IPTZService ptzService; + + @Autowired + private DeferredResultHolder resultHolder; + + @Operation(summary = "通用前端控制命令(参考国标文档A.3.1指令格式)", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "cmdCode", description = "指令码(对应国标文档指令格式中的字节4)", required = true) + @Parameter(name = "parameter1", description = "数据一(对应国标文档指令格式中的字节5, 范围0-255)", required = true) + @Parameter(name = "parameter2", description = "数据二(对应国标文档指令格式中的字节6, 范围0-255)", required = true) + @Parameter(name = "combindCode2", description = "组合码二(对应国标文档指令格式中的字节7, 范围0-15)", required = true) + @GetMapping("/common/{deviceId}/{channelId}") + public void frontEndCommand(@PathVariable String deviceId,@PathVariable String channelId,Integer cmdCode, Integer parameter1, Integer parameter2, Integer combindCode2){ + + if (log.isDebugEnabled()) { + log.debug(String.format("设备云台控制 API调用,deviceId:%s ,channelId:%s ,cmdCode:%d parameter1:%d parameter2:%d",deviceId, channelId, cmdCode, parameter1, parameter2)); + } + + if (parameter1 == null || parameter1 < 0 || parameter1 > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "parameter1 为 0-255的数字"); + } + if (parameter2 == null || parameter2 < 0 || parameter2 > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "parameter2 为 0-255的数字"); + } + if (combindCode2 == null || combindCode2 < 0 || combindCode2 > 15) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "combindCode2 为 0-15的数字"); + } + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + Assert.notNull(device, "设备[" + deviceId + "]不存在"); + + ptzService.frontEndCommand(device, channelId, cmdCode, parameter1, parameter2, combindCode2); + } + + @Operation(summary = "云台控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: left, right, up, down, upleft, upright, downleft, downright, zoomin, zoomout, stop", required = true) + @Parameter(name = "horizonSpeed", description = "水平速度(0-255)", required = true) + @Parameter(name = "verticalSpeed", description = "垂直速度(0-255)", required = true) + @Parameter(name = "zoomSpeed", description = "缩放速度(0-15)", required = true) + @GetMapping("/ptz/{deviceId}/{channelId}") + public void ptz(@PathVariable String deviceId,@PathVariable String channelId, String command, Integer horizonSpeed, Integer verticalSpeed, Integer zoomSpeed){ + + if (log.isDebugEnabled()) { + log.debug(String.format("设备云台控制 API调用,deviceId:%s ,channelId:%s ,command:%s ,horizonSpeed:%d ,verticalSpeed:%d ,zoomSpeed:%d",deviceId, channelId, command, horizonSpeed, verticalSpeed, zoomSpeed)); + } + if (horizonSpeed == null) { + horizonSpeed = 100; + }else if (horizonSpeed < 0 || horizonSpeed > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "horizonSpeed 为 0-255的数字"); + } + if (verticalSpeed == null) { + verticalSpeed = 100; + }else if (verticalSpeed < 0 || verticalSpeed > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "verticalSpeed 为 0-255的数字"); + } + if (zoomSpeed == null) { + zoomSpeed = 16; + }else if (zoomSpeed < 0 || zoomSpeed > 15) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "zoomSpeed 为 0-15的数字"); + } + + int cmdCode = 0; + switch (command){ + case "left": + cmdCode = 2; + break; + case "right": + cmdCode = 1; + break; + case "up": + cmdCode = 8; + break; + case "down": + cmdCode = 4; + break; + case "upleft": + cmdCode = 10; + break; + case "upright": + cmdCode = 9; + break; + case "downleft": + cmdCode = 6; + break; + case "downright": + cmdCode = 5; + break; + case "zoomin": + cmdCode = 16; + break; + case "zoomout": + cmdCode = 32; + break; + case "stop": + horizonSpeed = 0; + verticalSpeed = 0; + zoomSpeed = 0; + break; + default: + break; + } + frontEndCommand(deviceId, channelId, cmdCode, horizonSpeed, verticalSpeed, zoomSpeed); + } + + + @Operation(summary = "光圈控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: in, out, stop", required = true) + @Parameter(name = "speed", description = "光圈速度(0-255)", required = true) + @GetMapping("/fi/iris/{deviceId}/{channelId}") + public void iris(@PathVariable String deviceId,@PathVariable String channelId, String command, Integer speed){ + + if (log.isDebugEnabled()) { + log.debug("设备光圈控制 API调用,deviceId:{} ,channelId:{} ,command:{} ,speed:{} ",deviceId, channelId, command, speed); + } + + if (speed == null) { + speed = 100; + }else if (speed < 0 || speed > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "speed 为 0-255的数字"); + } + + int cmdCode = 0x40; + switch (command){ + case "in": + cmdCode = 0x44; + break; + case "out": + cmdCode = 0x48; + break; + case "stop": + speed = 0; + break; + default: + break; + } + frontEndCommand(deviceId, channelId, cmdCode, 0, speed, 0); + } + + @Operation(summary = "聚焦控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: near, far, stop", required = true) + @Parameter(name = "speed", description = "聚焦速度(0-255)", required = true) + @GetMapping("/fi/focus/{deviceId}/{channelId}") + public void focus(@PathVariable String deviceId,@PathVariable String channelId, String command, Integer speed){ + + if (log.isDebugEnabled()) { + log.debug("设备聚焦控制 API调用,deviceId:{} ,channelId:{} ,command:{} ,speed:{} ",deviceId, channelId, command, speed); + } + + if (speed == null) { + speed = 100; + }else if (speed < 0 || speed > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "speed 为 0-255的数字"); + } + + int cmdCode = 0x40; + switch (command){ + case "near": + cmdCode = 0x42; + break; + case "far": + cmdCode = 0x41; + break; + case "stop": + speed = 0; + break; + default: + break; + } + frontEndCommand(deviceId, channelId, cmdCode, speed, 0, 0); + } + + @Operation(summary = "查询预置位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @GetMapping("/preset/query/{deviceId}/{channelId}") + public DeferredResult> queryPreset(@PathVariable String deviceId, @PathVariable String channelId) { + if (log.isDebugEnabled()) { + log.debug("设备预置位查询API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> deferredResult = new DeferredResult<> (3 * 1000L); + deviceService.queryPreset(device, channelId, (code, msg, data) -> { + deferredResult.setResult(new WVPResult<>(code, msg, data)); + }); + + deferredResult.onTimeout(()->{ + log.warn("[获取设备预置位] 超时, {}", device.getDeviceId()); + deferredResult.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "超时")); + }); + return deferredResult; + } + + @Operation(summary = "预置位指令-设置预置位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "presetId", description = "预置位编号(1-255)", required = true) + @GetMapping("/preset/add/{deviceId}/{channelId}") + public void addPreset(@PathVariable String deviceId, @PathVariable String channelId, Integer presetId) { + if (presetId == null || presetId < 1 || presetId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "预置位编号必须为1-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x81, 1, presetId, 0); + } + + @Operation(summary = "预置位指令-调用预置位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "presetId", description = "预置位编号(1-255)", required = true) + @GetMapping("/preset/call/{deviceId}/{channelId}") + public void callPreset(@PathVariable String deviceId, @PathVariable String channelId, Integer presetId) { + if (presetId == null || presetId < 1 || presetId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "预置位编号必须为1-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x82, 1, presetId, 0); + } + + @Operation(summary = "预置位指令-删除预置位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "presetId", description = "预置位编号(1-255)", required = true) + @GetMapping("/preset/delete/{deviceId}/{channelId}") + public void deletePreset(@PathVariable String deviceId, @PathVariable String channelId, Integer presetId) { + if (presetId == null || presetId < 1 || presetId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "预置位编号必须为1-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x83, 1, presetId, 0); + } + + @Operation(summary = "巡航指令-加入巡航点", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "cruiseId", description = "巡航组号(0-255)", required = true) + @Parameter(name = "presetId", description = "预置位编号(1-255)", required = true) + @GetMapping("/cruise/point/add/{deviceId}/{channelId}") + public void addCruisePoint(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId, Integer presetId) { + if (presetId == null || cruiseId == null || presetId < 1 || presetId > 255 || cruiseId < 0 || cruiseId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "编号必须为1-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x84, cruiseId, presetId, 0); + } + + @Operation(summary = "巡航指令-删除一个巡航点", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "cruiseId", description = "巡航组号(1-255)", required = true) + @Parameter(name = "presetId", description = "预置位编号(0-255, 为0时删除整个巡航)", required = true) + @GetMapping("/cruise/point/delete/{deviceId}/{channelId}") + public void deleteCruisePoint(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId, Integer presetId) { + if (presetId == null || presetId < 0 || presetId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "预置位编号必须为0-255之间的数字, 为0时删除整个巡航"); + } + if (cruiseId == null || cruiseId < 0 || cruiseId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航组号必须为0-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x85, cruiseId, presetId, 0); + } + + @Operation(summary = "巡航指令-设置巡航速度", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "cruiseId", description = "巡航组号(0-255)", required = true) + @Parameter(name = "speed", description = "巡航速度(1-4095)", required = true) + @GetMapping("/cruise/speed/{deviceId}/{channelId}") + public void setCruiseSpeed(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId, Integer speed) { + if (cruiseId == null || cruiseId < 0 || cruiseId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航组号必须为0-255之间的数字"); + } + if (speed == null || speed < 1 || speed > 4095) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航速度必须为1-4095之间的数字"); + } + int parameter2 = speed & 0xFF; + int combindCode2 = speed >> 8; + frontEndCommand(deviceId, channelId, 0x86, cruiseId, parameter2, combindCode2); + } + + @Operation(summary = "巡航指令-设置巡航停留时间", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "cruiseId", description = "巡航组号", required = true) + @Parameter(name = "time", description = "巡航停留时间(1-4095)", required = true) + @GetMapping("/cruise/time/{deviceId}/{channelId}") + public void setCruiseTime(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId, Integer time) { + if (cruiseId == null || cruiseId < 0 || cruiseId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航组号必须为0-255之间的数字"); + } + if (time == null || time < 1 || time > 4095) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航停留时间必须为1-4095之间的数字"); + } + int parameter2 = time & 0xFF; + int combindCode2 = time >> 8; + frontEndCommand(deviceId, channelId, 0x87, cruiseId, parameter2, combindCode2); + } + + @Operation(summary = "巡航指令-开始巡航", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "cruiseId", description = "巡航组号)", required = true) + @GetMapping("/cruise/start/{deviceId}/{channelId}") + public void startCruise(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId) { + if (cruiseId == null || cruiseId < 0 || cruiseId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航组号必须为0-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x88, cruiseId, 0, 0); + } + + @Operation(summary = "巡航指令-停止巡航", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "cruiseId", description = "巡航组号", required = true) + @GetMapping("/cruise/stop/{deviceId}/{channelId}") + public void stopCruise(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId) { + if (cruiseId == null || cruiseId < 0 || cruiseId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航组号必须为0-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0, 0, 0, 0); + } + + @Operation(summary = "扫描指令-开始自动扫描", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-255)", required = true) + @GetMapping("/scan/start/{deviceId}/{channelId}") + public void startScan(@PathVariable String deviceId, @PathVariable String channelId, Integer scanId) { + if (scanId == null || scanId < 0 || scanId > 255 ) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "扫描组号必须为0-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x89, scanId, 0, 0); + } + + @Operation(summary = "扫描指令-停止自动扫描", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-255)", required = true) + @GetMapping("/scan/stop/{deviceId}/{channelId}") + public void stopScan(@PathVariable String deviceId, @PathVariable String channelId, Integer scanId) { + if (scanId == null || scanId < 0 || scanId > 255 ) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "扫描组号必须为0-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0, 0, 0, 0); + } + + @Operation(summary = "扫描指令-设置自动扫描左边界", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-255)", required = true) + @GetMapping("/scan/set/left/{deviceId}/{channelId}") + public void setScanLeft(@PathVariable String deviceId, @PathVariable String channelId, Integer scanId) { + if (scanId == null || scanId < 0 || scanId > 255 ) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "扫描组号必须为0-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x89, scanId, 1, 0); + } + + @Operation(summary = "扫描指令-设置自动扫描右边界", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-255)", required = true) + @GetMapping("/scan/set/right/{deviceId}/{channelId}") + public void setScanRight(@PathVariable String deviceId, @PathVariable String channelId, Integer scanId) { + if (scanId == null || scanId < 0 || scanId > 255 ) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "扫描组号必须为0-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x89, scanId, 2, 0); + } + + + @Operation(summary = "扫描指令-设置自动扫描速度", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-255)", required = true) + @Parameter(name = "speed", description = "自动扫描速度(1-4095)", required = true) + @GetMapping("/scan/set/speed/{deviceId}/{channelId}") + public void setScanSpeed(@PathVariable String deviceId, @PathVariable String channelId, Integer scanId, Integer speed) { + if (scanId == null || scanId < 0 || scanId > 255 ) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "扫描组号必须为0-255之间的数字"); + } + if (speed == null || speed < 1 || speed > 4095) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "自动扫描速度必须为1-4095之间的数字"); + } + int parameter2 = speed & 0xFF; + int combindCode2 = speed >> 8; + frontEndCommand(deviceId, channelId, 0x8A, scanId, parameter2, combindCode2); + } + + + @Operation(summary = "辅助开关控制指令-雨刷控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: on, off", required = true) + @GetMapping("/wiper/{deviceId}/{channelId}") + public void wiper(@PathVariable String deviceId,@PathVariable String channelId, String command){ + + if (log.isDebugEnabled()) { + log.debug("辅助开关控制指令-雨刷控制 API调用,deviceId:{} ,channelId:{} ,command:{}",deviceId, channelId, command); + } + + int cmdCode = 0; + switch (command){ + case "on": + cmdCode = 0x8c; + break; + case "off": + cmdCode = 0x8d; + break; + default: + break; + } + frontEndCommand(deviceId, channelId, cmdCode, 1, 0, 0); + } + + @Operation(summary = "辅助开关控制指令", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: on, off", required = true) + @Parameter(name = "switchId", description = "开关编号", required = true) + @GetMapping("/auxiliary/{deviceId}/{channelId}") + public void auxiliarySwitch(@PathVariable String deviceId,@PathVariable String channelId, String command, Integer switchId){ + + if (log.isDebugEnabled()) { + log.debug("辅助开关控制指令-雨刷控制 API调用,deviceId:{} ,channelId:{} ,command:{}, switchId: {}",deviceId, channelId, command, switchId); + } + + int cmdCode = 0; + switch (command){ + case "on": + cmdCode = 0x8c; + break; + case "off": + cmdCode = 0x8d; + break; + default: + break; + } + frontEndCommand(deviceId, channelId, cmdCode, switchId, 0, 0); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/RegionController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/RegionController.java new file mode 100644 index 0000000..ff415df --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/RegionController.java @@ -0,0 +1,152 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.Region; +import com.genersoft.iot.vmp.gb28181.bean.RegionTree; +import com.genersoft.iot.vmp.gb28181.service.IRegionService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "区域管理") +@RestController +@RequestMapping("/api/region") +public class RegionController { + + private final static Logger logger = LoggerFactory.getLogger(RegionController.class); + + @Autowired + private IRegionService regionService; + + @Operation(summary = "添加区域") + @Parameter(name = "region", description = "Region", required = true) + @ResponseBody + @PostMapping("/add") + public void add(@RequestBody Region region){ + regionService.add(region); + } + + @Operation(summary = "查询区域") + @Parameter(name = "query", description = "要搜索的内容", required = true) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @ResponseBody + @GetMapping("/page/list") + public PageInfo query( + @RequestParam(required = false) String query, + @RequestParam(required = true) int page, + @RequestParam(required = true) int count + ){ + return regionService.query(query, page, count); + } + + @Operation(summary = "查询区域节点") + @Parameter(name = "query", description = "要搜索的内容", required = true) + @Parameter(name = "parent", description = "所属行政区划编号", required = true) + @Parameter(name = "hasChannel", description = "是否查询通道", required = true) + @ResponseBody + @GetMapping("/tree/list") + public List queryForTree( + @RequestParam(required = false) Integer parent, + @RequestParam(required = false) Boolean hasChannel + ){ + return regionService.queryForTree(parent, hasChannel); + } + + + @Operation(summary = "查询区域") + @Parameter(name = "query", description = "要搜索的内容", required = true) + @Parameter(name = "channel", description = "true为查询通道,false为查询节点", required = true) + @ResponseBody + @GetMapping("/tree/query") + public PageInfo queryTree(Integer page, Integer count, + @RequestParam(required = true) String query + ){ + return regionService.queryList(page, count, query); + } + + @Operation(summary = "更新区域") + @Parameter(name = "region", description = "Region", required = true) + @ResponseBody + @PostMapping("/update") + public void update(@RequestBody Region region){ + regionService.update(region); + } + + @Operation(summary = "删除区域") + @Parameter(name = "id", description = "区域ID", required = true) + @ResponseBody + @DeleteMapping("/delete") + public void delete(Integer id){ + Assert.notNull(id, "区域ID需要存在"); + boolean result = regionService.deleteByDeviceId(id); + if (!result) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "移除失败"); + } + } + + @Operation(summary = "根据区域Id查询区域") + @Parameter(name = "regionDeviceId", description = "行政区划节点编号", required = true) + @ResponseBody + @GetMapping("/one") + public Region queryRegionByDeviceId( + @RequestParam(required = true) String regionDeviceId + ){ + if (ObjectUtils.isEmpty(regionDeviceId.trim())) { + throw new ControllerException(ErrorCode.ERROR400); + } + return regionService.queryRegionByDeviceId(regionDeviceId); + } + + @Operation(summary = "获取所属的行政区划下的行政区划") + @Parameter(name = "parent", description = "所属的行政区划", required = false) + @ResponseBody + @GetMapping("/base/child/list") + public List getAllChild(@RequestParam(required = false) String parent){ + if (ObjectUtils.isEmpty(parent)) { + parent = null; + } + return regionService.getAllChild(parent); + } + + @Operation(summary = "获取所属的行政区划下的行政区划") + @Parameter(name = "deviceId", description = "当前的行政区划", required = false) + @ResponseBody + @GetMapping("/path") + public List getPath(String deviceId){ + return regionService.getPath(deviceId); + } + + @Operation(summary = "从通道中同步行政区划") + @ResponseBody + @GetMapping("/sync") + public void sync(){ + regionService.syncFromChannel(); + } + + @Operation(summary = "根据行政区划编号从文件中查询层级和描述") + @ResponseBody + @GetMapping("/description") + public String getDescription(String civilCode){ + return regionService.getDescription(civilCode); + } + + @Operation(summary = "根据行政区划编号从文件中查询层级并添加") + @ResponseBody + @GetMapping("/addByCivilCode") + public void addByCivilCode(String civilCode){ + regionService.addByCivilCode(civilCode); + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/SseController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/SseController.java new file mode 100644 index 0000000..8e97295 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/SseController.java @@ -0,0 +1,42 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.gb28181.session.SseSessionManager; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + + +/** + * SSE 推送. + * + * @author lawrencehj + * @author xiaoQQya + * @since 2021/01/20 + */ +@Tag(name = "SSE 推送") +@RestController +@RequestMapping("/api") +public class SseController { + + @Resource + private SseSessionManager sseSessionManager; + + /** + * SSE 推送. + * + * @param browserId 浏览器ID + */ + @GetMapping("/emit") + public SseEmitter emit(HttpServletResponse response, @RequestParam String browserId) throws IOException, InterruptedException { +// response.setContentType("text/event-stream"); +// response.setCharacterEncoding("utf-8"); + return sseSessionManager.conect(browserId); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/AudioBroadcastEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/AudioBroadcastEvent.java new file mode 100644 index 0000000..3c58934 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/AudioBroadcastEvent.java @@ -0,0 +1,9 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + + +/** + * @author lin + */ +public interface AudioBroadcastEvent { + void call(String msg); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelForThin.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelForThin.java new file mode 100644 index 0000000..02323b3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelForThin.java @@ -0,0 +1,9 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import lombok.Data; + +@Data +public class ChannelForThin { + private Integer gbId; + private Integer mapLevel; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelReduce.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelReduce.java new file mode 100755 index 0000000..b4c1d01 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelReduce.java @@ -0,0 +1,137 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * 精简的channel信息展示,主要是选择通道的时候展示列表使用 + */ +@Schema(description = "精简的channel信息展示") +public class ChannelReduce { + + /** + * deviceChannel的数据库自增ID + */ + @Schema(description = "deviceChannel的数据库自增ID") + private int id; + + /** + * 通道id + */ + @Schema(description = "通道国标编号") + private String channelId; + + /** + * 设备id + */ + @Schema(description = "设备国标编号") + private String deviceId; + + /** + * 通道名 + */ + @Schema(description = "通道名") + private String name; + + /** + * 生产厂商 + */ + @Schema(description = "生产厂商") + private String manufacturer; + + /** + * wan地址 + */ + @Schema(description = "wan地址") + private String hostAddress; + + /** + * 子节点数 + */ + @Schema(description = "子节点数") + private int subCount; + + /** + * 平台Id + */ + @Schema(description = "平台上级国标编号") + private String platformId; + + /** + * 目录Id + */ + @Schema(description = "目录国标编号") + private String catalogId; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getManufacturer() { + return manufacturer; + } + + public void setManufacturer(String manufacturer) { + this.manufacturer = manufacturer; + } + + public String getHostAddress() { + return hostAddress; + } + + public void setHostAddress(String hostAddress) { + this.hostAddress = hostAddress; + } + + public int getSubCount() { + return subCount; + } + + public void setSubCount(int subCount) { + this.subCount = subCount; + } + + public String getPlatformId() { + return platformId; + } + + public void setPlatformId(String platformId) { + this.platformId = platformId; + } + + public String getCatalogId() { + return catalogId; + } + + public void setCatalogId(String catalogId) { + this.catalogId = catalogId; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToGroupByGbDeviceParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToGroupByGbDeviceParam.java new file mode 100644 index 0000000..b95a7b8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToGroupByGbDeviceParam.java @@ -0,0 +1,12 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import lombok.Data; + +import java.util.List; + +@Data +public class ChannelToGroupByGbDeviceParam { + private List deviceIds; + private String parentId; + private String businessGroup; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToGroupParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToGroupParam.java new file mode 100644 index 0000000..5889893 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToGroupParam.java @@ -0,0 +1,14 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import lombok.Data; + +import java.util.List; + +@Data +public class ChannelToGroupParam { + + private String parentId; + private String businessGroup; + private List channelIds; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToRegionByGbDeviceParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToRegionByGbDeviceParam.java new file mode 100644 index 0000000..6820bb6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToRegionByGbDeviceParam.java @@ -0,0 +1,11 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import lombok.Data; + +import java.util.List; + +@Data +public class ChannelToRegionByGbDeviceParam { + private List deviceIds; + private String civilCode; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToRegionParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToRegionParam.java new file mode 100644 index 0000000..7f74004 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToRegionParam.java @@ -0,0 +1,21 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description="提交行政区划关联多个通道的参数") +public class ChannelToRegionParam { + + @Schema(description = "行政区划编号") + private String civilCode; + + @Schema(description = "选择的通道, 和all参数二选一") + private List channelIds; + + @Schema(description = "所有通道, 和channelIds参数二选一") + private Boolean all; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/DrawThinParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/DrawThinParam.java new file mode 100644 index 0000000..bd43b66 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/DrawThinParam.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import lombok.Getter; +import lombok.Setter; + +import java.util.Map; + +@Getter +@Setter +public class DrawThinParam { + private Map zoomParam; + private Extent extent; + + /** + * 地理坐标系, WGS84/GCJ02, 用来标识 extent 参数的坐标系 + */ + private String geoCoordSys; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/Extent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/Extent.java new file mode 100644 index 0000000..33c923e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/Extent.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Extent { + private Double minLng; + private Double maxLng; + private Double minLat; + private Double maxLat; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/PlayResult.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/PlayResult.java new file mode 100755 index 0000000..630eb66 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/PlayResult.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import org.springframework.web.context.request.async.DeferredResult; + +public class PlayResult { + + private DeferredResult> result; + private String uuid; + + private Device device; + + public DeferredResult> getResult() { + return result; + } + + public void setResult(DeferredResult> result) { + this.result = result; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public Device getDevice() { + return device; + } + + public void setDevice(Device device) { + this.device = device; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ResetParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ResetParam.java new file mode 100644 index 0000000..bcb144b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ResetParam.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class ResetParam { + + private Integer id; + private List chanelFields; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/UpdateChannelParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/UpdateChannelParam.java new file mode 100755 index 0000000..5f36002 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/UpdateChannelParam.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "通道关联参数") +public class UpdateChannelParam { + + @Schema(description = "上级平台的数据库ID") + private Integer platformId; + + + @Schema(description = "关联所有通道") + private boolean all; + + @Schema(description = "待关联的通道ID") + List channelIds; + + @Schema(description = "待关联的设备ID") + List deviceIds; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java new file mode 100644 index 0000000..1e93f07 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java @@ -0,0 +1,698 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.controller.bean.Extent; +import com.genersoft.iot.vmp.gb28181.dao.provider.ChannelProvider; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.web.custom.bean.CameraChannel; +import com.genersoft.iot.vmp.web.custom.bean.CameraGroup; +import com.genersoft.iot.vmp.web.custom.bean.Point; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.List; + +@Mapper +@Repository +public interface CommonGBChannelMapper { + + + @SelectProvider(type = ChannelProvider.class, method = "queryByDeviceId") + List queryByDeviceId(@Param("gbDeviceId") String gbDeviceId); + + @Insert(" ") + @Options(useGeneratedKeys = true, keyProperty = "gbId", keyColumn = "id") + int insert(CommonGBChannel commonGBChannel); + + @SelectProvider(type = ChannelProvider.class, method = "queryById") + CommonGBChannel queryById(@Param("gbId") int gbId); + + @Delete(value = {"delete from wvp_device_channel where id = #{gbId} "}) + void delete(int gbId); + + @Update(value = {" "}) + int update(CommonGBChannel commonGBChannel); + + @Update(value = {" "}) + int updateStatusById(@Param("gbId") int gbId, @Param("status") String status); + + @Update("") + int updateStatusForListById(List commonGBChannels, @Param("status") String status); + + @SelectProvider(type = ChannelProvider.class, method = "queryInListByStatus") + List queryInListByStatus(List commonGBChannelList, @Param("status") String status); + + + @Insert(" ") + int batchAdd(List commonGBChannels); + + @Update("") + int updateStatus(List commonGBChannels); + + @Update(value = {" "}) + void reset(@Param("id") int id, List fields, @Param("updateTime") String updateTime); + + + @SelectProvider(type = ChannelProvider.class, method = "queryByIds") + List queryByIds(Collection ids); + + @Delete(value = {" "}) + void batchDelete(List channelListInDb); + + @SelectProvider(type = ChannelProvider.class, method = "queryListByCivilCode") + List queryListByCivilCode(@Param("query") String query, @Param("online") Boolean online, + @Param("dataType") Integer dataType, @Param("civilCode") String civilCode); + + + + @SelectProvider(type = ChannelProvider.class, method = "queryListByParentId") + List queryListByParentId(@Param("query") String query, @Param("online") Boolean online, + @Param("dataType") Integer dataType, @Param("groupDeviceId") String groupDeviceId); + + + + @Select("") + List queryForRegionTreeByCivilCode(@Param("parentDeviceId") String parentDeviceId); + + @Update(value = {" "}) + int removeCivilCode(List allChildren); + + + @Update(value = {" "}) + int updateRegion(@Param("civilCode") String civilCode, @Param("channelList") List channelList); + + @SelectProvider(type = ChannelProvider.class, method = "queryByIdsOrCivilCode") + List queryByIdsOrCivilCode(@Param("civilCode") String civilCode, @Param("ids") List ids); + + @Update(value = {" "}) + int removeCivilCodeByChannels(List channelList); + + @Update(value = {" "}) + int removeCivilCodeByChannelIds(List channelIdList); + + @SelectProvider(type = ChannelProvider.class, method = "queryByCivilCode") + List queryByCivilCode(@Param("civilCode") String civilCode); + + @SelectProvider(type = ChannelProvider.class, method = "queryByDataTypeAndDeviceIds") + List queryByDataTypeAndDeviceIds(@Param("dataType") Integer dataType, List deviceIds); + + @SelectProvider(type = ChannelProvider.class, method = "queryByGbDeviceIds") + List queryByGbDeviceIds(List deviceIds); + + @Select(value = {" "}) + List queryByGbDeviceIdsForIds(@Param("dataType") Integer dataType, List deviceIds); + + @SelectProvider(type = ChannelProvider.class, method = "queryByGroupList") + List queryByGroupList(List groupList); + + @Update(value = {" "}) + int removeParentIdByChannels(List channelList); + + @SelectProvider(type = ChannelProvider.class, method = "queryByBusinessGroup") + List queryByBusinessGroup(@Param("businessGroup") String businessGroup); + + @SelectProvider(type = ChannelProvider.class, method = "queryByParentId") + List queryByParentId(@Param("parentId") String parentId); + + @Update(value = {" "}) + int updateBusinessGroupByChannelList(@Param("businessGroup") String businessGroup, List channelList); + + @Update(value = {" "}) + int updateParentIdByChannelList(@Param("parentId") String parentId, List channelList); + + @Select("") + List queryForGroupTreeByParentId(@Param("query") String query, @Param("parent") String parent); + + @Update(value = {" "}) + int updateGroup(@Param("parentId") String parentId, @Param("businessGroup") String businessGroup, + List channelList); + + @Update({""}) + int batchUpdate(List commonGBChannels); + + @SelectProvider(type = ChannelProvider.class, method = "queryWithPlatform") + List queryWithPlatform(@Param("platformId") Integer platformId); + + @SelectProvider(type = ChannelProvider.class, method = "queryShareChannelByParentId") + List queryShareChannelByParentId(@Param("parentId") String parentId, @Param("platformId") Integer platformId); + + @SelectProvider(type = ChannelProvider.class, method = "queryShareChannelByCivilCode") + List queryShareChannelByCivilCode(@Param("civilCode") String civilCode, @Param("platformId") Integer platformId); + + @Update(value = {" "}) + int updateCivilCodeByChannelList(@Param("civilCode") String civilCode, List channelList); + + @SelectProvider(type = ChannelProvider.class, method = "queryListByStreamPushList") + List queryListByStreamPushList(@Param("dataType") Integer dataType, List streamPushList); + + @Update(value = {" "}) + void updateGpsByDeviceId(List gpsMsgInfoList); + + @SelectProvider(type = ChannelProvider.class, method = "queryList") + List queryList(@Param("query") String query, @Param("online") Boolean online, + @Param("hasRecordPlan") Boolean hasRecordPlan, @Param("dataType") Integer dataType, + @Param("civilCode") String civilCode, @Param("parentDeviceId") String parentDeviceId); + + @Update(value = {" "}) + void removeRecordPlan(List channelIds); + + @Update(value = {" "}) + void addRecordPlan(List channelIds, @Param("planId") Integer planId); + + @Update(value = {" "}) + void addRecordPlanForAll(@Param("planId") Integer planId); + + @Update(value = {" "}) + void removeRecordPlanByPlanId( @Param("planId") Integer planId); + + + @Select("") + List queryForRecordPlanForWebList(@Param("planId") Integer planId, @Param("query") String query, + @Param("dataType") Integer dataType, @Param("online") Boolean online, + @Param("hasLink") Boolean hasLink); + + @SelectProvider(type = ChannelProvider.class, method = "queryByDataId") + CommonGBChannel queryByDataId(@Param("dataType") Integer dataType, @Param("dataDeviceId") Integer dataDeviceId); + + @SelectProvider(type = ChannelProvider.class, method = "queryListByCivilCodeForUnusual") + List queryListByCivilCodeForUnusual(@Param("query") String query, @Param("online") Boolean online, @Param("dataType")Integer dataType); + + @SelectProvider(type = ChannelProvider.class, method = "queryAllForUnusualCivilCode") + List queryAllForUnusualCivilCode(); + + @SelectProvider(type = ChannelProvider.class, method = "queryListByParentForUnusual") + List queryListByParentForUnusual(@Param("query") String query, @Param("online") Boolean online, @Param("dataType")Integer dataType); + + @SelectProvider(type = ChannelProvider.class, method = "queryAllForUnusualParent") + List queryAllForUnusualParent(); + + @Update(value = {" "}) + void removeParentIdByChannelIds(List channelIdsForClear); + + + @SelectProvider(type = ChannelProvider.class, method = "queryOnlineListsByGbDeviceId") + List queryOnlineListsByGbDeviceId(@Param("deviceId") int deviceId); + + @SelectProvider(type = ChannelProvider.class, method = "queryCommonChannelByDeviceChannel") + CommonGBChannel queryCommonChannelByDeviceChannel(@Param("dataType") Integer dataType, @Param("dataDeviceId") Integer dataDeviceId, @Param("deviceId") String deviceId); + + @Update("UPDATE wvp_device_channel SET stream_id = #{stream} where id = #{gbId}") + void updateStream(int gbId, String stream); + + @Update("") + void updateGps(List commonGBChannels); + + + @SelectProvider(type = ChannelProvider.class, method = "queryListForSy") + List queryListForSy(@Param("groupDeviceId") String groupDeviceId, @Param("online") Boolean online); + + + @SelectProvider(type = ChannelProvider.class, method = "queryGbChannelByChannelDeviceIdAndGbDeviceId") + List queryGbChannelByChannelDeviceIdAndGbDeviceId(@Param("channelDeviceId") String channelDeviceId, @Param("gbDeviceId") String gbDeviceId); + + @SelectProvider(type = ChannelProvider.class, method = "queryListByDeviceIds") + List queryListByDeviceIds(List deviceIds); + + @SelectProvider(type = ChannelProvider.class, method = "queryListWithChildForSy") + List queryListWithChildForSy(@Param("query") String query, @Param("sortName") String sortName, @Param("order") Boolean order, @Param("groupList") List groupList, @Param("online") Boolean online); + + @SelectProvider(type = ChannelProvider.class, method = "queryListByAddressAndDirectionType") + List queryListByAddressAndDirectionType(@Param("address") String address, @Param("directionType") Integer directionType); + + @SelectProvider(type = ChannelProvider.class, method = "queryListInBox") + List queryListInBox(@Param("minLongitude") Double minLongitude, @Param("maxLongitude") Double maxLongitude, + @Param("minLatitude") Double minLatitude, @Param("maxLatitude") Double maxLatitude, + @Param("level") Integer level, @Param("groupList") List groupList); + + @SelectProvider(type = ChannelProvider.class, method = "queryListInCircleForMysql", databaseId = "mysql") + @SelectProvider(type = ChannelProvider.class, method = "queryListInCircleForH2", databaseId = "h2") + @SelectProvider(type = ChannelProvider.class, method = "queryListInCircleForKingBase", databaseId = "kingbase") + @SelectProvider(type = ChannelProvider.class, method = "queryListInCircleForKingBase", databaseId = "postgresql") + List queryListInCircle(@Param("centerLongitude") Double centerLongitude, @Param("centerLatitude") Double centerLatitude, + @Param("radius") Double radius, @Param("level") Integer level, @Param("groupList") List groupList); + + @SelectProvider(type = ChannelProvider.class, method = "queryListInPolygonForMysql", databaseId = "mysql") + @SelectProvider(type = ChannelProvider.class, method = "queryListInPolygonForH2", databaseId = "h2") + @SelectProvider(type = ChannelProvider.class, method = "queryListInPolygonForKingBase", databaseId = "kingbase") + @SelectProvider(type = ChannelProvider.class, method = "queryListInPolygonForKingBase", databaseId = "postgresql") + List queryListInPolygon(@Param("pointList") List pointList, @Param("level") Integer level, @Param("groupList") List groupList); + + @SelectProvider(type = ChannelProvider.class, method = "queryListForSyMobile") + List queryListForSyMobile(@Param("business") String business); + + @SelectProvider(type = ChannelProvider.class, method = "queryCameraChannelById") + CameraChannel queryCameraChannelById(@Param("gbId") Integer id); + + @Update("") + void saveLevel(List channels); + + @SelectProvider(type = ChannelProvider.class, method = "queryCameraChannelByIds") + List queryCameraChannelByIds(List channelList); + + @SelectProvider(type = ChannelProvider.class, method = "queryOldChanelListByChannels") + List queryOldChanelListByChannels(List channelList); + + @SelectProvider(type = ChannelProvider.class, method = "queryMeetingChannelList") + List queryMeetingChannelList(@Param("business") String business); + + @Update("UPDATE wvp_device_channel SET map_level=null") + int resetLevel(); + + @SelectProvider(type = ChannelProvider.class, method = "queryCameraChannelInBox") + List queryCameraChannelInBox(@Param("minLon") double minLon, @Param("maxLon") double maxLon, + @Param("minLat") double minLat, @Param("maxLat") double maxLat); + + @Select("select " + + "MAX(coalesce(gb_longitude, longitude)) as maxLng, " + + "MIN(coalesce(gb_longitude, longitude)) as minLng, " + + "MAX(coalesce(gb_latitude, latitude)) as maxLat, " + + "MIN(coalesce(gb_latitude, latitude)) as minLat " + + " from wvp_device_channel " + + " where channel_type = 0") + Extent queryExtent(); + + @SelectProvider(type = ChannelProvider.class, method = "queryAllWithPosition") + List queryAllWithPosition(); + + @SelectProvider(type = ChannelProvider.class, method = "queryListInExtent") + List queryListInExtent(@Param("minLng") double minLng, @Param("maxLng") double maxLng, + @Param("minLat") double minLat, @Param("maxLat") double maxLat); + + @SelectProvider(type = ChannelProvider.class, method = "queryListOutExtent") + List queryListOutExtent(@Param("minLng") double minLng, @Param("maxLng") double maxLng, + @Param("minLat") double minLat, @Param("maxLat") double maxLat); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceAlarmMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceAlarmMapper.java new file mode 100755 index 0000000..c380e59 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceAlarmMapper.java @@ -0,0 +1,51 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 用于存储设备的报警信息 + */ +@Mapper +@Repository +public interface DeviceAlarmMapper { + + @Insert("INSERT INTO wvp_device_alarm (device_id, channel_id, alarm_priority, alarm_method, alarm_time, alarm_description, longitude, latitude, alarm_type , create_time ) " + + "VALUES (#{deviceId}, #{channelId}, #{alarmPriority}, #{alarmMethod}, #{alarmTime}, #{alarmDescription}, #{longitude}, #{latitude}, #{alarmType}, #{createTime})") + int add(DeviceAlarm alarm); + + + @Select( value = {" "}) + List query(@Param("deviceId") String deviceId, @Param("channelId") String channelId, @Param("alarmPriority") String alarmPriority, @Param("alarmMethod") String alarmMethod, + @Param("alarmType") String alarmType, @Param("startTime") String startTime, @Param("endTime") String endTime); + + + @Delete(" " + ) + int clearAlarmBeforeTime(@Param("id") Integer id, @Param("deviceIdList") List deviceIdList, @Param("time") String time); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java new file mode 100755 index 0000000..b561d8d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java @@ -0,0 +1,618 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.controller.bean.ChannelReduce; +import com.genersoft.iot.vmp.gb28181.dao.provider.DeviceChannelProvider; +import com.genersoft.iot.vmp.web.gb28181.dto.DeviceChannelExtend; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 用于存储设备通道信息 + */ +@Mapper +@Repository +public interface DeviceChannelMapper { + + + @Insert("") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int add(DeviceChannel channel); + + @Update(value = {" "}) + int update(DeviceChannel channel); + + @SelectProvider(type = DeviceChannelProvider.class, method = "queryChannels") + List queryChannels(@Param("dataDeviceId") Integer dataDeviceId, @Param("civilCode") String civilCode, + @Param("businessGroupId") String businessGroupId, @Param("parentChannelId") String parentChannelId, + @Param("query") String query, @Param("queryParent") Boolean queryParent, + @Param("hasSubChannel") Boolean hasSubChannel, @Param("online") Boolean online, + @Param("channelIds") List channelIds, @Param("hasStream") Boolean hasStream); + + @SelectProvider(type = DeviceChannelProvider.class, method = "queryChannelsByDeviceDbId") + List queryChannelsByDeviceDbId(@Param("dataDeviceId") int dataDeviceId); + + @Select("") + List queryChaneIdListByDeviceDbIds(List deviceDbIds); + + @Delete("DELETE FROM wvp_device_channel WHERE data_type =1 and data_device_id=#{dataDeviceId}") + int cleanChannelsByDeviceId(@Param("dataDeviceId") int dataDeviceId); + + @Delete("DELETE FROM wvp_device_channel WHERE data_type=#{dataType} and data_device_id=#{dataDeviceId} AND device_id=#{deviceId}") + int deleteForNotify(DeviceChannel channel); + + @Select(value = {" "}) + List queryChannelsWithDeviceInfo( @Param("deviceId") String deviceId, @Param("parentChannelId") String parentChannelId, @Param("query") String query, @Param("hasSubChannel") Boolean hasSubChannel, @Param("online") Boolean online, @Param("channelIds") List channelIds); + + @Update(value = {"UPDATE wvp_device_channel SET stream_id=#{streamId} WHERE id=#{channelId}"}) + void startPlay(@Param("channelId") Integer channelId, @Param("streamId") String streamId); + + + @Select(value = {" "}) + List queryChannelListInAll(@Param("query") String query, @Param("online") Boolean online, @Param("hasSubChannel") Boolean hasSubChannel, @Param("platformId") String platformId, @Param("catalogId") String catalogId); + + + @Update(value = {"UPDATE wvp_device_channel SET status='OFF' WHERE id=#{id}"}) + void offline(@Param("id") int id); + + @Insert("") + int batchAdd(@Param("addChannels") List addChannels); + + + @Update(value = {"UPDATE wvp_device_channel SET status='ON' WHERE id=#{id}"}) + void online(@Param("id") int id); + + @Update({""}) + int batchUpdate(List updateChannels); + + @Update(value = {" "}) + int updatePosition(DeviceChannel deviceChannel); + + @Select("select " + + " id,\n" + + " data_device_id,\n" + + " create_time,\n" + + " update_time,\n" + + " sub_count,\n" + + " stream_id,\n" + + " has_audio,\n" + + " gps_time,\n" + + " stream_identification,\n" + + " channel_type,\n" + + " device_id,\n" + + " name,\n" + + " manufacturer,\n" + + " model,\n" + + " owner,\n" + + " civil_code,\n" + + " block,\n" + + " address,\n" + + " parental,\n" + + " parent_id,\n" + + " safety_way,\n" + + " register_way,\n" + + " cert_num,\n" + + " certifiable,\n" + + " err_code,\n" + + " end_time,\n" + + " secrecy,\n" + + " ip_address,\n" + + " port,\n" + + " password,\n" + + " status,\n" + + " longitude,\n" + + " latitude,\n" + + " gb_longitude,\n" + + " gb_latitude,\n" + + " ptz_type,\n" + + " position_type,\n" + + " room_type,\n" + + " use_type,\n" + + " supply_light_type,\n" + + " direction_type,\n" + + " resolution,\n" + + " business_group_id,\n" + + " download_speed,\n" + + " svc_space_support_mod,\n" + + " svc_time_support_mode\n" + + " from wvp_device_channel where data_type = 1 and data_device_id = #{dataDeviceId}") + List queryAllChannelsForRefresh(@Param("dataDeviceId") int dataDeviceId); + + @Select("select de.* from wvp_device de left join wvp_device_channel dc on de.device_id = dc.device_id where dc.data_type = 1 and dc.device_id=#{channelId}") + List getDeviceByChannelDeviceId(@Param("channelId") String channelId); + + + @Delete({""}) + int batchDel(List deleteChannelList); + + @Update({""}) + int batchUpdateStatus(List channels); + + @Select("select count(1) from wvp_device_channel where status = 'ON'") + int getOnlineCount(); + + @Select("select count(1) from wvp_device_channel") + int getAllChannelCount(); + + @Update("") + void updateChannelStreamIdentification(DeviceChannel channel); + + @Update("") + void updateAllChannelStreamIdentification(@Param("streamIdentification") String streamIdentification); + + @Update({""}) + void batchUpdatePosition(List channelList); + + @SelectProvider(type = DeviceChannelProvider.class, method = "getOne") + DeviceChannel getOne(@Param("id") int id); + + @Select(value = {" "}) + DeviceChannel getOneForSource(@Param("id") int id); + + @SelectProvider(type = DeviceChannelProvider.class, method = "getOneByDeviceId") + DeviceChannel getOneByDeviceId(@Param("dataDeviceId") int dataDeviceId, @Param("channelId") String channelId); + + + @Select(value = {" "}) + DeviceChannel getOneByDeviceIdForSource(@Param("dataDeviceId") int dataDeviceId, @Param("channelId") String channelId); + + + @Update(value = {"UPDATE wvp_device_channel SET stream_id=null WHERE id=#{channelId}"}) + void stopPlayById(@Param("channelId") Integer channelId); + + @Update(value = {" "}) + void changeAudio(@Param("channelId") int channelId, @Param("audio") boolean audio); + + @Update("UPDATE wvp_device_channel SET status=#{status} WHERE data_type=#{dataType} and data_device_id=#{dataDeviceId} AND device_id=#{deviceId}") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void updateStatus(DeviceChannel channel); + + @Update({""}) + void updateChannelForNotify(DeviceChannel channel); + + + @Select(value = {" "}) + DeviceChannel getOneBySourceChannelId(@Param("dataDeviceId") int dataDeviceId, @Param("channelId") String channelId); + + @Update(value = {"UPDATE wvp_device_channel SET status = 'OFF' WHERE data_type = 1 and data_device_id=#{deviceId}"}) + void offlineByDeviceId(@Param("deviceId") int deviceId); + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMapper.java new file mode 100755 index 0000000..ded0297 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMapper.java @@ -0,0 +1,505 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 用于存储设备信息 + */ +@Mapper +@Repository +public interface DeviceMapper { + + @Select("SELECT " + + "id, " + + "device_id, " + + "coalesce(custom_name, name) as name, " + + "password, " + + "manufacturer, " + + "model, " + + "firmware, " + + "transport," + + "stream_mode," + + "ip," + + "sdp_ip," + + "local_ip," + + "port," + + "host_address," + + "expires," + + "register_time," + + "keepalive_time," + + "create_time," + + "update_time," + + "charset," + + "subscribe_cycle_for_catalog," + + "subscribe_cycle_for_mobile_position," + + "mobile_position_submission_interval," + + "subscribe_cycle_for_alarm," + + "ssrc_check," + + "as_message_channel," + + "geo_coord_sys," + + "on_line," + + "server_id,"+ + "media_server_id," + + "broadcast_push_after_ack," + + "(SELECT count(0) FROM wvp_device_channel dc WHERE dc.data_type = 1 and dc.data_device_id= de.id) as channel_count "+ + " FROM wvp_device de WHERE de.device_id = #{deviceId}") + Device getDeviceByDeviceId( @Param("deviceId") String deviceId); + + @Insert("INSERT INTO wvp_device (" + + "device_id, " + + "name, " + + "manufacturer, " + + "model, " + + "firmware, " + + "transport," + + "stream_mode," + + "media_server_id," + + "ip," + + "sdp_ip," + + "local_ip," + + "port," + + "host_address," + + "expires," + + "register_time," + + "keepalive_time," + + "heart_beat_interval," + + "heart_beat_count," + + "position_capability," + + "create_time," + + "update_time," + + "charset," + + "subscribe_cycle_for_catalog," + + "subscribe_cycle_for_mobile_position,"+ + "mobile_position_submission_interval,"+ + "subscribe_cycle_for_alarm,"+ + "ssrc_check,"+ + "as_message_channel,"+ + "broadcast_push_after_ack,"+ + "geo_coord_sys,"+ + "server_id,"+ + "on_line"+ + ") VALUES (" + + "#{deviceId}," + + "#{name}," + + "#{manufacturer}," + + "#{model}," + + "#{firmware}," + + "#{transport}," + + "#{streamMode}," + + "#{mediaServerId}," + + "#{ip}," + + "#{sdpIp}," + + "#{localIp}," + + "#{port}," + + "#{hostAddress}," + + "#{expires}," + + "#{registerTime}," + + "#{keepaliveTime}," + + "#{heartBeatInterval}," + + "#{heartBeatCount}," + + "#{positionCapability}," + + "#{createTime}," + + "#{updateTime}," + + "#{charset}," + + "#{subscribeCycleForCatalog}," + + "#{subscribeCycleForMobilePosition}," + + "#{mobilePositionSubmissionInterval}," + + "#{subscribeCycleForAlarm}," + + "#{ssrcCheck}," + + "#{asMessageChannel}," + + "#{broadcastPushAfterAck}," + + "#{geoCoordSys}," + + "#{serverId}," + + "#{onLine}" + + ")") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int add(Device device); + + @Update(value = {" "}) + int update(Device device); + + @Select( + " " + ) + List getDevices(@Param("dataType") Integer dataType, @Param("online") Boolean online); + + @Delete("DELETE FROM wvp_device WHERE device_id=#{deviceId}") + int del(String deviceId); + + @Select("SELECT " + + "id, " + + "device_id, " + + "coalesce(custom_name, name) as name, " + + "password, " + + "manufacturer, " + + "model, " + + "firmware, " + + "transport," + + "stream_mode," + + "ip," + + "sdp_ip,"+ + "local_ip,"+ + "port,"+ + "host_address,"+ + "expires,"+ + "register_time,"+ + "keepalive_time,"+ + "create_time,"+ + "update_time,"+ + "charset,"+ + "subscribe_cycle_for_catalog,"+ + "subscribe_cycle_for_mobile_position,"+ + "mobile_position_submission_interval,"+ + "subscribe_cycle_for_alarm,"+ + "ssrc_check,"+ + "as_message_channel,"+ + "broadcast_push_after_ack,"+ + "geo_coord_sys,"+ + "server_id,"+ + "on_line"+ + " FROM wvp_device WHERE on_line = true") + List getOnlineDevices(); + + @Select("SELECT " + + "id, " + + "device_id, " + + "coalesce(custom_name, name) as name, " + + "password, " + + "manufacturer, " + + "model, " + + "firmware, " + + "transport," + + "stream_mode," + + "ip," + + "sdp_ip,"+ + "local_ip,"+ + "port,"+ + "host_address,"+ + "expires,"+ + "register_time,"+ + "keepalive_time,"+ + "create_time,"+ + "update_time,"+ + "charset,"+ + "subscribe_cycle_for_catalog,"+ + "subscribe_cycle_for_mobile_position,"+ + "mobile_position_submission_interval,"+ + "subscribe_cycle_for_alarm,"+ + "ssrc_check,"+ + "media_server_id,"+ + "as_message_channel,"+ + "broadcast_push_after_ack,"+ + "geo_coord_sys,"+ + "server_id,"+ + "on_line"+ + " FROM wvp_device WHERE on_line = true and server_id = #{serverId}") + List getOnlineDevicesByServerId(@Param("serverId") String serverId); + + @Select("SELECT " + + "id,"+ + "device_id,"+ + "coalesce(custom_name,name)as name,"+ + "password,"+ + "manufacturer,"+ + "model,"+ + "firmware,"+ + "transport,"+ + "stream_mode,"+ + "ip,"+ + "sdp_ip,"+ + "local_ip,"+ + "port,"+ + "host_address,"+ + "expires,"+ + "register_time,"+ + "keepalive_time,"+ + "create_time,"+ + "update_time,"+ + "charset,"+ + "subscribe_cycle_for_catalog,"+ + "subscribe_cycle_for_mobile_position,"+ + "mobile_position_submission_interval,"+ + "subscribe_cycle_for_alarm,"+ + "ssrc_check,"+ + "as_message_channel,"+ + "broadcast_push_after_ack,"+ + "geo_coord_sys,"+ + "on_line"+ + " FROM wvp_device WHERE ip = #{host} AND port=#{port}") + Device getDeviceByHostAndPort(@Param("host") String host, @Param("port") int port); + + @Update(value = {" "}) + void updateCustom(Device device); + + @Insert("INSERT INTO wvp_device (" + + "device_id,"+ + "custom_name,"+ + "password,"+ + "sdp_ip,"+ + "create_time,"+ + "update_time,"+ + "charset,"+ + "ssrc_check,"+ + "as_message_channel,"+ + "broadcast_push_after_ack,"+ + "geo_coord_sys,"+ + "on_line,"+ + "stream_mode," + + "server_id," + + "media_server_id"+ + ") VALUES (" + + "#{deviceId}," + + "#{name}," + + "#{password}," + + "#{sdpIp}," + + "#{createTime}," + + "#{updateTime}," + + "#{charset}," + + "#{ssrcCheck}," + + "#{asMessageChannel}," + + "#{broadcastPushAfterAck}," + + "#{geoCoordSys}," + + "#{onLine}," + + "#{streamMode}," + + "#{serverId}," + + "#{mediaServerId}" + + ")") + void addCustomDevice(Device device); + + @Select("select * FROM wvp_device") + List getAll(); + + @Select("select * FROM wvp_device where as_message_channel = true") + List queryDeviceWithAsMessageChannel(); + + @Select(" ") + List getDeviceList(@Param("dataType") Integer dataType, @Param("query") String query, @Param("status") Boolean status); + + @Select("select * from wvp_device_channel where id = #{id}") + DeviceChannel getRawChannel(@Param("id") int id); + + @Select("select * from wvp_device where id = #{id}") + Device query(@Param("id") Integer id); + + @Select("select wd.* from wvp_device wd left join wvp_device_channel wdc on wdc.data_type = #{dataType} and wd.id = wdc.data_device_id where wdc.id = #{channelId}") + Device queryByChannelId(@Param("dataType") Integer dataType, @Param("channelId") Integer channelId); + + @Select("select wd.* from wvp_device wd left join wvp_device_channel wdc on wdc.data_type = #{dataType} and wd.id = wdc.data_device_id where wdc.device_id = #{channelDeviceId}") + Device getDeviceBySourceChannelDeviceId(@Param("dataType") Integer dataType, @Param("channelDeviceId") String channelDeviceId); + + @Update(value = {" "}) + void updateSubscribeCatalog(Device device); + + @Update(value = {" "}) + void updateSubscribeMobilePosition(Device device); + + @Update(value = {" "}) + void offlineByList(List offlineDevices); + + + @Update({""}) + void batchUpdate(List devices); + + + @Select(value = {" "}) + List queryByDeviceIds(List deviceIds); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMobilePositionMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMobilePositionMapper.java new file mode 100755 index 0000000..3e09df8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMobilePositionMapper.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +@Mapper +public interface DeviceMobilePositionMapper { + + @Insert("INSERT INTO wvp_device_mobile_position (device_id,channel_id, device_name,time,longitude,latitude,altitude,speed,direction,report_source,create_time)"+ + "VALUES (#{deviceId}, #{channelId}, #{deviceName}, #{time}, #{longitude}, #{latitude}, #{altitude}, #{speed}, #{direction}, #{reportSource}, #{createTime})") + int insertNewPosition(MobilePosition mobilePosition); + + @Select(value = {" "}) + List queryPositionByDeviceIdAndTime(@Param("deviceId") String deviceId, @Param("channelId") String channelId, @Param("startTime") String startTime, @Param("endTime") String endTime); + + @Select("SELECT * FROM wvp_device_mobile_position WHERE device_id = #{deviceId}" + + " ORDER BY time DESC LIMIT 1") + MobilePosition queryLatestPositionByDevice(String deviceId); + + @Delete("DELETE FROM wvp_device_mobile_position WHERE device_id = #{deviceId}") + int clearMobilePositionsByDeviceId(String deviceId); + + @Insert("") + void batchadd(List mobilePositions); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/GroupMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/GroupMapper.java new file mode 100644 index 0000000..1edee01 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/GroupMapper.java @@ -0,0 +1,323 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Group; +import com.genersoft.iot.vmp.gb28181.bean.GroupTree; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.web.custom.bean.CameraCount; +import com.genersoft.iot.vmp.web.custom.bean.CameraGroup; +import org.apache.ibatis.annotations.*; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Mapper +public interface GroupMapper { + + @Insert("INSERT INTO wvp_common_group (device_id, name, parent_id, parent_device_id, business_group, create_time, update_time, civil_code, alias) " + + "VALUES (#{deviceId}, #{name}, #{parentId}, #{parentDeviceId}, #{businessGroup}, #{createTime}, #{updateTime}, #{civilCode}, #{alias})") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int add(Group group); + + @Insert("INSERT INTO wvp_common_group (device_id, name, business_group, create_time, update_time, civil_code, alias) " + + "VALUES (#{deviceId}, #{name}, #{businessGroup}, #{createTime}, #{updateTime}, #{civilCode}, #{alias})") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int addBusinessGroup(Group group); + + @Delete("DELETE FROM wvp_common_group WHERE id=#{id}") + int delete(@Param("id") int id); + + @Update(" UPDATE wvp_common_group " + + " SET update_time=#{updateTime}, device_id=#{deviceId}, name=#{name}, parent_id=#{parentId}, " + + " parent_device_id=#{parentDeviceId}, business_group=#{businessGroup}, civil_code=#{civilCode}, " + + " alias=#{alias}" + + " WHERE id = #{id}") + int update(Group group); + + @Select(value = {" "}) + List query(@Param("query") String query, @Param("parentId") String parentId, @Param("businessGroup") String businessGroup); + + @Select("SELECT * from wvp_common_group WHERE parent_id = #{parentId} ") + List getChildren(@Param("parentId") int parentId); + + @Select("SELECT * from wvp_common_group WHERE id = #{id} ") + Group queryOne(@Param("id") int id); + + + @Insert(" ") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int batchAdd(List groupList); + + @Select(" ") + List queryForTree(@Param("query") String query, @Param("parentId") Integer parentId); + + @Select(" ") + List queryForTreeByBusinessGroup(@Param("query") String query, + @Param("businessGroup") String businessGroup); + + @Select(" ") + List queryBusinessGroupForTree(@Param("query") String query); + + @Select("SELECT * from wvp_common_group WHERE device_id = #{deviceId} and business_group = #{businessGroup}") + Group queryOneByDeviceId(@Param("deviceId") String deviceId, @Param("businessGroup") String businessGroup); + + @Select("SELECT * from wvp_common_group WHERE device_id = #{deviceId}") + Group queryOneByOnlyDeviceId(@Param("deviceId") String deviceId); + + @Delete("") + int batchDelete(List allChildren); + + @Select("SELECT * from wvp_common_group WHERE device_id = #{businessGroup} and business_group = #{businessGroup} ") + Group queryBusinessGroup(@Param("businessGroup") String businessGroup); + + @Select("SELECT * from wvp_common_group WHERE business_group = #{businessGroup} ") + List queryByBusinessGroup(@Param("businessGroup") String businessGroup); + + @Select("SELECT * from wvp_common_group WHERE business_group = #{businessGroup}") + @MapKey("id") + Map queryByBusinessGroupForMap(@Param("businessGroup") String businessGroup); + + @Delete("DELETE FROM wvp_common_group WHERE business_group = #{businessGroup}") + int deleteByBusinessGroup(@Param("businessGroup") String businessGroup); + + @Update(" UPDATE wvp_common_group " + + " SET parent_device_id=#{group.deviceId}, business_group = #{group.businessGroup}" + + " WHERE parent_id = #{parentId}") + int updateChild(@Param("parentId") Integer parentId, Group group); + + @Select(" ") + List queryInGroupListByDeviceId(List groupList); + + @Select(" ") + Set queryInChannelList(List channelList); + + @Select(" ") + Set queryParentInChannelList(Set groupSet); + + @Select(" ") + List queryForPlatform(@Param("platformId") Integer platformId); + + @Select(" ") + Set queryNotShareGroupForPlatformByChannelList(List channelList, @Param("platformId") Integer platformId); + + @Select(" ") + Set queryNotShareGroupForPlatformByGroupList(Set allGroup, @Param("platformId") Integer platformId); + + + @Select(" ") + Set queryByChannelList(List channelList); + + @Update(value = " ", databaseId = "mysql") + @Update(value = " ", databaseId = "h2") + @Update( value = " ", databaseId = "postgresql") + @Update( value = " ", databaseId = "kingbase") + void updateParentId(List groupListForAdd); + + @Update(value = " ", databaseId = "mysql") + @Update(value = " ", databaseId = "h2") + @Update( value = " ", databaseId = "kingbase") + @Update( value = " ", databaseId = "postgresql") + void updateParentIdWithBusinessGroup(List groupListForAdd); + + @Select(" ") + List queryForPlatformByGroupId(@Param("groupId") int groupId); + + @Delete("DELETE FROM wvp_platform_group WHERE group_id = #{groupId}") + void deletePlatformGroup(@Param("groupId") int groupId); + + @Select("SELECT * from wvp_common_group WHERE alias = #{alias} ") + CameraGroup queryGroupByAlias(@Param("alias") String alias); + + @Select("SELECT * from wvp_common_group WHERE alias = #{alias} and business_group = #{businessGroup}") + Group queryGroupByAliasAndBusinessGroup(@Param("alias") String alias, @Param("deviceId") String businessGroup); + + + @Select("") + List queryCountWithChild(List groupList); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/PlatformChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/PlatformChannelMapper.java new file mode 100755 index 0000000..8d44f8d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/PlatformChannelMapper.java @@ -0,0 +1,551 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +@Mapper +@Repository +public interface PlatformChannelMapper { + + + @Insert("") + int addChannels(@Param("platformId") Integer platformId, @Param("channelList") List channelList); + + @Delete("") + int delChannelForDeviceId(String deviceId); + + @Select("select d.*\n" + + "from wvp_platform_channel pgc\n" + + " left join wvp_device_channel dc on dc.id = pgc.device_channel_id\n" + + " left join wvp_device d on dc.device_id = d.device_id\n" + + "where dc.channel_type = 0 and dc.channel_id = #{channelId} and pgc.platform_id=#{platformId}") + List queryDeviceByPlatformIdAndChannelId(@Param("platformId") String platformId, @Param("channelId") String channelId); + + @Select(" ") + List queryPlatFormListForGBWithGBId(@Param("channelId") Integer channelId, List platforms); + + @Select("select dc.channel_id, dc.device_id,dc.name,d.manufacturer,d.model,d.firmware\n" + + "from wvp_platform_channel pgc\n" + + " left join wvp_device_channel dc on dc.id = pgc.device_channel_id\n" + + " left join wvp_device d on dc.device_id = d.device_id\n" + + "where dc.channel_type = 0 and dc.channel_id = #{channelId} and pgc.platform_id=#{platformId}") + List queryDeviceInfoByPlatformIdAndChannelId(@Param("platformId") String platformId, @Param("channelId") String channelId); + + @Select(" SELECT wp.* from wvp_platform_channel pgc " + + " left join wvp_device_channel dc on dc.id = pgc.device_channel_id " + + " left join wvp_platform wp on wp.id = pgc.platform_id" + + " WHERE dc.channel_type = 0 and dc.device_id=#{channelId}") + List queryParentPlatformByChannelId(@Param("channelId") String channelId); + + @Select("") + List queryForPlatformForWebList(@Param("platformId") Integer platformId, @Param("query") String query, + @Param("dataType") Integer dataType, @Param("online") Boolean online, + @Param("hasShare") Boolean hasShare); + + @Select("select\n" + + " wdc.id as gb_id,\n" + + " wdc.data_type,\n" + + " wdc.data_device_id,\n" + + " wdc.create_time,\n" + + " wdc.update_time,\n" + + " coalesce(wpgc.custom_device_id, wdc.gb_device_id, wdc.device_id) as gb_device_id,\n" + + " coalesce(wpgc.custom_name, wdc.gb_name, wdc.name) as gb_name,\n" + + " coalesce(wpgc.custom_manufacturer, wdc.gb_manufacturer, wdc.manufacturer) as gb_manufacturer,\n" + + " coalesce(wpgc.custom_model, wdc.gb_model, wdc.model) as gb_model,\n" + + " coalesce(wpgc.custom_owner, wdc.gb_owner, wdc.owner) as gb_owner,\n" + + " coalesce(wpgc.custom_civil_code, wdc.gb_civil_code, wdc.civil_code) as gb_civil_code,\n" + + " coalesce(wpgc.custom_block, wdc.gb_block, wdc.block) as gb_block,\n" + + " coalesce(wpgc.custom_address, wdc.gb_address, wdc.address) as gb_address,\n" + + " coalesce(wpgc.custom_parental, wdc.gb_parental, wdc.parental) as gb_parental,\n" + + " coalesce(wpgc.custom_parent_id, wdc.gb_parent_id, wdc.parent_id) as gb_parent_id,\n" + + " coalesce(wpgc.custom_safety_way, wdc.gb_safety_way, wdc.safety_way) as gb_safety_way,\n" + + " coalesce(wpgc.custom_register_way, wdc.gb_register_way, wdc.register_way) as gb_register_way,\n" + + " coalesce(wpgc.custom_cert_num, wdc.gb_cert_num, wdc.cert_num) as gb_cert_num,\n" + + " coalesce(wpgc.custom_certifiable, wdc.gb_certifiable, wdc.certifiable) as gb_certifiable,\n" + + " coalesce(wpgc.custom_err_code, wdc.gb_err_code, wdc.err_code) as gb_err_code,\n" + + " coalesce(wpgc.custom_end_time, wdc.gb_end_time, wdc.end_time) as gb_end_time,\n" + + " coalesce(wpgc.custom_secrecy, wdc.gb_secrecy, wdc.secrecy) as gb_secrecy,\n" + + " coalesce(wpgc.custom_ip_address, wdc.gb_ip_address, wdc.ip_address) as gb_ip_address,\n" + + " coalesce(wpgc.custom_port, wdc.gb_port, wdc.port) as gb_port,\n" + + " coalesce(wpgc.custom_password, wdc.gb_password, wdc.password) as gb_password,\n" + + " coalesce(wpgc.custom_status, wdc.gb_status, wdc.status) as gb_status,\n" + + " coalesce(wpgc.custom_longitude, wdc.gb_longitude, wdc.longitude) as gb_longitude,\n" + + " coalesce(wpgc.custom_latitude, wdc.gb_latitude, wdc.latitude) as gb_latitude,\n" + + " coalesce(wpgc.custom_ptz_type, wdc.gb_ptz_type, wdc.ptz_type) as gb_ptz_type,\n" + + " coalesce(wpgc.custom_position_type, wdc.gb_position_type, wdc.position_type) as gb_position_type,\n" + + " coalesce(wpgc.custom_room_type, wdc.gb_room_type, wdc.room_type) as gb_room_type,\n" + + " coalesce(wpgc.custom_use_type, wdc.gb_use_type, wdc.use_type) as gb_use_type,\n" + + " coalesce(wpgc.custom_supply_light_type, wdc.gb_supply_light_type, wdc.supply_light_type) as gb_supply_light_type,\n" + + " coalesce(wpgc.custom_direction_type, wdc.gb_direction_type, wdc.direction_type) as gb_direction_type,\n" + + " coalesce(wpgc.custom_resolution, wdc.gb_resolution, wdc.resolution) as gb_resolution,\n" + + " coalesce(wpgc.custom_business_group_id, wdc.gb_business_group_id, wdc.business_group_id) as gb_business_group_id,\n" + + " coalesce(wpgc.custom_download_speed, wdc.gb_download_speed, wdc.download_speed) as gb_download_speed,\n" + + " coalesce(wpgc.custom_svc_space_support_mod, wdc.gb_svc_space_support_mod, wdc.svc_space_support_mod) as gb_svc_space_support_mod,\n" + + " coalesce(wpgc.custom_svc_time_support_mode, wdc.gb_svc_time_support_mode, wdc.svc_time_support_mode) as gb_svc_time_support_mode\n" + + " from wvp_device_channel wdc" + + " left join wvp_platform_channel wpgc on wdc.id = wpgc.device_channel_id" + + " where wdc.channel_type = 0 and wpgc.platform_id = #{platformId} and coalesce(wpgc.custom_device_id, wdc.gb_device_id, wdc.device_id) = #{channelDeviceId} order by wdc.id " + + ) + List queryOneWithPlatform(@Param("platformId") Integer platformId, @Param("channelDeviceId") String channelDeviceId); + + + @Select("") + List queryNotShare(@Param("platformId") Integer platformId, List channelIds); + + @Select("") + List queryShare(@Param("platformId") Integer platformId, List channelIds); + + @Delete("") + int removeChannelsWithPlatform(@Param("platformId") Integer platformId, List channelList); + + @Delete("") + int removeChannels(List channelList); + + @Insert("") + int addPlatformGroup(Collection groupListNotShare, @Param("platformId") Integer platformId); + + @Insert("") + int addPlatformRegion(List regionListNotShare, @Param("platformId") Integer platformId); + + @Delete("") + int removePlatformGroup(List groupList, @Param("platformId") Integer platformId); + + @Delete("") + void removePlatformGroupById(@Param("id") int id, @Param("platformId") Integer platformId); + + @Delete("") + void removePlatformRegionById(@Param("id") int id, @Param("platformId") Integer platformId); + + @Select(" ") + Set queryShareChildrenGroup(@Param("parentId") Integer parentId, @Param("platformId") Integer platformId); + + @Select(" ") + Set queryShareChildrenRegion(@Param("parentId") String parentId, @Param("platformId") Integer platformId); + + @Select(" ") + Set queryShareParentGroupByGroupSet(Set groupSet, @Param("platformId") Integer platformId); + + @Select(" ") + Set queryShareParentRegionByRegionSet(Set regionSet, @Param("platformId") Integer platformId); + + @Select(" ") + List queryPlatFormListByChannelList(Collection ids); + + @Select(" ") + List queryPlatFormListByChannelId(@Param("channelId") int channelId); + + @Delete("") + void removeChannelsByPlatformId(@Param("platformId") Integer platformId); + + @Delete("") + void removePlatformGroupsByPlatformId(@Param("platformId") Integer platformId); + + @Delete("") + void removePlatformRegionByPlatformId(@Param("platformId") Integer platformId); + + @Update(value = {" "}) + void updateCustomChannel(PlatformChannel channel); + + + @Select("") + CommonGBChannel queryShareChannel(@Param("platformId") int platformId, @Param("gbId") int gbId); + + + @Select(" ") + Set queryShareGroup(@Param("platformId") Integer platformId); + + @Select(" ") + Set queryShareRegion(Integer id); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/PlatformMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/PlatformMapper.java new file mode 100755 index 0000000..bb3c0f0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/PlatformMapper.java @@ -0,0 +1,110 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 用于存储上级平台 + */ +@Mapper +@Repository +public interface PlatformMapper { + + @Insert("INSERT INTO wvp_platform (enable, name, server_gb_id, server_gb_domain, server_ip, server_port,device_gb_id,device_ip,"+ + " device_port,username,password,expires,keep_timeout,transport,character_set,ptz,rtcp,status,catalog_group, update_time," + + " create_time, as_message_channel, send_stream_ip, auto_push_channel, catalog_with_platform,catalog_with_group,catalog_with_region, "+ + " civil_code,manufacturer,model,address,register_way,secrecy,server_id) " + + " VALUES (#{enable}, #{name}, #{serverGBId}, #{serverGBDomain}, #{serverIp}, #{serverPort}, #{deviceGBId}, #{deviceIp}, " + + " #{devicePort}, #{username}, #{password}, #{expires}, #{keepTimeout}, #{transport}, #{characterSet}, #{ptz}, #{rtcp}, #{status}, #{catalogGroup},#{updateTime}," + + " #{createTime}, #{asMessageChannel}, #{sendStreamIp}, #{autoPushChannel}, #{catalogWithPlatform}, #{catalogWithGroup},#{catalogWithRegion}, " + + " #{civilCode}, #{manufacturer}, #{model}, #{address}, #{registerWay}, #{secrecy}, #{serverId})") + int add(Platform parentPlatform); + + @Update("UPDATE wvp_platform " + + "SET update_time = #{updateTime}," + + " enable=#{enable}, " + + " name=#{name}," + + " server_gb_id=#{serverGBId}, " + + " server_gb_domain=#{serverGBDomain}, " + + " server_ip=#{serverIp}," + + " server_port=#{serverPort}, " + + " device_gb_id=#{deviceGBId}," + + " device_ip=#{deviceIp}, " + + " device_port=#{devicePort}, " + + " username=#{username}, " + + " password=#{password}, " + + " expires=#{expires}, " + + " keep_timeout=#{keepTimeout}, " + + " transport=#{transport}, " + + " character_set=#{characterSet}, " + + " ptz=#{ptz}, " + + " rtcp=#{rtcp}, " + + " status=#{status}, " + + " catalog_group=#{catalogGroup}, " + + " as_message_channel=#{asMessageChannel}, " + + " send_stream_ip=#{sendStreamIp}, " + + " auto_push_channel=#{autoPushChannel}, " + + " catalog_with_platform=#{catalogWithPlatform}, " + + " catalog_with_group=#{catalogWithGroup}, " + + " catalog_with_region=#{catalogWithRegion}, " + + " civil_code=#{civilCode}, " + + " manufacturer=#{manufacturer}, " + + " model=#{model}, " + + " address=#{address}, " + + " register_way=#{registerWay}, " + + " server_id=#{serverId}, " + + " secrecy=#{secrecy} " + + "WHERE id=#{id}") + int update(Platform parentPlatform); + + @Delete("DELETE FROM wvp_platform WHERE id=#{id}") + int delete(@Param("id") Integer id); + + @Select(" ") + List queryList(@Param("query") String query); + + @Select("SELECT * FROM wvp_platform WHERE server_id=#{serverId} and enable=#{enable} ") + List queryEnableParentPlatformListByServerId(@Param("serverId") String serverId, @Param("enable") boolean enable); + + @Select("SELECT * FROM wvp_platform WHERE enable=true and as_message_channel=true") + List queryEnablePlatformListWithAsMessageChannel(); + + @Select("SELECT * FROM wvp_platform WHERE server_gb_id=#{platformGbId}") + Platform getParentPlatByServerGBId(String platformGbId); + + @Select("SELECT * FROM wvp_platform WHERE id=#{id}") + Platform query(int id); + + @Update("UPDATE wvp_platform SET status=#{online}, server_id = #{serverId} WHERE id=#{id}" ) + int updateStatus(@Param("id") int id, @Param("online") boolean online, @Param("serverId") String serverId); + + @Select("SELECT server_id FROM wvp_platform WHERE enable=true and server_id != #{serverId} group by server_id") + List queryServerIdsWithEnableAndNotInServer(@Param("serverId") String serverId); + + @Select("SELECT * FROM wvp_platform WHERE server_id = #{serverId}") + List queryByServerId(@Param("serverId") String serverId); + + @Select("SELECT * FROM wvp_platform ") + List queryAll(); + + @Select("SELECT * FROM wvp_platform WHERE enable=true and server_id = #{serverId}") + List queryServerIdsWithEnableAndServer(@Param("serverId") String serverId); + + @Update("UPDATE wvp_platform SET status=false where server_id = #{serverId}" ) + void offlineAll(@Param("serverId") String serverId); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/RegionMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/RegionMapper.java new file mode 100644 index 0000000..3d67be0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/RegionMapper.java @@ -0,0 +1,197 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.common.CivilCodePo; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Region; +import com.genersoft.iot.vmp.gb28181.bean.RegionTree; +import org.apache.ibatis.annotations.*; + +import java.util.List; +import java.util.Set; + +@Mapper +public interface RegionMapper { + + @Insert("INSERT INTO wvp_common_region (device_id, name, parent_id, parent_device_id, create_time, update_time) " + + "VALUES (#{deviceId}, #{name}, #{parentId}, #{parentDeviceId}, #{createTime}, #{updateTime})") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void add(Region region); + + @Delete("DELETE FROM wvp_common_region WHERE id=#{id}") + int delete(@Param("id") int id); + + @Update(" UPDATE wvp_common_region " + + " SET update_time=#{updateTime}, device_id=#{deviceId}, name=#{name}, parent_id=#{parentId}, parent_device_id=#{parentDeviceId}" + + " WHERE id = #{id}") + int update(Region region); + + @Select(value = {" "}) + List query(@Param("query") String query, @Param("parentId") String parentId); + + @Select("SELECT * from wvp_common_region WHERE parent_id = #{parentId} ORDER BY id ") + List getChildren(@Param("parentId") Integer parentId); + + @Select("SELECT * from wvp_common_region WHERE id = #{id} ") + Region queryOne(@Param("id") int id); + + @Select(" select dc.civil_code as civil_code " + + " from wvp_device_channel dc " + + " where dc.civil_code not in " + + " (select device_id from wvp_common_region)") + List getUninitializedCivilCode(); + + @Select(" ") + List queryInList(Set codes); + + + @Insert(" ") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int batchAdd(List regionList); + + @Select(" ") + List queryForTree(@Param("parentId") Integer parentId); + + @Delete("") + void batchDelete(List allChildren); + + @Select(" ") + List queryInRegionListByDeviceId(List regionList); + + @Select(" ") + List queryByPlatform(@Param("platformId") Integer platformId); + + + @Update(value = " ", databaseId = "mysql") + @Update(value = " ", databaseId = "h2") + @Update( value = " ", databaseId = "kingbase") + @Update( value = " ", databaseId = "postgresql") + void updateParentId(List regionListForAdd); + + @Update(" ") + void updateChild(@Param("parentId") int parentId, @Param("parentDeviceId") String parentDeviceId); + + @Select("SELECT * from wvp_common_region WHERE device_id = #{deviceId} ") + Region queryByDeviceId(@Param("deviceId") String deviceId); + + @Select(" ") + Set queryParentInChannelList(Set regionSet); + + @Select(" ") + Set queryByChannelList(List channelList); + + @Select(" ") + Set queryNotShareRegionForPlatformByChannelList(List channelList, @Param("platformId") Integer platformId); + + @Select(" ") + Set queryNotShareRegionForPlatformByRegionList(Set allRegion, @Param("platformId") Integer platformId); + + + @Select(" ") + Set queryInCivilCodePoList(List civilCodePoList); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java new file mode 100644 index 0000000..96f3cba --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java @@ -0,0 +1,955 @@ +package com.genersoft.iot.vmp.gb28181.dao.provider; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Group; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.web.custom.bean.CameraGroup; +import com.genersoft.iot.vmp.web.custom.bean.Point; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class ChannelProvider { + + public final static String BASE_SQL = "select\n" + + " id as gb_id,\n" + + " data_type,\n" + + " data_device_id,\n" + + " create_time,\n" + + " update_time,\n" + + " stream_id,\n" + + " record_plan_id,\n" + + " enable_broadcast,\n" + + " map_level,\n" + + " coalesce(gb_device_id, device_id) as gb_device_id,\n" + + " coalesce(gb_name, name) as gb_name,\n" + + " coalesce(gb_manufacturer, manufacturer) as gb_manufacturer,\n" + + " coalesce(gb_model, model) as gb_model,\n" + + " coalesce(gb_owner, owner) as gb_owner,\n" + + " coalesce(gb_civil_code, civil_code) as gb_civil_code,\n" + + " coalesce(gb_block, block) as gb_block,\n" + + " coalesce(gb_address, address) as gb_address,\n" + + " coalesce(gb_parental, parental) as gb_parental,\n" + + " coalesce(gb_parent_id, parent_id) as gb_parent_id,\n" + + " coalesce(gb_safety_way, safety_way) as gb_safety_way,\n" + + " coalesce(gb_register_way, register_way) as gb_register_way,\n" + + " coalesce(gb_cert_num, cert_num) as gb_cert_num,\n" + + " coalesce(gb_certifiable, certifiable) as gb_certifiable,\n" + + " coalesce(gb_err_code, err_code) as gb_err_code,\n" + + " coalesce(gb_end_time, end_time) as gb_end_time,\n" + + " coalesce(gb_secrecy, secrecy) as gb_secrecy,\n" + + " coalesce(gb_ip_address, ip_address) as gb_ip_address,\n" + + " coalesce(gb_port, port) as gb_port,\n" + + " coalesce(gb_password, password) as gb_password,\n" + + " coalesce(gb_status, status) as gb_status,\n" + + " coalesce(gb_longitude, longitude) as gb_longitude,\n" + + " coalesce(gb_latitude, latitude) as gb_latitude,\n" + + " coalesce(gb_ptz_type, ptz_type) as gb_ptz_type,\n" + + " coalesce(gb_position_type, position_type) as gb_position_type,\n" + + " coalesce(gb_room_type, room_type) as gb_room_type,\n" + + " coalesce(gb_use_type, use_type) as gb_use_type,\n" + + " coalesce(gb_supply_light_type, supply_light_type) as gb_supply_light_type,\n" + + " coalesce(gb_direction_type, direction_type) as gb_direction_type,\n" + + " coalesce(gb_resolution, resolution) as gb_resolution,\n" + + " coalesce(gb_business_group_id, business_group_id) as gb_business_group_id,\n" + + " coalesce(gb_download_speed, download_speed) as gb_download_speed,\n" + + " coalesce(gb_svc_space_support_mod, svc_space_support_mod) as gb_svc_space_support_mod,\n" + + " coalesce(gb_svc_time_support_mode,svc_time_support_mode) as gb_svc_time_support_mode\n" + + " from wvp_device_channel\n" + ; + + public final static String BASE_SQL_TABLE_NAME = "select\n" + + " wdc.id as gb_id,\n" + + " wdc.data_type,\n" + + " wdc.data_device_id,\n" + + " wdc.create_time,\n" + + " wdc.update_time,\n" + + " wdc.stream_id,\n" + + " wdc.record_plan_id,\n" + + " wdc.enable_broadcast,\n" + + " coalesce(wdc.gb_device_id, wdc.device_id) as gb_device_id,\n" + + " coalesce(wdc.gb_name, wdc.name) as gb_name,\n" + + " coalesce(wdc.gb_manufacturer, wdc.manufacturer) as gb_manufacturer,\n" + + " coalesce(wdc.gb_model, wdc.model) as gb_model,\n" + + " coalesce(wdc.gb_owner, wdc.owner) as gb_owner,\n" + + " coalesce(wdc.gb_civil_code, wdc.civil_code) as gb_civil_code,\n" + + " coalesce(wdc.gb_block, wdc.block) as gb_block,\n" + + " coalesce(wdc.gb_address, wdc.address) as gb_address,\n" + + " coalesce(wdc.gb_parental, wdc.parental) as gb_parental,\n" + + " coalesce(wdc.gb_parent_id, wdc.parent_id) as gb_parent_id,\n" + + " coalesce(wdc.gb_safety_way, wdc.safety_way) as gb_safety_way,\n" + + " coalesce(wdc.gb_register_way, wdc.register_way) as gb_register_way,\n" + + " coalesce(wdc.gb_cert_num, wdc.cert_num) as gb_cert_num,\n" + + " coalesce(wdc.gb_certifiable, wdc.certifiable) as gb_certifiable,\n" + + " coalesce(wdc.gb_err_code, wdc.err_code) as gb_err_code,\n" + + " coalesce(wdc.gb_end_time, wdc.end_time) as gb_end_time,\n" + + " coalesce(wdc.gb_secrecy, wdc.secrecy) as gb_secrecy,\n" + + " coalesce(wdc.gb_ip_address, wdc.ip_address) as gb_ip_address,\n" + + " coalesce(wdc.gb_port, wdc.port) as gb_port,\n" + + " coalesce(wdc.gb_password, wdc.password) as gb_password,\n" + + " coalesce(wdc.gb_status, wdc.status) as gb_status,\n" + + " coalesce(wdc.gb_longitude, wdc.longitude) as gb_longitude,\n" + + " coalesce(wdc.gb_latitude, wdc.latitude) as gb_latitude,\n" + + " coalesce(wdc.gb_ptz_type, wdc.ptz_type) as gb_ptz_type,\n" + + " coalesce(wdc.gb_position_type, wdc.position_type) as gb_position_type,\n" + + " coalesce(wdc.gb_room_type, wdc.room_type) as gb_room_type,\n" + + " coalesce(wdc.gb_use_type, wdc.use_type) as gb_use_type,\n" + + " coalesce(wdc.gb_supply_light_type, wdc.supply_light_type) as gb_supply_light_type,\n" + + " coalesce(wdc.gb_direction_type, wdc.direction_type) as gb_direction_type,\n" + + " coalesce(wdc.gb_resolution, wdc.resolution) as gb_resolution,\n" + + " coalesce(wdc.gb_business_group_id, wdc.business_group_id) as gb_business_group_id,\n" + + " coalesce(wdc.gb_download_speed, wdc.download_speed) as gb_download_speed,\n" + + " coalesce(wdc.gb_svc_space_support_mod, wdc.svc_space_support_mod) as gb_svc_space_support_mod,\n" + + " coalesce(wdc.gb_svc_time_support_mode, wdc.svc_time_support_mode) as gb_svc_time_support_mode\n" + + " from wvp_device_channel wdc\n" + ; + + private final static String BASE_SQL_FOR_PLATFORM = + "select\n" + + " wdc.id as gb_id,\n" + + " wdc.data_type,\n" + + " wdc.data_device_id,\n" + + " wdc.create_time,\n" + + " wdc.update_time,\n" + + " wdc.enable_broadcast,\n" + + " coalesce(wpgc.custom_device_id, wdc.gb_device_id, wdc.device_id) as gb_device_id,\n" + + " coalesce(wpgc.custom_name, wdc.gb_name, wdc.name) as gb_name,\n" + + " coalesce(wpgc.custom_manufacturer, wdc.gb_manufacturer, wdc.manufacturer) as gb_manufacturer,\n" + + " coalesce(wpgc.custom_model, wdc.gb_model, wdc.model) as gb_model,\n" + + " coalesce(wpgc.custom_owner, wdc.gb_owner, wdc.owner) as gb_owner,\n" + + " coalesce(wpgc.custom_civil_code, wdc.gb_civil_code, wdc.civil_code) as gb_civil_code,\n" + + " coalesce(wpgc.custom_block, wdc.gb_block, wdc.block) as gb_block,\n" + + " coalesce(wpgc.custom_address, wdc.gb_address, wdc.address) as gb_address,\n" + + " coalesce(wpgc.custom_parental, wdc.gb_parental, wdc.parental) as gb_parental,\n" + + " coalesce(wpgc.custom_parent_id, wdc.gb_parent_id, wdc.parent_id) as gb_parent_id,\n" + + " coalesce(wpgc.custom_safety_way, wdc.gb_safety_way, wdc.safety_way) as gb_safety_way,\n" + + " coalesce(wpgc.custom_register_way, wdc.gb_register_way, wdc.register_way) as gb_register_way,\n" + + " coalesce(wpgc.custom_cert_num, wdc.gb_cert_num, wdc.cert_num) as gb_cert_num,\n" + + " coalesce(wpgc.custom_certifiable, wdc.gb_certifiable, wdc.certifiable) as gb_certifiable,\n" + + " coalesce(wpgc.custom_err_code, wdc.gb_err_code, wdc.err_code) as gb_err_code,\n" + + " coalesce(wpgc.custom_end_time, wdc.gb_end_time, wdc.end_time) as gb_end_time,\n" + + " coalesce(wpgc.custom_secrecy, wdc.gb_secrecy, wdc.secrecy) as gb_secrecy,\n" + + " coalesce(wpgc.custom_ip_address, wdc.gb_ip_address, wdc.ip_address) as gb_ip_address,\n" + + " coalesce(wpgc.custom_port, wdc.gb_port, wdc.port) as gb_port,\n" + + " coalesce(wpgc.custom_password, wdc.gb_password, wdc.password) as gb_password,\n" + + " coalesce(wpgc.custom_status, wdc.gb_status, wdc.status) as gb_status,\n" + + " coalesce(wpgc.custom_longitude, wdc.gb_longitude, wdc.longitude) as gb_longitude,\n" + + " coalesce(wpgc.custom_latitude, wdc.gb_latitude, wdc.latitude) as gb_latitude,\n" + + " coalesce(wpgc.custom_ptz_type, wdc.gb_ptz_type, wdc.ptz_type) as gb_ptz_type,\n" + + " coalesce(wpgc.custom_position_type, wdc.gb_position_type, wdc.position_type) as gb_position_type,\n" + + " coalesce(wpgc.custom_room_type, wdc.gb_room_type, wdc.room_type) as gb_room_type,\n" + + " coalesce(wpgc.custom_use_type, wdc.gb_use_type, wdc.use_type) as gb_use_type,\n" + + " coalesce(wpgc.custom_supply_light_type, wdc.gb_supply_light_type, wdc.supply_light_type) as gb_supply_light_type,\n" + + " coalesce(wpgc.custom_direction_type, wdc.gb_direction_type, wdc.direction_type) as gb_direction_type,\n" + + " coalesce(wpgc.custom_resolution, wdc.gb_resolution, wdc.resolution) as gb_resolution,\n" + + " coalesce(wpgc.custom_business_group_id, wdc.gb_business_group_id, wdc.business_group_id) as gb_business_group_id,\n" + + " coalesce(wpgc.custom_download_speed, wdc.gb_download_speed, wdc.download_speed) as gb_download_speed,\n" + + " coalesce(wpgc.custom_svc_space_support_mod, wdc.gb_svc_space_support_mod, wdc.svc_space_support_mod) as gb_svc_space_support_mod,\n" + + " coalesce(wpgc.custom_svc_time_support_mode, wdc.gb_svc_time_support_mode, wdc.svc_time_support_mode) as gb_svc_time_support_mode\n" + + " from wvp_device_channel wdc" + + " left join wvp_platform_channel wpgc on wdc.id = wpgc.device_channel_id" + ; + + private final static String BASE_SQL_FOR_CAMERA_DEVICE = + "select\n" + + " wdc.id as gb_id,\n" + + " wdc.data_type,\n" + + " wdc.data_device_id,\n" + + " wdc.create_time,\n" + + " wdc.update_time,\n" + + " wdc.stream_id,\n" + + " wdc.record_plan_id,\n" + + " wdc.enable_broadcast,\n" + + " wd.device_id as deviceCode,\n" + + " wcg.alias as groupAlias,\n" + + " wcg2.alias as topGroupGAlias,\n" + + " coalesce(wdc.gb_device_id, wdc.device_id) as gb_device_id,\n" + + " coalesce(wdc.gb_name, wdc.name) as gb_name,\n" + + " coalesce(wdc.gb_manufacturer, wdc.manufacturer) as gb_manufacturer,\n" + + " coalesce(wdc.gb_model, wdc.model) as gb_model,\n" + + " coalesce(wdc.gb_owner, wdc.owner) as gb_owner,\n" + + " coalesce(wdc.gb_civil_code, wdc.civil_code) as gb_civil_code,\n" + + " coalesce(wdc.gb_block, wdc.block) as gb_block,\n" + + " coalesce(wdc.gb_address, wdc.address) as gb_address,\n" + + " coalesce(wdc.gb_parental, wdc.parental) as gb_parental,\n" + + " coalesce(wdc.gb_parent_id, wdc.parent_id) as gb_parent_id,\n" + + " coalesce(wdc.gb_safety_way, wdc.safety_way) as gb_safety_way,\n" + + " coalesce(wdc.gb_register_way, wdc.register_way) as gb_register_way,\n" + + " coalesce(wdc.gb_cert_num, wdc.cert_num) as gb_cert_num,\n" + + " coalesce(wdc.gb_certifiable, wdc.certifiable) as gb_certifiable,\n" + + " coalesce(wdc.gb_err_code, wdc.err_code) as gb_err_code,\n" + + " coalesce(wdc.gb_end_time, wdc.end_time) as gb_end_time,\n" + + " coalesce(wdc.gb_secrecy, wdc.secrecy) as gb_secrecy,\n" + + " coalesce(wdc.gb_ip_address, wdc.ip_address) as gb_ip_address,\n" + + " coalesce(wdc.gb_port, wdc.port) as gb_port,\n" + + " coalesce(wdc.gb_password, wdc.password) as gb_password,\n" + + " coalesce(wdc.gb_status, wdc.status) as gb_status,\n" + + " coalesce(wdc.gb_longitude, wdc.longitude) as gb_longitude,\n" + + " coalesce(wdc.gb_latitude, wdc.latitude) as gb_latitude,\n" + + " coalesce(wdc.gb_ptz_type, wdc.ptz_type) as gb_ptz_type,\n" + + " coalesce(wdc.gb_position_type, wdc.position_type) as gb_position_type,\n" + + " coalesce(wdc.gb_room_type, wdc.room_type) as gb_room_type,\n" + + " coalesce(wdc.gb_use_type, wdc.use_type) as gb_use_type,\n" + + " coalesce(wdc.gb_supply_light_type, wdc.supply_light_type) as gb_supply_light_type,\n" + + " coalesce(wdc.gb_direction_type, wdc.direction_type) as gb_direction_type,\n" + + " coalesce(wdc.gb_resolution, wdc.resolution) as gb_resolution,\n" + + " coalesce(wdc.gb_business_group_id, wdc.business_group_id) as gb_business_group_id,\n" + + " coalesce(wdc.gb_download_speed, wdc.download_speed) as gb_download_speed,\n" + + " coalesce(wdc.gb_svc_space_support_mod, wdc.svc_space_support_mod) as gb_svc_space_support_mod,\n" + + " coalesce(wdc.gb_svc_time_support_mode, wdc.svc_time_support_mode) as gb_svc_time_support_mode\n" + + " from wvp_device_channel wdc\n" + + " left join wvp_device wd on wdc.data_type = 1 AND wd.id = wdc.data_device_id" + + " left join wvp_common_group wcg on wcg.device_id = coalesce(wdc.gb_parent_id, wdc.parent_id)" + + " left join wvp_common_group wcg2 on wcg2.device_id = wcg.business_group" + ; + + public String queryByDeviceId(Map params ){ + return BASE_SQL + " where channel_type = 0 and coalesce(gb_device_id, device_id) = #{gbDeviceId}"; + } + + public String queryById(Map params ){ + return BASE_SQL + " where channel_type = 0 and id = #{gbId}"; + } + + public String queryByDataId(Map params ){ + return BASE_SQL + " where channel_type = 0 and data_type = #{dataType} and data_device_id = #{dataDeviceId}"; + } + + public String queryListByCivilCode(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" where channel_type = 0 "); + if (params.get("query") != null) { + sqlBuild.append(" AND (coalesce(gb_device_id, device_id) LIKE concat('%',#{query},'%') escape '/'" + + " OR coalesce(gb_name, name) LIKE concat('%',#{query},'%') escape '/' )") + ; + } + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'OFF'"); + } + if (params.get("civilCode") != null) { + sqlBuild.append(" AND coalesce(gb_civil_code, civil_code) = #{civilCode}"); + }else { + sqlBuild.append(" AND coalesce(gb_civil_code, civil_code) is null"); + } + if (params.get("dataType") != null) { + sqlBuild.append(" AND data_type = #{dataType}"); + } + return sqlBuild.toString(); + } + + public String queryListByParentId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" where channel_type = 0 "); + if (params.get("query") != null) { + sqlBuild.append(" AND (coalesce(gb_device_id, device_id) LIKE concat('%',#{query},'%') escape '/'" + + " OR coalesce(gb_name, name) LIKE concat('%',#{query},'%') escape '/' )") + ; + } + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'OFF'"); + } + if (params.get("groupDeviceId") != null) { + sqlBuild.append(" AND coalesce(gb_parent_id, parent_id) = #{groupDeviceId}"); + }else { + sqlBuild.append(" AND coalesce(gb_parent_id, parent_id) is null"); + } + if (params.get("dataType") != null) { + sqlBuild.append(" AND data_type = #{dataType}"); + } + return sqlBuild.toString(); + } + + public String queryList(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" where channel_type = 0 "); + if (params.get("query") != null) { + sqlBuild.append(" AND (coalesce(gb_device_id, device_id) LIKE concat('%',#{query},'%') escape '/'" + + " OR coalesce(gb_name, name) LIKE concat('%',#{query},'%') escape '/' )") + ; + } + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'OFF'"); + } + if (params.get("hasRecordPlan") != null && (Boolean)params.get("hasRecordPlan")) { + sqlBuild.append(" AND record_plan_id > 0"); + } + if (params.get("dataType") != null) { + sqlBuild.append(" AND data_type = #{dataType}"); + } + if (params.get("civilCode") != null) { + sqlBuild.append(" AND coalesce(gb_civil_code, civil_code) = #{civilCode}"); + } + if (params.get("parentDeviceId") != null) { + sqlBuild.append(" AND coalesce(gb_parent_id, parent_id) = #{parentDeviceId}"); + } + return sqlBuild.toString(); + } + + public String queryInListByStatus(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and gb_status=#{status} and id in ( "); + + List commonGBChannelList = (List)params.get("commonGBChannelList"); + boolean first = true; + for (CommonGBChannel channel : commonGBChannelList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(channel.getGbId()); + first = false; + } + sqlBuild.append(" )"); + return sqlBuild.toString() ; + } + + public String queryByIds(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and id in ( "); + + Collection ids = (Collection)params.get("ids"); + boolean first = true; + for (Integer id : ids) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(id); + first = false; + } + sqlBuild.append(" )"); + return sqlBuild.toString() ; + } + + public String queryByDataTypeAndDeviceIds(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and data_type = #{dataType} and data_device_id in ( "); + + Collection ids = (Collection)params.get("deviceIds"); + boolean first = true; + for (Integer id : ids) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(id); + first = false; + } + sqlBuild.append(" )"); + return sqlBuild.toString() ; + } + + public String queryByGbDeviceIds(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and coalesce(gb_device_id, device_id) in ( "); + + Collection ids = (Collection)params.get("deviceIds"); + boolean first = true; + for (String id : ids) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append("'"); + sqlBuild.append(id); + sqlBuild.append("'"); + first = false; + } + sqlBuild.append(" )"); + return sqlBuild.toString() ; + } + + public String queryByDeviceIds(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and id in ( "); + + Collection ids = (Collection)params.get("deviceIds"); + boolean first = true; + for (Integer id : ids) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(id); + first = false; + } + sqlBuild.append(" )"); + return sqlBuild.toString() ; + } + + public String queryByIdsOrCivilCode(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and "); + if (params.get("civilCode") != null) { + sqlBuild.append(" coalesce(gb_civil_code, civil_code) = #{civilCode} "); + if (params.get("ids") != null) { + sqlBuild.append(" OR "); + } + } + if (params.get("ids") != null) { + sqlBuild.append(" id in ( "); + Collection ids = (Collection)params.get("ids"); + boolean first = true; + for (Integer id : ids) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(id); + first = false; + } + sqlBuild.append(" )"); + } + return sqlBuild.toString() ; + } + + public String queryByCivilCode(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and coalesce(gb_civil_code, civil_code) = #{civilCode} "); + return sqlBuild.toString(); + } + + public String queryByBusinessGroup(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and coalesce(gb_business_group_id, business_group_id) = #{businessGroup} "); + return sqlBuild.toString() ; + } + + public String queryByParentId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and gb_parent_id = #{parentId} "); + return sqlBuild.toString() ; + } + + public String queryByGroupList(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + + sqlBuild.append(" where channel_type = 0 and gb_parent_id in ( "); + Collection ids = (Collection)params.get("groupList"); + boolean first = true; + for (Group group : ids) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(group.getDeviceId()); + first = false; + } + sqlBuild.append(" )"); + + return sqlBuild.toString() ; + } + + public String queryListByStreamPushList(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + + sqlBuild.append(" where channel_type = 0 and data_type = #{dataType} and data_device_id in ( "); + Collection ids = (Collection)params.get("streamPushList"); + boolean first = true; + for (StreamPush streamPush : ids) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(streamPush.getId()); + first = false; + } + sqlBuild.append(" )"); + + return sqlBuild.toString() ; + } + + public String queryWithPlatform(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_PLATFORM); + sqlBuild.append(" where wpgc.platform_id = #{platformId}"); + return sqlBuild.toString() ; + } + + public String queryShareChannelByParentId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_PLATFORM); + sqlBuild.append(" where wpgc.platform_id = #{platformId} and coalesce(wpgc.custom_parent_id, wdc.gb_parent_id, wdc.parent_id) = #{parentId}"); + return sqlBuild.toString() ; + } + + public String queryShareChannelByCivilCode(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_PLATFORM); + sqlBuild.append(" where wpgc.platform_id = #{platformId} and coalesce(wpgc.custom_civil_code, wdc.gb_civil_code, wdc.civil_code) = #{civilCode}"); + return sqlBuild.toString() ; + } + + public String queryListByCivilCodeForUnusual(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_TABLE_NAME); + sqlBuild.append(" left join (select wcr.device_id from wvp_common_region wcr) temp on temp.device_id = coalesce(wdc.gb_civil_code, wdc.civil_code)" + + " where coalesce(wdc.gb_civil_code, wdc.civil_code) is not null and temp.device_id is null "); + sqlBuild.append(" AND wdc.channel_type = 0 "); + if (params.get("query") != null) { + sqlBuild.append(" AND (coalesce(wdc.gb_device_id, wdc.device_id) LIKE concat('%',#{query},'%') escape '/'" + + " OR coalesce(wdc.gb_name, wdc.name) LIKE concat('%',#{query},'%') escape '/' )") + ; + } + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(wdc.gb_status, wdc.status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(wdc.gb_status, wdc.status) = 'OFF'"); + } + if (params.get("dataType") != null) { + sqlBuild.append(" AND wdc.data_type = #{dataType}"); + } + return sqlBuild.toString(); + } + + public String queryListByParentForUnusual(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_TABLE_NAME); + sqlBuild.append(" left join (select wcg.device_id from wvp_common_group wcg) temp on temp.device_id = coalesce(wdc.gb_parent_id, wdc.parent_id)" + + " where coalesce(wdc.gb_parent_id, wdc.parent_id) is not null and temp.device_id is null "); + sqlBuild.append(" AND wdc.channel_type = 0 "); + if (params.get("query") != null) { + sqlBuild.append(" AND (coalesce(wdc.gb_device_id, wdc.device_id) LIKE concat('%',#{query},'%') escape '/'" + + " OR coalesce(wdc.gb_name, wdc.name) LIKE concat('%',#{query},'%') escape '/' )") + ; + } + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(wdc.gb_status, wdc.status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(wdc.gb_status, wdc.status) = 'OFF'"); + } + if (params.get("dataType") != null) { + sqlBuild.append(" AND wdc.data_type = #{dataType}"); + } + return sqlBuild.toString(); + } + + public String queryCommonChannelByDeviceChannel(Map params ){ + return BASE_SQL + + " where data_type=#{dataType} and data_device_id=#{dataDeviceId} AND device_id=#{deviceId}"; + } + + public String queryCameraChannelInBox(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_TABLE_NAME); + sqlBuild.append(" where wdc.channel_type = 0 AND coalesce(wdc.gb_longitude, wdc.longitude) > #{minLon} " + + "AND coalesce(wdc.gb_longitude, wdc.longitude) <= #{maxLon} " + + "AND coalesce(wdc.gb_latitude, wdc.latitude) > #{minLat} " + + "AND coalesce(wdc.gb_latitude, wdc.latitude) <= #{maxLat}"); + return sqlBuild.toString(); + } + + public String queryOldChanelListByChannels(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" where id in ( "); + + List channelList = (List)params.get("channelList"); + boolean first = true; + for (CommonGBChannel channel : channelList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(channel.getGbId()); + first = false; + } + sqlBuild.append(" )"); + return sqlBuild.toString() ; + } + + public String queryAllForUnusualCivilCode(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append("select wdc.id from wvp_device_channel wdc "); + sqlBuild.append(" left join (select wcr.device_id from wvp_common_region wcr) temp on temp.device_id = coalesce(wdc.gb_civil_code, wdc.civil_code)" + + " where coalesce(wdc.gb_civil_code, wdc.civil_code) is not null and temp.device_id is null "); + sqlBuild.append(" AND wdc.channel_type = 0 "); + return sqlBuild.toString(); + } + + public String queryAllForUnusualParent(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append("select wdc.id from wvp_device_channel wdc "); + sqlBuild.append(" left join (select wcg.device_id from wvp_common_group wcg) temp on temp.device_id = coalesce(wdc.gb_parent_id, wdc.parent_id)" + + " where coalesce(wdc.gb_parent_id, wdc.parent_id) is not null and temp.device_id is null "); + sqlBuild.append(" AND wdc.channel_type = 0 "); + return sqlBuild.toString(); + } + + public String queryOnlineListsByGbDeviceId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_TABLE_NAME); + sqlBuild.append(" where wdc.channel_type = 0 AND coalesce(wdc.gb_status, wdc.status) = 'ON' AND wdc.data_type = 1 AND data_device_id = #{deviceId}"); + return sqlBuild.toString(); + } + + public String queryListForSy(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where wdc.channel_type = 0 AND wdc.data_type != 2 AND (wdc.gb_ptz_type is null or ( wdc.gb_ptz_type != 98 AND wdc.gb_ptz_type != 99)) AND coalesce(wdc.gb_parent_id, wdc.parent_id) = #{groupDeviceId}"); + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(wdc.gb_status, wdc.status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(wdc.gb_status, wdc.status) = 'OFF'"); + } + sqlBuild.append(" order by coalesce(wdc.gb_status, wdc.status) desc"); + + return sqlBuild.toString(); + } + + public String queryListWithChildForSy(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where wdc.channel_type = 0 AND wdc.data_type != 2 AND (wdc.gb_ptz_type is null or ( wdc.gb_ptz_type != 98 AND wdc.gb_ptz_type != 99)) "); + + + List groupList = (List)params.get("groupList"); + if (groupList != null && !groupList.isEmpty()) { + sqlBuild.append(" AND coalesce(wdc.gb_parent_id, wdc.parent_id) in ("); + boolean first = true; + for (CameraGroup group : groupList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append("'" + group.getDeviceId() + "'"); + first = false; + } + sqlBuild.append(" )"); + } + if (params.get("query") != null) { + sqlBuild.append(" AND (coalesce(wdc.gb_device_id, wdc.device_id) LIKE concat('%',#{query},'%') escape '/'" + + " OR coalesce(wdc.gb_name, wdc.name) LIKE concat('%',#{query},'%') escape '/' )") + ; + } + + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(wdc.gb_status, status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(wdc.gb_status, status) = 'OFF'"); + } + + if (params.get("sortName") != null) { + StringBuilder sqlBuildForSort = new StringBuilder(); + sqlBuildForSort.append("select * from ( "); + sqlBuildForSort.append(sqlBuild); + sqlBuildForSort.append(" ) as temp"); + String sortName = (String)params.get("sortName"); + switch (sortName) { + case "gbId": + sqlBuildForSort.append(" order by gb_id "); + break; + case "gbDeviceId": + sqlBuildForSort.append(" order by gb_device_id "); + break; + case "gbName": + sqlBuildForSort.append(" order by gb_name "); + break; + case "gbStatus": + sqlBuildForSort.append(" order by gb_status "); + break; + case "createTime": + sqlBuildForSort.append(" order by create_time "); + break; + case "updateTime": + sqlBuildForSort.append(" order by update_time "); + break; + case "deviceCode": + sqlBuildForSort.append(" order by deviceCode "); + break; + } + + if (params.get("order") != null && (Boolean)params.get("order")) { + sqlBuildForSort.append(" ASC"); + } + if (params.get("order") != null && !(Boolean)params.get("order")) { + sqlBuildForSort.append(" DESC"); + } + return sqlBuildForSort.toString(); + }else { + return sqlBuild.toString(); + } + } + + public String queryListInBox(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where wdc.channel_type = 0 AND wdc.data_type != 2 AND (wdc.gb_ptz_type is null or ( wdc.gb_ptz_type != 98 AND wdc.gb_ptz_type != 99)) " + + " AND coalesce(wdc.gb_parent_id, wdc.parent_id) in ("); + + sqlBuild.append(" "); + List groupList = (List)params.get("groupList"); + boolean first = true; + for (CameraGroup group : groupList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append("'" + group.getDeviceId() + "'"); + first = false; + } + sqlBuild.append(" )"); + + sqlBuild.append(" AND coalesce(wdc.gb_longitude, wdc.longitude) >= #{minLongitude} AND coalesce(wdc.gb_longitude, wdc.longitude) <= #{maxLongitude}"); + sqlBuild.append(" AND coalesce(wdc.gb_latitude, wdc.latitude) >= #{minLatitude} AND coalesce(wdc.gb_latitude, wdc.latitude) <= #{maxLatitude}"); + + if (params.get("level") != null) { + sqlBuild.append(" AND ( map_level <= #{level} or map_level is null )"); + } + + return sqlBuild.toString(); + } + + public String queryListInCircleForMysql(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where wdc.channel_type = 0 AND wdc.data_type != 2 AND (wdc.gb_ptz_type is null or ( wdc.gb_ptz_type != 98 AND wdc.gb_ptz_type != 99)) " + + " AND coalesce(wdc.gb_parent_id, wdc.parent_id) in ("); + + sqlBuild.append(" "); + List groupList = (List)params.get("groupList"); + boolean first = true; + for (CameraGroup group : groupList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append("'" + group.getDeviceId() + "'"); + first = false; + } + sqlBuild.append(" )"); + + String geomTextBuilder = "point(" + params.get("centerLongitude") + " " + params.get("centerLatitude") + ")"; + + sqlBuild.append("AND ST_Distance_Sphere(point(coalesce(wdc.gb_longitude, wdc.longitude), coalesce(wdc.gb_latitude, wdc.latitude)), ST_GeomFromText('").append(geomTextBuilder).append("')) < #{radius}"); + + if (params.get("level") != null) { + sqlBuild.append(" AND ( map_level <= #{level} or map_level is null )"); + } + + return sqlBuild.toString(); + } + + public String queryListInCircleForKingBase(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where wdc.channel_type = 0 AND wdc.data_type != 2 AND (wdc.gb_ptz_type is null or ( wdc.gb_ptz_type != 98 AND wdc.gb_ptz_type != 99)) " + + " AND coalesce(wdc.gb_parent_id, wdc.parent_id) in ("); + + sqlBuild.append(" "); + List groupList = (List)params.get("groupList"); + boolean first = true; + for (CameraGroup group : groupList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append("'" + group.getDeviceId() + "'"); + first = false; + } + sqlBuild.append(" )"); + + String geomTextBuilder = "point(" + params.get("centerLongitude") + " " + params.get("centerLatitude") + ")"; + + sqlBuild.append("AND ST_DistanceSphere(ST_MakePoint(coalesce(wdc.gb_longitude, wdc.longitude), coalesce(wdc.gb_latitude, wdc.latitude)), ST_GeomFromText('").append(geomTextBuilder).append("')) < #{radius}"); + + if (params.get("level") != null) { + sqlBuild.append(" AND ( map_level <= #{level} or map_level is null )"); + } + + return sqlBuild.toString(); + } + + public String queryListInPolygonForMysql(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where wdc.channel_type = 0 AND wdc.data_type != 2 AND (wdc.gb_ptz_type is null or ( wdc.gb_ptz_type != 98 AND wdc.gb_ptz_type != 99)) " + + " AND coalesce(wdc.gb_parent_id, wdc.parent_id) in ("); + + sqlBuild.append(" "); + List groupList = (List)params.get("groupList"); + boolean first = true; + for (CameraGroup group : groupList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append("'" + group.getDeviceId() + "'"); + first = false; + } + sqlBuild.append(" )"); + + StringBuilder geomTextBuilder = new StringBuilder(); + geomTextBuilder.append("POLYGON(("); + List pointList = (List)params.get("pointList"); + for (int i = 0; i < pointList.size(); i++) { + if (i > 0) { + geomTextBuilder.append(", "); + } + Point point = pointList.get(i); + geomTextBuilder.append(point.getLng()).append(" ").append(point.getLat()); + } + geomTextBuilder.append("))"); + sqlBuild.append("AND ST_Within(point(coalesce(wdc.gb_longitude, wdc.longitude), coalesce(wdc.gb_latitude, wdc.latitude)), ST_GeomFromText('").append(geomTextBuilder).append("'))"); + + if (params.get("level") != null) { + sqlBuild.append(" AND ( map_level <= #{level} or map_level is null )"); + } + + return sqlBuild.toString(); + } + + public String queryListInPolygonForKingBase(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where wdc.channel_type = 0 AND wdc.data_type != 2 AND (wdc.gb_ptz_type is null or ( wdc.gb_ptz_type != 98 AND wdc.gb_ptz_type != 99)) " + + " AND coalesce(wdc.gb_parent_id, wdc.parent_id) in ("); + + sqlBuild.append(" "); + List groupList = (List)params.get("groupList"); + boolean first = true; + for (CameraGroup group : groupList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append("'" + group.getDeviceId() + "'"); + first = false; + } + sqlBuild.append(" )"); + + StringBuilder geomTextBuilder = new StringBuilder(); + geomTextBuilder.append("POLYGON(("); + List pointList = (List)params.get("pointList"); + for (int i = 0; i < pointList.size(); i++) { + if (i > 0) { + geomTextBuilder.append(", "); + } + Point point = pointList.get(i); + geomTextBuilder.append(point.getLng()).append(" ").append(point.getLat()); + } + geomTextBuilder.append("))"); + sqlBuild.append("AND ST_Within(ST_MakePoint(coalesce(wdc.gb_longitude, wdc.longitude), coalesce(wdc.gb_latitude, wdc.latitude)), ST_GeomFromText('").append(geomTextBuilder).append("'))"); + + if (params.get("level") != null) { + sqlBuild.append(" AND ( map_level <= #{level} or map_level is null )"); + } + + return sqlBuild.toString(); + } + + public String queryGbChannelByChannelDeviceIdAndGbDeviceId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where coalesce(wdc.gb_device_id, wdc.device_id) = #{channelDeviceId}"); + if (params.get("gbDeviceId") != null) { + sqlBuild.append(" AND wdc.data_type = 1 and wd.device_id = #{gbDeviceId}"); + } + return sqlBuild.toString(); + } + + public String queryListByAddressAndDirectionType(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where coalesce(wdc.gb_address, wdc.address) = #{address}"); + if (params.get("directionType") != null) { + sqlBuild.append(" and coalesce(wdc.gb_direction_type, wdc.direction_type) = #{directionType}"); + } + return sqlBuild.toString(); + } + + + public String queryListByDeviceIds(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(""); + return sqlBuild.toString() ; + } + + public String queryCameraChannelByIds(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where wdc.id in ( "); + + List channelList = (List)params.get("channelList"); + boolean first = true; + for (CommonGBChannel channel : channelList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(channel.getGbId()); + first = false; + } + sqlBuild.append(" )"); + return sqlBuild.toString() ; + } + + public String queryListForSyMobile(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" WHERE wdc.gb_ptz_type = 99 and wdc.channel_type = 0 AND wdc.data_type != 2 "); + if (params.get("business") != null) { + sqlBuild.append(" AND coalesce(gb_business_group_id, business_group_id) = #{business}"); + } + return sqlBuild.toString(); + } + + public String queryMeetingChannelList(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" WHERE wdc.channel_type = 0 AND wdc.data_type = 3 and wdc.gb_ptz_type = 98 and coalesce(wdc.gb_business_group_id, wdc.business_group_id) = #{business}"); + return sqlBuild.toString(); + } + + + public String queryCameraChannelById(Map params ){ + return BASE_SQL_FOR_CAMERA_DEVICE + " where wdc.id = #{gbId}"; + } + + public String queryAllWithPosition(Map params ){ + return BASE_SQL + " where channel_type = 0 " + + " AND coalesce(gb_longitude, longitude) > 0" + + " AND coalesce(gb_latitude, latitude) > 0 " + + " ORDER BY map_level"; + } + + public String queryListInExtent(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" where channel_type = 0 " + + "AND coalesce(gb_longitude, longitude) > #{minLng} " + + "AND coalesce(gb_longitude, longitude) <= #{maxLng} " + + "AND coalesce(gb_latitude, latitude) > #{minLat} " + + "AND coalesce(gb_latitude, latitude) <= #{maxLat}"); + return sqlBuild.toString(); + } + + public String queryListOutExtent(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" where channel_type = 0 AND ( " + + "coalesce(gb_longitude, longitude) <= #{minLng} " + + "or coalesce(gb_longitude, longitude) > #{maxLng} " + + "or coalesce(gb_latitude, latitude) <= #{minLat} " + + "or coalesce(gb_latitude, latitude) > #{maxLat}" + + ")"); + return sqlBuild.toString(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/DeviceChannelProvider.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/DeviceChannelProvider.java new file mode 100644 index 0000000..af5b8e0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/DeviceChannelProvider.java @@ -0,0 +1,190 @@ +package com.genersoft.iot.vmp.gb28181.dao.provider; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import org.springframework.util.ObjectUtils; + +import java.util.List; +import java.util.Map; + +public class DeviceChannelProvider { + + public String getBaseSelectSql(){ + return "SELECT " + + " dc.id,\n" + + " dc.data_device_id,\n" + + " dc.create_time,\n" + + " dc.update_time,\n" + + " dc.sub_count,\n" + + " dc.stream_id,\n" + + " dc.has_audio,\n" + + " dc.gps_time,\n" + + " dc.stream_identification,\n" + + " dc.channel_type,\n" + + " d.device_id as parent_device_id,\n" + + " coalesce(d.custom_name, d.name) as parent_name,\n" + + " coalesce(dc.gb_device_id, dc.device_id) as device_id,\n" + + " coalesce(dc.gb_name, dc.name) as name,\n" + + " coalesce(dc.gb_manufacturer, dc.manufacturer) as manufacturer,\n" + + " coalesce(dc.gb_model, dc.model) as model,\n" + + " coalesce(dc.gb_owner, dc.owner) as owner,\n" + + " coalesce(dc.gb_civil_code, dc.civil_code) as civil_code,\n" + + " coalesce(dc.gb_block, dc.block) as block,\n" + + " coalesce(dc.gb_address, dc.address) as address,\n" + + " coalesce(dc.gb_parental, dc.parental) as parental,\n" + + " coalesce(dc.gb_parent_id, dc.parent_id) as parent_id,\n" + + " coalesce(dc.gb_safety_way, dc.safety_way) as safety_way,\n" + + " coalesce(dc.gb_register_way, dc.register_way) as register_way,\n" + + " coalesce(dc.gb_cert_num, dc.cert_num) as cert_num,\n" + + " coalesce(dc.gb_certifiable, dc.certifiable) as certifiable,\n" + + " coalesce(dc.gb_err_code, dc.err_code) as err_code,\n" + + " coalesce(dc.gb_end_time, dc.end_time) as end_time,\n" + + " coalesce(dc.gb_secrecy, dc.secrecy) as secrecy,\n" + + " coalesce(dc.gb_ip_address, dc.ip_address) as ip_address,\n" + + " coalesce(dc.gb_port, dc.port) as port,\n" + + " coalesce(dc.gb_password, dc.password) as password,\n" + + " coalesce(dc.gb_status, dc.status) as status,\n" + + " coalesce(dc.gb_longitude, dc.longitude) as longitude,\n" + + " coalesce(dc.gb_latitude, dc.latitude) as latitude,\n" + + " coalesce(dc.gb_ptz_type, dc.ptz_type) as ptz_type,\n" + + " coalesce(dc.gb_position_type, dc.position_type) as position_type,\n" + + " coalesce(dc.gb_room_type, dc.room_type) as room_type,\n" + + " coalesce(dc.gb_use_type, dc.use_type) as use_type,\n" + + " coalesce(dc.gb_supply_light_type, dc.supply_light_type) as supply_light_type,\n" + + " coalesce(dc.gb_direction_type, dc.direction_type) as direction_type,\n" + + " coalesce(dc.gb_resolution, dc.resolution) as resolution,\n" + + " coalesce(dc.gb_business_group_id, dc.business_group_id) as business_group_id,\n" + + " coalesce(dc.gb_download_speed, dc.download_speed) as download_speed,\n" + + " coalesce(dc.gb_svc_space_support_mod, dc.svc_space_support_mod) as svc_space_support_mod,\n" + + " coalesce(dc.gb_svc_time_support_mode,dc.svc_time_support_mode) as svc_time_support_mode\n" + + " from " + + " wvp_device_channel dc " + + " left join wvp_device d on d.id = dc.data_device_id " + ; + } + public String queryChannels(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(getBaseSelectSql()); + sqlBuild.append(" where data_type = " + ChannelDataType.GB28181); + if (params.get("dataDeviceId") != null) { + sqlBuild.append(" AND dc.data_device_id = #{dataDeviceId} "); + } + if (params.get("businessGroupId") != null ) { + sqlBuild.append(" AND coalesce(dc.gb_business_group_id, dc.business_group_id)=#{businessGroupId} AND coalesce(dc.gb_parent_id, dc.parent_id) is null"); + }else if (params.get("parentChannelId") != null ) { + sqlBuild.append(" AND coalesce(dc.gb_parent_id, dc.parent_id)=#{parentChannelId}"); + } + if (params.get("civilCode") != null ) { + sqlBuild.append(" AND (coalesce(dc.gb_civil_code, dc.civil_code) = #{civilCode} " + + "OR (LENGTH(coalesce(dc.gb_device_id, dc.device_id))=LENGTH(#{civilCode}) + 2) AND coalesce(dc.gb_device_id, dc.device_id) LIKE concat(#{civilCode},'%'))"); + } + if (params.get("query") != null && !ObjectUtils.isEmpty(params.get("query"))) { + sqlBuild.append(" AND (coalesce(dc.gb_device_id, dc.device_id) LIKE concat('%',#{query},'%') escape '/'" + + " OR coalesce(dc.gb_name, dc.name) LIKE concat('%',#{query},'%') escape '/'"); + if (params.get("queryParent") != null && (Boolean) params.get("queryParent")) { + sqlBuild.append(" OR d.device_id LIKE concat('%',#{query},'%') escape '/'"); + sqlBuild.append(" OR coalesce(d.custom_name, d.name) LIKE concat('%',#{query},'%') escape '/'"); + } + sqlBuild.append(")"); + } + if (params.get("hasStream") != null && (Boolean) params.get("hasStream")) { + sqlBuild.append(" AND dc.stream_id IS NOT NULL"); + } + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'OFF'"); + } + if (params.get("hasSubChannel") != null && (Boolean)params.get("hasSubChannel")) { + sqlBuild.append(" AND dc.sub_count > 0"); + } + if (params.get("hasSubChannel") != null && !(Boolean)params.get("hasSubChannel")) { + sqlBuild.append(" AND dc.sub_count = 0"); + } + List channelIds = (List)params.get("channelIds"); + if (channelIds != null && !channelIds.isEmpty()) { + sqlBuild.append(" AND dc.device_id in ("); + boolean first = true; + for (String id : channelIds) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(id); + first = false; + } + sqlBuild.append(" )"); + } + sqlBuild.append(" ORDER BY d.device_id, dc.device_id"); + return sqlBuild.toString(); + } + + + public String queryChannelsByDeviceDbId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(getBaseSelectSql()); + sqlBuild.append(" where data_type = " + ChannelDataType.GB28181 + " and dc.data_device_id = #{dataDeviceId}"); + return sqlBuild.toString(); + } + + public String queryAllChannels(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(getBaseSelectSql()); + sqlBuild.append(" where data_type = " + ChannelDataType.GB28181 + " and dc.data_device_id = #{dataDeviceId}"); + return sqlBuild.toString(); + } + + public String getOne(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(getBaseSelectSql()); + sqlBuild.append(" where dc.id=#{id}"); + return sqlBuild.toString(); + } + + public String getOneByDeviceId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(getBaseSelectSql()); + sqlBuild.append(" where data_type = " + ChannelDataType.GB28181 + " and dc.data_device_id=#{dataDeviceId} and coalesce(dc.gb_device_id, dc.device_id) = #{channelId}"); + return sqlBuild.toString(); + } + + + + public String queryByDeviceId(Map params ){ + return getBaseSelectSql() + " where data_type = " + ChannelDataType.GB28181 + " and channel_type = 0 and coalesce(gb_device_id, device_id) = #{gbDeviceId}"; + } + + public String queryById(Map params ){ + return getBaseSelectSql() + " where data_type = " + ChannelDataType.GB28181 + " and channel_type = 0 and id = #{gbId}"; + } + + + public String queryList(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(getBaseSelectSql()); + sqlBuild.append(" where channel_type = 0 and data_type = " + ChannelDataType.GB28181); + if (params.get("query") != null) { + sqlBuild.append(" AND (coalesce(gb_device_id, device_id) LIKE concat('%',#{query},'%')" + + " OR coalesce(gb_name, name) LIKE concat('%',#{query},'%') )") + ; + } + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'OFF'"); + } + if (params.get("hasCivilCode") != null && (Boolean)params.get("hasCivilCode")) { + sqlBuild.append(" AND coalesce(gb_civil_code, civil_code) is not null"); + } + if (params.get("hasCivilCode") != null && !(Boolean)params.get("hasCivilCode")) { + sqlBuild.append(" AND coalesce(gb_civil_code, civil_code) is null"); + } + if (params.get("hasGroup") != null && (Boolean)params.get("hasGroup")) { + sqlBuild.append(" AND coalesce(gb_parent_id, parent_id) is not null"); + } + if (params.get("hasGroup") != null && !(Boolean)params.get("hasGroup")) { + sqlBuild.append(" AND coalesce(gb_parent_id, parent_id) is null"); + } + return sqlBuild.toString(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java new file mode 100755 index 0000000..df21658 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java @@ -0,0 +1,120 @@ +package com.genersoft.iot.vmp.gb28181.event; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm; +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.event.alarm.AlarmEvent; +import com.genersoft.iot.vmp.gb28181.event.channel.ChannelEvent; +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import com.genersoft.iot.vmp.gb28181.event.subscribe.mobilePosition.MobilePositionEvent; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerOfflineEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerOnlineEvent; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import java.util.*; + +/** + * @description:Event事件通知推送器,支持推送在线事件、离线事件 + * @author: swwheihei + * @date: 2020年5月6日 上午11:30:50 + */ +@Slf4j +@Component +public class EventPublisher { + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IRedisRpcService redisRpcService; + + /** + * 设备报警事件 + * @param deviceAlarm + */ + public void deviceAlarmEventPublish(DeviceAlarm deviceAlarm) { + AlarmEvent alarmEvent = new AlarmEvent(this); + alarmEvent.setAlarmInfo(deviceAlarm); + applicationEventPublisher.publishEvent(alarmEvent); + } + + public void mediaServerOfflineEventPublish(MediaServer mediaServer){ + MediaServerOfflineEvent outEvent = new MediaServerOfflineEvent(this); + outEvent.setMediaServer(mediaServer); + applicationEventPublisher.publishEvent(outEvent); + } + + public void mediaServerOnlineEventPublish(MediaServer mediaServer) { + MediaServerOnlineEvent outEvent = new MediaServerOnlineEvent(this); + outEvent.setMediaServer(mediaServer); + applicationEventPublisher.publishEvent(outEvent); + } + + public void channelEventPublish(CommonGBChannel commonGBChannel, ChannelEvent.ChannelEventMessageType type) { + channelEventPublish(Collections.singletonList(commonGBChannel), type); + } + + public void channelEventPublishForUpdate(CommonGBChannel commonGBChannel, CommonGBChannel deviceChannelForOld) { + ChannelEvent channelEvent = ChannelEvent.getInstanceForUpdate(this, Collections.singletonList(commonGBChannel), Collections.singletonList(deviceChannelForOld)); + applicationEventPublisher.publishEvent(channelEvent); + } + + public void channelEventPublishForUpdate(List channelList, List channelListForOld) { + ChannelEvent channelEvent = ChannelEvent.getInstanceForUpdate(this, channelList, channelListForOld); + applicationEventPublisher.publishEvent(channelEvent); + } + + public void channelEventPublish(List channelList, ChannelEvent.ChannelEventMessageType type) { + ChannelEvent channelEvent = ChannelEvent.getInstance(this, type, channelList); + applicationEventPublisher.publishEvent(channelEvent); + } + + public void catalogEventPublish(Platform platform, CommonGBChannel deviceChannel, String type) { + catalogEventPublish(platform, Collections.singletonList(deviceChannel), type); + } + public void catalogEventPublish(Platform platform, List deviceChannels, String type) { + if (platform != null && !userSetting.getServerId().equals(platform.getServerId())) { + log.info("[国标级联] 目录状态推送, 此上级平台由其他服务处理,消息已经忽略"); + return; + } + CatalogEvent outEvent = new CatalogEvent(this); + List channels = new ArrayList<>(); + if (deviceChannels.size() > 1) { + // 数据去重 + Set gbIdSet = new HashSet<>(); + for (CommonGBChannel deviceChannel : deviceChannels) { + if (deviceChannel != null && deviceChannel.getGbDeviceId() != null && !gbIdSet.contains(deviceChannel.getGbDeviceId())) { + gbIdSet.add(deviceChannel.getGbDeviceId()); + channels.add(deviceChannel); + } + } + }else { + channels = deviceChannels; + } + outEvent.setChannels(channels); + outEvent.setType(type); + if (platform != null) { + outEvent.setPlatform(platform); + } + applicationEventPublisher.publishEvent(outEvent); + + } + + public void mobilePositionEventPublish(MobilePosition mobilePosition) { + MobilePositionEvent event = new MobilePositionEvent(this); + event.setMobilePosition(mobilePosition); + applicationEventPublisher.publishEvent(event); + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/MessageSubscribe.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/MessageSubscribe.java new file mode 100755 index 0000000..f86c878 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/MessageSubscribe.java @@ -0,0 +1,73 @@ +package com.genersoft.iot.vmp.gb28181.event; + +import com.genersoft.iot.vmp.gb28181.event.sip.MessageEvent; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.DelayQueue; + +/** + * @author lin + */ +@Slf4j +@Component +public class MessageSubscribe { + + private final Map> subscribes = new ConcurrentHashMap<>(); + + private final DelayQueue> delayQueue = new DelayQueue<>(); + + @Scheduled(fixedDelay = 200) //每200毫秒执行 + public void execute(){ + while (!delayQueue.isEmpty()) { + try { + MessageEvent take = delayQueue.take(); + // 出现超时异常 + if(take.getCallback() != null) { + take.getCallback().run(ErrorCode.ERROR486.getCode(), "消息超时未回复", null); + } + subscribes.remove(take.getKey()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + + public void addSubscribe(MessageEvent event) { + MessageEvent messageEvent = subscribes.get(event.getKey()); + if (messageEvent != null) { + subscribes.remove(event.getKey()); + delayQueue.remove(messageEvent); + } + subscribes.put(event.getKey(), event); + delayQueue.offer(event); + } + + public MessageEvent getSubscribe(String key) { + return subscribes.get(key); + } + + public void removeSubscribe(String key) { + if(key == null){ + return; + } + MessageEvent messageEvent = subscribes.get(key); + if (messageEvent != null) { + subscribes.remove(key); + delayQueue.remove(messageEvent); + } + } + + public boolean isEmpty(){ + return subscribes.isEmpty(); + } + + public Integer size() { + return subscribes.size(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/SipSubscribe.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/SipSubscribe.java new file mode 100755 index 0000000..1ef18ca --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/SipSubscribe.java @@ -0,0 +1,181 @@ +package com.genersoft.iot.vmp.gb28181.event; + +import com.genersoft.iot.vmp.gb28181.bean.DeviceNotFoundEvent; +import com.genersoft.iot.vmp.gb28181.event.sip.SipEvent; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.sip.DialogTerminatedEvent; +import javax.sip.ResponseEvent; +import javax.sip.TimeoutEvent; +import javax.sip.TransactionTerminatedEvent; +import javax.sip.header.WarningHeader; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.DelayQueue; + +/** + * @author lin + */ +@Slf4j +@Component +public class SipSubscribe { + + private final Map subscribes = new ConcurrentHashMap<>(); + + private final DelayQueue delayQueue = new DelayQueue<>(); + + + @Scheduled(fixedDelay = 200) //每200毫秒执行 + public void execute(){ + while (!delayQueue.isEmpty()) { + try { + SipEvent take = delayQueue.take(); + // 出现超时异常 + if(take.getErrorEvent() != null) { + EventResult eventResult = new EventResult<>(); + eventResult.type = EventResultType.timeout; + eventResult.msg = "消息超时未回复"; + eventResult.statusCode = -1024; + take.getErrorEvent().response(eventResult); + } + subscribes.remove(take.getKey()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + public interface Event { void response(EventResult eventResult); + } + + /** + * + */ + public enum EventResultType{ + // 超时 + timeout, + // 回复 + response, + // 事务已结束 + transactionTerminated, + // 会话已结束 + dialogTerminated, + // 设备未找到 + deviceNotFoundEvent, + // 消息发送失败 + cmdSendFailEvent, + // 消息发送失败 + failedToGetPort, + // 收到失败的回复 + failedResult + } + + public static class EventResult{ + public int statusCode; + public EventResultType type; + public String msg; + public String callId; + public T event; + + public EventResult() { + } + + public EventResult(T event) { + this.event = event; + if (event instanceof ResponseEvent) { + ResponseEvent responseEvent = (ResponseEvent)event; + SIPResponse response = (SIPResponse)responseEvent.getResponse(); + this.type = EventResultType.response; + if (response != null) { + WarningHeader warningHeader = (WarningHeader)response.getHeader(WarningHeader.NAME); + if (warningHeader != null && !ObjectUtils.isEmpty(warningHeader.getText())) { + this.msg = ""; + if (warningHeader.getCode() > 0) { + this.msg += warningHeader.getCode() + ":"; + } + if (warningHeader.getAgent() != null) { + this.msg += warningHeader.getCode() + ":"; + } + if (warningHeader.getText() != null) { + this.msg += warningHeader.getText(); + } + }else { + this.msg = response.getReasonPhrase(); + } + this.statusCode = response.getStatusCode(); + this.callId = response.getCallIdHeader().getCallId(); + } + }else if (event instanceof TimeoutEvent) { + TimeoutEvent timeoutEvent = (TimeoutEvent)event; + this.type = EventResultType.timeout; + this.msg = "消息超时未回复"; + this.statusCode = -1024; + if (timeoutEvent.isServerTransaction()) { + this.callId = ((SIPRequest)timeoutEvent.getServerTransaction().getRequest()).getCallIdHeader().getCallId(); + }else { + this.callId = ((SIPRequest)timeoutEvent.getClientTransaction().getRequest()).getCallIdHeader().getCallId(); + } + }else if (event instanceof TransactionTerminatedEvent) { + TransactionTerminatedEvent transactionTerminatedEvent = (TransactionTerminatedEvent)event; + this.type = EventResultType.transactionTerminated; + this.msg = "事务已结束"; + this.statusCode = -1024; + if (transactionTerminatedEvent.isServerTransaction()) { + this.callId = ((SIPRequest)transactionTerminatedEvent.getServerTransaction().getRequest()).getCallIdHeader().getCallId(); + }else { + this.callId = ((SIPRequest)transactionTerminatedEvent.getClientTransaction().getRequest()).getCallIdHeader().getCallId(); + } + }else if (event instanceof DialogTerminatedEvent) { + DialogTerminatedEvent dialogTerminatedEvent = (DialogTerminatedEvent)event; + this.type = EventResultType.dialogTerminated; + this.msg = "会话已结束"; + this.statusCode = -1024; + this.callId = dialogTerminatedEvent.getDialog().getCallId().getCallId(); + }else if (event instanceof DeviceNotFoundEvent) { + this.type = EventResultType.deviceNotFoundEvent; + this.msg = "设备未找到"; + this.statusCode = -1024; + this.callId = ((DeviceNotFoundEvent) event).getCallId(); + } + } + } + + + public void addSubscribe(String key, SipEvent event) { + SipEvent sipEvent = subscribes.get(key); + if (sipEvent != null) { + subscribes.remove(key); + delayQueue.remove(sipEvent); + } + subscribes.put(key, event); + delayQueue.offer(event); + } + + public SipEvent getSubscribe(String key) { + return subscribes.get(key); + } + + public void removeSubscribe(String key) { + if(key == null){ + return; + } + SipEvent sipEvent = subscribes.get(key); + if (sipEvent != null) { + subscribes.remove(key); + delayQueue.remove(sipEvent); + } + } + + public boolean isEmpty(){ + return subscribes.isEmpty(); + } + + public Integer size() { + return subscribes.size(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEvent.java new file mode 100755 index 0000000..11ed3a5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEvent.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.gb28181.event.alarm; + +import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; + +/** + * @description: 报警事件 + * @author: lawrencehj + * @data: 2021-01-20 + */ + +public class AlarmEvent extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public AlarmEvent(Object source) { + super(source); + } + + private DeviceAlarm deviceAlarm; + + public DeviceAlarm getAlarmInfo() { + return deviceAlarm; + } + + public void setAlarmInfo(DeviceAlarm deviceAlarm) { + this.deviceAlarm = deviceAlarm; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEventListener.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEventListener.java new file mode 100755 index 0000000..46c4cc1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEventListener.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.gb28181.event.alarm; + +import com.genersoft.iot.vmp.gb28181.session.SseSessionManager; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; + +/** + * 报警事件监听器. + * + * @author lawrencehj + * @author xiaoQQya + * @since 2021/01/20 + */ +@Slf4j +@Component +public class AlarmEventListener implements ApplicationListener { + + @Resource + private SseSessionManager sseSessionManager; + + @Override + public void onApplicationEvent(@NotNull AlarmEvent event) { + if (log.isDebugEnabled()) { + log.debug("设备报警事件触发, deviceId: {}, {}", event.getAlarmInfo().getDeviceId(), event.getAlarmInfo().getAlarmDescription()); + } + sseSessionManager.sendForAll("message", event.getAlarmInfo()); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/channel/ChannelEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/channel/ChannelEvent.java new file mode 100755 index 0000000..534c6ce --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/channel/ChannelEvent.java @@ -0,0 +1,53 @@ +package com.genersoft.iot.vmp.gb28181.event.channel; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; +import java.util.List; + +/** + * 通道事件 + */ + +@Setter +@Getter +public class ChannelEvent extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public ChannelEvent(Object source) { + super(source); + } + + private List channels; + + private List oldChannels; + + private ChannelEventMessageType messageType; + + + + public enum ChannelEventMessageType { + ADD, UPDATE, DEL, ON, OFF, VLOST, DEFECT + } + + public static ChannelEvent getInstance(Object source, ChannelEventMessageType messageType, List channelList) { + ChannelEvent channelEvent = new ChannelEvent(source); + channelEvent.setMessageType(messageType); + channelEvent.setChannels(channelList); + return channelEvent; + } + + public static ChannelEvent getInstanceForUpdate(Object source, List channelList, List channelListForOld) { + ChannelEvent channelEvent = new ChannelEvent(source); + channelEvent.setMessageType(ChannelEventMessageType.UPDATE); + channelEvent.setChannels(channelList); + channelEvent.setOldChannels(channelListForOld); + return channelEvent; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEndEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEndEvent.java new file mode 100755 index 0000000..ef2283f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEndEvent.java @@ -0,0 +1,28 @@ +package com.genersoft.iot.vmp.gb28181.event.record; + +import com.genersoft.iot.vmp.gb28181.bean.RecordInfo; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; + +/** + * @description: 录像查询结束时间 + * @author: pan + * @data: 2022-02-23 + */ +@Setter +@Getter +public class RecordInfoEndEvent extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public RecordInfoEndEvent(Object source) { + super(source); + } + + private RecordInfo recordInfo; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEvent.java new file mode 100755 index 0000000..e6a9da9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEvent.java @@ -0,0 +1,28 @@ +package com.genersoft.iot.vmp.gb28181.event.record; + +import com.genersoft.iot.vmp.gb28181.bean.RecordInfo; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; + +/** + * @description: 录像查询结束时间 + * @author: pan + * @data: 2022-02-23 + */ + +@Setter +@Getter +public class RecordInfoEvent extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public RecordInfoEvent(Object source) { + super(source); + } + + private RecordInfo recordInfo; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEventListener.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEventListener.java new file mode 100755 index 0000000..f2a0986 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEventListener.java @@ -0,0 +1,61 @@ +package com.genersoft.iot.vmp.gb28181.event.record; + +import com.genersoft.iot.vmp.gb28181.bean.RecordInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @description: 录像查询结束事件 + * @author: pan + * @data: 2022-02-23 + */ +@Slf4j +@Component +public class RecordInfoEventListener implements ApplicationListener { + + private final Map handlerMap = new ConcurrentHashMap<>(); + public interface RecordEndEventHandler{ + void handler(RecordInfo recordInfo); + } + + @Override + public void onApplicationEvent(RecordInfoEvent event) { + String deviceId = event.getRecordInfo().getDeviceId(); + String channelId = event.getRecordInfo().getChannelId(); + int count = event.getRecordInfo().getCount(); + int sumNum = event.getRecordInfo().getSumNum(); + log.info("录像查询事件触发,deviceId:{}, channelId: {}, 录像数量{}/{}条", event.getRecordInfo().getDeviceId(), + event.getRecordInfo().getChannelId(), count,sumNum); + if (!handlerMap.isEmpty()) { + RecordEndEventHandler handler = handlerMap.get(deviceId + channelId); + log.info("录像查询事件触发, 发送订阅,deviceId:{}, channelId: {}", + event.getRecordInfo().getDeviceId(), event.getRecordInfo().getChannelId()); + if (handler !=null){ + handler.handler(event.getRecordInfo()); + if (count ==sumNum){ + handlerMap.remove(deviceId + channelId); + } + } + } + } + + /** + * 添加 + */ + public void addEndEventHandler(String device, String channelId, RecordEndEventHandler recordEndEventHandler) { + log.info("录像查询事件添加监听,deviceId:{}, channelId: {}", device, channelId); + handlerMap.put(device + channelId, recordEndEventHandler); + } + /** + * 添加 + */ + public void delEndEventHandler(String device, String channelId) { + log.info("录像查询事件移除监听,deviceId:{}, channelId: {}", device, channelId); + handlerMap.remove(device + channelId); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/sip/MessageEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/sip/MessageEvent.java new file mode 100644 index 0000000..e55fce3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/sip/MessageEvent.java @@ -0,0 +1,56 @@ +package com.genersoft.iot.vmp.gb28181.event.sip; + +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +@Data +public class MessageEvent implements Delayed { + /** + * 超时时间(单位: 毫秒) + */ + private long delay; + + private String cmdType; + + private String sn; + + private String deviceId; + + private String result; + + private T t; + + private ErrorCallback callback; + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(delay - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } + + public String getKey(){ + return cmdType + sn; + } + + public static MessageEvent getInstance(String cmdType, String sn, String deviceId, Long delay, ErrorCallback callback){ + MessageEvent messageEvent = new MessageEvent<>(); + messageEvent.cmdType = cmdType; + messageEvent.sn = sn; + messageEvent.deviceId = deviceId; + messageEvent.callback = callback; + if (delay == null) { + messageEvent.delay = System.currentTimeMillis() + 1000; + }else { + messageEvent.delay = System.currentTimeMillis() + delay; + } + return messageEvent; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/sip/SipEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/sip/SipEvent.java new file mode 100644 index 0000000..0bca7cd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/sip/SipEvent.java @@ -0,0 +1,51 @@ +package com.genersoft.iot.vmp.gb28181.event.sip; + +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +@Data +public class SipEvent implements Delayed { + + private String key; + + /** + * 成功的回调 + */ + private SipSubscribe.Event okEvent; + + /** + * 错误的回调,包括超时 + */ + private SipSubscribe.Event errorEvent; + + /** + * 超时时间(单位: 毫秒) + */ + private long delay; + + private SipTransactionInfo sipTransactionInfo; + + public static SipEvent getInstance(String key, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent, long delay) { + SipEvent sipEvent = new SipEvent(); + sipEvent.setKey(key); + sipEvent.setOkEvent(okEvent); + sipEvent.setErrorEvent(errorEvent); + sipEvent.setDelay(System.currentTimeMillis() + delay); + return sipEvent; + } + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(delay - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEvent.java new file mode 100755 index 0000000..d0d2122 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEvent.java @@ -0,0 +1,60 @@ +package com.genersoft.iot.vmp.gb28181.event.subscribe.catalog; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.util.List; + +@Setter +@Getter +public class CatalogEvent extends ApplicationEvent { + + public CatalogEvent(Object source) { + super(source); + } + + /** + * 上线 + */ + public static final String ON = "ON"; + + /** + * 离线 + */ + public static final String OFF = "OFF"; + + /** + * 视频丢失 + */ + public static final String VLOST = "VLOST"; + + /** + * 故障 + */ + public static final String DEFECT = "DEFECT"; + + /** + * 增加 + */ + public static final String ADD = "ADD"; + + /** + * 删除 + */ + public static final String DEL = "DEL"; + + /** + * 更新 + */ + public static final String UPDATE = "UPDATE"; + + private List channels; + + private String type; + + private Platform platform; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEventLister.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEventLister.java new file mode 100755 index 0000000..2aef0ce --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEventLister.java @@ -0,0 +1,178 @@ +package com.genersoft.iot.vmp.gb28181.event.subscribe.catalog; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.SubscribeHolder; +import com.genersoft.iot.vmp.gb28181.bean.SubscribeInfo; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * catalog事件 + */ +@Slf4j +//@Component +public class CatalogEventLister implements ApplicationListener { + + @Autowired + private IPlatformService platformService; + + @Autowired + private IPlatformChannelService platformChannelService; + + @Autowired + private ISIPCommanderForPlatform sipCommanderFroPlatform; + + @Autowired + private SubscribeHolder subscribeHolder; + + @Autowired + private UserSetting userSetting; + + @Override + public void onApplicationEvent(CatalogEvent event) { + SubscribeInfo subscribe = null; + Platform parentPlatform = null; + log.info("[Catalog事件: {}]通道数量: {}", event.getType(), event.getChannels().size()); + Map> platformMap = new HashMap<>(); + Map channelMap = new HashMap<>(); + if (event.getPlatform() != null) { + parentPlatform = event.getPlatform(); + if (parentPlatform.getServerGBId() == null) { + log.info("[Catalog事件: {}] 平台服务国标编码未找到", event.getType()); + return; + } + subscribe = subscribeHolder.getCatalogSubscribe(parentPlatform.getServerGBId()); + if (subscribe == null) { + log.info("[Catalog事件: {}] 未订阅目录事件", event.getType()); + return; + } + + }else { + List allPlatform = platformService.queryAll(userSetting.getServerId()); + // 获取所用订阅 + List platforms = subscribeHolder.getAllCatalogSubscribePlatform(allPlatform); + if (event.getChannels() != null) { + if (!platforms.isEmpty()) { + for (CommonGBChannel deviceChannel : event.getChannels()) { + List parentPlatformsForGB = platformChannelService.queryPlatFormListByChannelDeviceId( + deviceChannel.getGbId(), platforms); + platformMap.put(deviceChannel.getGbDeviceId(), parentPlatformsForGB); + channelMap.put(deviceChannel.getGbDeviceId(), deviceChannel); + } + }else { + log.info("[Catalog事件: {}] 未订阅目录事件", event.getType()); + } + }else { + log.info("[Catalog事件: {}] 事件内通道数为0", event.getType()); + } + } + switch (event.getType()) { + case CatalogEvent.ON: + case CatalogEvent.OFF: + case CatalogEvent.DEL: + + if (parentPlatform != null) { + List channels = new ArrayList<>(); + if (event.getChannels() != null) { + channels.addAll(event.getChannels()); + } + if (!channels.isEmpty()) { + log.info("[Catalog事件: {}]平台:{},影响通道{}个", event.getType(), parentPlatform.getServerGBId(), channels.size()); + try { + sipCommanderFroPlatform.sendNotifyForCatalogOther(event.getType(), parentPlatform, channels, subscribe, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + }else if (!platformMap.keySet().isEmpty()) { + for (String serverGbId : platformMap.keySet()) { + List platformList = platformMap.get(serverGbId); + if (platformList != null && !platformList.isEmpty()) { + for (Platform platform : platformList) { + SubscribeInfo subscribeInfo = subscribeHolder.getCatalogSubscribe(platform.getServerGBId()); + if (subscribeInfo == null) { + continue; + } + log.info("[Catalog事件: {}]平台:{},影响通道{}", event.getType(), platform.getServerGBId(), serverGbId); + List deviceChannelList = new ArrayList<>(); + CommonGBChannel deviceChannel = new CommonGBChannel(); + deviceChannel.setGbDeviceId(serverGbId); + deviceChannelList.add(deviceChannel); + try { + sipCommanderFroPlatform.sendNotifyForCatalogOther(event.getType(), platform, deviceChannelList, subscribeInfo, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + }else { + log.info("[Catalog事件: {}] 未找到上级平台: {}", event.getType(), serverGbId); + } + } + } + break; + case CatalogEvent.VLOST: + break; + case CatalogEvent.DEFECT: + break; + case CatalogEvent.ADD: + case CatalogEvent.UPDATE: + if (parentPlatform != null) { + List deviceChannelList = new ArrayList<>(); + if (event.getChannels() != null) { + deviceChannelList.addAll(event.getChannels()); + } + if (!deviceChannelList.isEmpty()) { + log.info("[Catalog事件: {}]平台:{},影响通道{}个", event.getType(), parentPlatform.getServerGBId(), deviceChannelList.size()); + try { + sipCommanderFroPlatform.sendNotifyForCatalogAddOrUpdate(event.getType(), parentPlatform, deviceChannelList, subscribe, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + }else if (!platformMap.keySet().isEmpty()) { + for (String gbId : platformMap.keySet()) { + List parentPlatforms = platformMap.get(gbId); + if (parentPlatforms != null && !parentPlatforms.isEmpty()) { + for (Platform platform : parentPlatforms) { + SubscribeInfo subscribeInfo = subscribeHolder.getCatalogSubscribe(platform.getServerGBId()); + if (subscribeInfo == null) { + continue; + } + log.info("[Catalog事件: {}]平台:{},影响通道{}", event.getType(), platform.getServerGBId(), gbId); + List channelList = new ArrayList<>(); + CommonGBChannel deviceChannel = channelMap.get(gbId); + channelList.add(deviceChannel); + try { + sipCommanderFroPlatform.sendNotifyForCatalogAddOrUpdate(event.getType(), platform, channelList, subscribeInfo, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | + SipException | IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + } + } + } + break; + default: + break; + } + } +} + diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/mobilePosition/MobilePositionEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/mobilePosition/MobilePositionEvent.java new file mode 100755 index 0000000..f6a4ad7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/mobilePosition/MobilePositionEvent.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.gb28181.event.subscribe.mobilePosition; + +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + + +public class MobilePositionEvent extends ApplicationEvent { + public MobilePositionEvent(Object source) { + super(source); + } + + @Getter + @Setter + private MobilePosition mobilePosition; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/mobilePosition/MobilePositionEventLister.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/mobilePosition/MobilePositionEventLister.java new file mode 100755 index 0000000..7b06f07 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/mobilePosition/MobilePositionEventLister.java @@ -0,0 +1,75 @@ +package com.genersoft.iot.vmp.gb28181.event.subscribe.mobilePosition; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.SubscribeHolder; +import com.genersoft.iot.vmp.gb28181.bean.SubscribeInfo; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderForPlatform; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.List; + +/** + * 移动位置通知消息转发 + */ +@Slf4j +@Component +public class MobilePositionEventLister implements ApplicationListener { + + @Autowired + private IPlatformService platformService; + + @Autowired + private IPlatformChannelService platformChannelService; + + @Autowired + private SIPCommanderForPlatform sipCommanderForPlatform; + + @Autowired + private SubscribeHolder subscribeHolder; + + @Autowired + private UserSetting userSetting; + + @Override + public void onApplicationEvent(MobilePositionEvent event) { + if (event.getMobilePosition().getChannelId() == 0) { + return; + } + List allPlatforms = platformService.queryAll(userSetting.getServerId()); + // 获取所用订阅 + List platforms = subscribeHolder.getAllMobilePositionSubscribePlatform(allPlatforms); + if (platforms.isEmpty()) { + return; + } + List platformsForGB = platformChannelService.queryPlatFormListByChannelDeviceId(event.getMobilePosition().getChannelId(), platforms); + + for (Platform platform : platformsForGB) { + if (log.isDebugEnabled()){ + log.debug("[向上级发送MobilePosition] 通道:{},平台:{}, 位置: {}:{}", event.getMobilePosition().getChannelId(), + platform.getServerGBId(), event.getMobilePosition().getLongitude(), event.getMobilePosition().getLatitude()); + } + SubscribeInfo subscribe = subscribeHolder.getMobilePositionSubscribe(platform.getServerGBId()); + try { + GPSMsgInfo gpsMsgInfo = GPSMsgInfo.getInstance(event.getMobilePosition()); + // 获取通道编号 + CommonGBChannel commonGBChannel = platformChannelService.queryChannelByPlatformIdAndChannelId(platform.getId(), event.getMobilePosition().getChannelId()); + sipCommanderForPlatform.sendNotifyMobilePosition(platform, gpsMsgInfo, commonGBChannel, + subscribe); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceAlarmService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceAlarmService.java new file mode 100755 index 0000000..3677188 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceAlarmService.java @@ -0,0 +1,43 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm; +import com.github.pagehelper.PageInfo; + +import java.util.List; + +/** + * 报警相关业务处理 + */ +public interface IDeviceAlarmService { + + /** + * 根据多个添加获取报警列表 + * @param page 当前页 + * @param count 每页数量 + * @param deviceId 设备id + * @param alarmPriority 报警级别, 1为一级警情, 2为二级警情, 3为三级警情, 4为四级 警情- + * @param alarmMethod 报警方式 , 1为电话报警, 2为设备报警, 3为短信报警, 4为 GPS报警, 5为视频报警, 6为设备故障报警, + * 7其他报警;可以为直接组合如12为电话报警或 设备报警- + * @param alarmType 报警类型 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 报警列表 + */ + PageInfo getAllAlarm(int page, int count, String deviceId, String channelId, String alarmPriority, String alarmMethod, + String alarmType, String startTime, String endTime); + + /** + * 添加一个报警 + * @param deviceAlarm 添加报警 + */ + void add(DeviceAlarm deviceAlarm); + + /** + * 清空时间以前的报警 + * @param id 数据库id + * @param deviceIdList 制定需要清理的设备id + * @param time 不写时间则清空所有时间的 + */ + int clearAlarmBeforeTime(Integer id, List deviceIdList, String time); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java new file mode 100755 index 0000000..48f23a3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java @@ -0,0 +1,99 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.enums.DeviceControlType; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo; +import com.genersoft.iot.vmp.web.gb28181.dto.DeviceChannelExtend; +import com.github.pagehelper.PageInfo; +import jakarta.validation.constraints.NotNull; +import org.dom4j.Element; + +import java.util.List; + +/** + * 国标通道业务类 + * @author lin + */ +public interface IDeviceChannelService { + + /** + * 批量添加设备通道 + */ + int updateChannels(Device device, List channels); + + /** + * 获取统计信息 + * @return + */ + ResourceBaseInfo getOverview(); + + /** + * 获取一个通道 + */ + DeviceChannel getOne(String deviceId, String channelId); + + DeviceChannel getOneForSource(String deviceId, String channelId); + + /** + * 修改通道的码流类型 + */ + void updateChannelStreamIdentification(DeviceChannel channel); + + List queryChaneListByDeviceId(String deviceId); + + void updateChannelGPS(Device device, DeviceChannel deviceChannel, MobilePosition mobilePosition); + + void startPlay(Integer channelId, String stream); + + void stopPlay(Integer channelId); + + void online(DeviceChannel channel); + + void offline(DeviceChannel channel); + + void deleteForNotify(DeviceChannel channel); + + void cleanChannelsForDevice(int deviceId); + + boolean resetChannels(int deviceDbId, List deviceChannels); + + PageInfo getSubChannels(int deviceDbId, String channelId, String query, Boolean channelType, Boolean online, int page, int count); + + List queryChannelExtendsByDeviceId(String deviceId, List channelIds, Boolean online); + + PageInfo queryChannelsByDeviceId(String deviceId, String query, Boolean channelType, Boolean online, int page, int count); + + PageInfo queryChannels(String query, Boolean queryParent, Boolean channelType, Boolean online, Boolean hasStream, int page, int count); + + List queryDeviceWithAsMessageChannel(); + + DeviceChannel getRawChannel(int id); + + DeviceChannel getOneById(Integer channelId); + + DeviceChannel getOneForSourceById(Integer channelId); + + DeviceChannel getBroadcastChannel(int deviceDbId); + + void changeAudio(Integer channelId, Boolean audio); + + void updateChannelStatusForNotify(DeviceChannel channel); + + void addChannel(DeviceChannel channel); + + void updateChannelForNotify(DeviceChannel channel); + + DeviceChannel getOneForSource(int deviceDbId, String channelId); + + DeviceChannel getOneBySourceId(int deviceDbId, String channelId); + + List queryChaneIdListByDeviceDbIds(List deviceDbId); + + void handlePtzCmd(@NotNull Integer dataDeviceId, @NotNull Integer gbId, Element rootElement, DeviceControlType type, ErrorCallback callback); + + void queryRecordInfo(Device device, DeviceChannel channel, String startTime, String endTime, ErrorCallback object); + + void queryRecordInfo(CommonGBChannel channel, String startTime, String endTime, ErrorCallback object); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java new file mode 100755 index 0000000..2930c4f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java @@ -0,0 +1,205 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageInfo; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 设备相关业务处理 + * @author lin + */ +public interface IDeviceService { + + /** + * 设备上线 + * @param device 设备信息 + */ + void online(Device device, SipTransactionInfo sipTransactionInfo); + + /** + * 设备下线 + * @param deviceId 设备编号 + */ + void offline(String deviceId, String reason); + + /** + * 添加目录订阅 + * @param device 设备信息 + * @return 布尔 + */ + boolean addCatalogSubscribe(Device device, SipTransactionInfo transactionInfo); + + /** + * 移除目录订阅 + * @param device 设备信息 + * @return 布尔 + */ + boolean removeCatalogSubscribe(Device device, CommonCallback callback); + + /** + * 添加移动位置订阅 + * @param device 设备信息 + * @return 布尔 + */ + boolean addMobilePositionSubscribe(Device device, SipTransactionInfo transactionInfo); + + /** + * 移除移动位置订阅 + * @param device 设备信息 + * @return 布尔 + */ + boolean removeMobilePositionSubscribe(Device device, CommonCallback callback); + + /** + * 移除移动位置订阅 + * @param deviceId 设备ID + * @return 同步状态 + */ + SyncStatus getChannelSyncStatus(String deviceId); + + /** + * 查看是否仍在同步 + * @param deviceId 设备ID + * @return 布尔 + */ + Boolean isSyncRunning(String deviceId); + + /** + * 通道同步 + * @param device 设备信息 + */ + void sync(Device device); + + /** + * 查询设备信息 + * @param deviceId 设备编号 + * @return 设备信息 + */ + Device getDeviceByDeviceId(String deviceId); + + /** + * 获取所有在线设备 + * @return 设备列表 + */ + List getAllOnlineDevice(String serverId); + + List getAllByStatus(Boolean status); + + /** + * 判断是否注册已经失效 + * @param device 设备信息 + * @return 布尔 + */ + boolean expire(Device device); + + /** + * 检查设备状态 + * @param device 设备信息 + */ + Boolean getDeviceStatus(Device device); + + /** + * 根据IP和端口获取设备信息 + * @param host IP + * @param port 端口 + * @return 设备信息 + */ + Device getDeviceByHostAndPort(String host, int port); + + /** + * 更新设备 + * @param device 设备信息 + */ + void updateDevice(Device device); + + @Transactional + void updateDeviceList(List deviceList); + + /** + * 检查设备编号是否已经存在 + * @param deviceId 设备编号 + * @return + */ + boolean isExist(String deviceId); + + /** + * 添加设备 + * @param device + */ + void addCustomDevice(Device device); + + /** + * 页面表单更新设备信息 + * @param device + */ + void updateCustomDevice(Device device); + + /** + * 删除设备 + * @param deviceId + * @return + */ + boolean delete(String deviceId); + + /** + * 获取统计信息 + * @return + */ + ResourceBaseInfo getOverview(); + + /** + * 获取所有设备 + */ + List getAll(); + + PageInfo getAll(int page, int count, String query, Boolean status); + + Device getDevice(Integer gbDeviceDbId); + + Device getDeviceByChannelId(Integer channelId); + + Device getDeviceBySourceChannelDeviceId(String requesterId); + + void subscribeCatalog(int id, int cycle); + + void subscribeMobilePosition(int id, int cycle, int interval); + + WVPResult devicesSync(Device device); + + void deviceBasicConfig(Device device, BasicParam basicParam, ErrorCallback callback); + + void deviceConfigQuery(Device device, String channelId, String configType, ErrorCallback callback); + + void teleboot(Device device); + + void record(Device device, String channelId, String recordCmdStr, ErrorCallback callback); + + void guard(Device device, String guardCmdStr, ErrorCallback callback); + + void resetAlarm(Device device, String channelId, String alarmMethod, String alarmType, ErrorCallback callback); + + void iFrame(Device device, String channelId); + + void homePosition(Device device, String channelId, Boolean enabled, Integer resetTime, Integer presetIndex, ErrorCallback callback); + + void dragZoomIn(Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy, ErrorCallback callback); + + void dragZoomOut(Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy, ErrorCallback callback); + + void deviceStatus(Device device, ErrorCallback callback); + + void updateDeviceHeartInfo(Device device); + + void alarm(Device device, String startPriority, String endPriority, String alarmMethod, String alarmType, String startTime, String endTime, ErrorCallback callback); + + void deviceInfo(Device device, ErrorCallback callback); + + void queryPreset(Device device, String channelId, ErrorCallback> callback); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelControlService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelControlService.java new file mode 100644 index 0000000..bc28dbd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelControlService.java @@ -0,0 +1,19 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +import java.util.List; + +public interface IGbChannelControlService { + + + void ptz(CommonGBChannel channel, FrontEndControlCodeForPTZ frontEndControlCode, ErrorCallback callback); + void fi(CommonGBChannel channel, FrontEndControlCodeForFI frontEndControlCode, ErrorCallback callback); + void preset(CommonGBChannel channel, FrontEndControlCodeForPreset frontEndControlCode, ErrorCallback callback); + void tour(CommonGBChannel channel, FrontEndControlCodeForTour frontEndControlCode, ErrorCallback callback); + void scan(CommonGBChannel channel, FrontEndControlCodeForScan frontEndControlCode, ErrorCallback callback); + void wiper(CommonGBChannel channel, FrontEndControlCodeForWiper controlCode, ErrorCallback callback); + void auxiliary(CommonGBChannel channel, FrontEndControlCodeForAuxiliary frontEndControlCode, ErrorCallback callback); + void queryPreset(CommonGBChannel channel, ErrorCallback> callback); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java new file mode 100644 index 0000000..b0653e7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.CommonRecordInfo; +import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +import java.util.List; + +public interface IGbChannelPlayService { + + void startInvite(CommonGBChannel channel, InviteMessageInfo inviteInfo, Platform platform, ErrorCallback callback); + + void stopInvite(InviteSessionType type, CommonGBChannel channel, String stream); + + void playback(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback callback); + + void download(CommonGBChannel channel, Long startTime, Long stopTime, Integer downloadSpeed, + ErrorCallback callback); + + void stopPlay(CommonGBChannel channel); + + void play(CommonGBChannel channel, Platform platform, Boolean record, ErrorCallback callback); + + void stopPlayback(CommonGBChannel channel, String stream); + + void stopDownload(CommonGBChannel channel, String stream); + + void playbackPause(CommonGBChannel channel, String streamId); + + void playbackResume(CommonGBChannel channel, String streamId); + + void playbackSeek(CommonGBChannel channel, String stream, long seekTime); + + void playbackSpeed(CommonGBChannel channel, String stream, Double speed); + + void queryRecord(CommonGBChannel channel, String startTime, String endTime, ErrorCallback> callback); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java new file mode 100644 index 0000000..6f5b45a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java @@ -0,0 +1,116 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.controller.bean.Extent; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.github.pagehelper.PageInfo; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public interface IGbChannelService { + + CommonGBChannel queryByDeviceId(String gbDeviceId); + + int add(CommonGBChannel commonGBChannel); + + int delete(int gbId); + + void delete(Collection ids); + + int update(CommonGBChannel commonGBChannel); + + int offline(CommonGBChannel commonGBChannel); + + int offline(List commonGBChannelList); + + int online(CommonGBChannel commonGBChannel); + + int online(List commonGBChannelList); + + void batchAdd(List commonGBChannels); + + void updateStatus(List channelList); + + CommonGBChannel getOne(int id); + + List getIndustryCodeList(); + + List getDeviceTypeList(); + + List getNetworkIdentificationTypeList(); + + void reset(int id, List chanelFields); + + PageInfo queryListByCivilCode(int page, int count, String query, Boolean online, Integer channelType, String civilCode); + + PageInfo queryListByParentId(int page, int count, String query, Boolean online, Integer channelType, String groupDeviceId); + + void removeCivilCode(List allChildren); + + void addChannelToRegion(String civilCode, List channelIds); + + void deleteChannelToRegion(String civilCode, List channelIds); + + void deleteChannelToRegionByCivilCode(String civilCode); + + void deleteChannelToRegionByChannelIds(List channelIds); + + void addChannelToRegionByGbDevice(String civilCode, List deviceIds); + + void deleteChannelToRegionByGbDevice(List deviceIds); + + void removeParentIdByBusinessGroup(String businessGroup); + + void removeParentIdByGroupList(List groupList); + + void updateBusinessGroup(String oldBusinessGroup, String newBusinessGroup); + + void updateParentIdGroup(String oldParentId, String newParentId); + + void addChannelToGroup(String parentId, String businessGroup, List channelIds); + + void deleteChannelToGroup(String parentId, String businessGroup, List channelIds); + + void addChannelToGroupByGbDevice(String parentId, String businessGroup, List deviceIds); + + void deleteChannelToGroupByGbDevice(List deviceIds); + + void batchUpdate(List commonGBChannels); + + CommonGBChannel queryOneWithPlatform(Integer platformId, String channelDeviceId); + + void updateCivilCode(String oldCivilCode, String newCivilCode); + + List queryListByStreamPushList(List streamPushList); + + PageInfo queryList(int page, int count, String query, Boolean online, Boolean hasRecordPlan, Integer channelType, String civilCode, String parentDeviceId); + + PageInfo queryListByCivilCodeForUnusual(int page, int count, String query, Boolean online, Integer channelType); + + void clearChannelCivilCode(Boolean all, List channelIds); + + PageInfo queryListByParentForUnusual(int page, int count, String query, Boolean online, Integer channelType); + + void clearChannelParent(Boolean all, List channelIds); + + void updateGPSFromGPSMsgInfo(List gpsMsgInfoList); + + void updateGPS(List channelList); + + List queryListForMap(String query, Boolean online, Boolean hasRecordPlan, Integer channelType); + + CommonGBChannel queryCommonChannelByDeviceChannel(DeviceChannel channel); + + void resetLevel(); + + byte[] getTile(int z, int x, int y, String geoCoordSys); + + String drawThin(Map zoomParam, Extent extent, String geoCoordSys); + + DrawThinProcess thinProgress(String id); + + void saveThin(String id); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGroupService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGroupService.java new file mode 100644 index 0000000..7b58256 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGroupService.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.gb28181.bean.Group; +import com.genersoft.iot.vmp.gb28181.bean.GroupTree; +import com.github.pagehelper.PageInfo; + +import java.util.List; + + +public interface IGroupService { + + void add(Group group); + + List queryAllChildren(Integer id); + + void update(Group group); + + Group queryGroupByDeviceId(String regionDeviceId); + + List queryForTree(String query, Integer parent, Boolean hasChannel); + + boolean delete(int id); + + boolean batchAdd(List groupList); + + List getPath(String deviceId, String businessGroup); + + PageInfo queryList(Integer page, Integer count, String query); + + Group queryGroupByAlias(String groupAlias); + + void sync(); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IInviteStreamService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IInviteStreamService.java new file mode 100755 index 0000000..e8e4d3c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IInviteStreamService.java @@ -0,0 +1,85 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +import java.util.List; + +/** + * 记录国标点播的状态,包括实时预览,下载,录像回放 + */ +public interface IInviteStreamService { + + /** + * 更新点播的状态信息 + */ + void updateInviteInfo(InviteInfo inviteInfo); + + void updateInviteInfo(InviteInfo inviteInfo, Long time); + + InviteInfo updateInviteInfoForStream(InviteInfo inviteInfo, String stream); + + /** + * 获取点播的状态信息 + */ + InviteInfo getInviteInfo(InviteSessionType type, Integer channelId, String stream); + + /** + * 移除点播的状态信息 + */ + void removeInviteInfo(InviteSessionType type, Integer channelId, String stream); + /** + * 移除点播的状态信息 + */ + void removeInviteInfo(InviteInfo inviteInfo); + /** + * 移除点播的状态信息 + */ + void removeInviteInfoByDeviceAndChannel(InviteSessionType inviteSessionType, Integer channelId); + + List getAllInviteInfo(); + + /** + * 获取点播的状态信息 + */ + InviteInfo getInviteInfoByDeviceAndChannel(InviteSessionType type, Integer channelId); + + /** + * 获取点播的状态信息 + */ + InviteInfo getInviteInfoByStream(InviteSessionType type, String stream); + + + /** + * 添加一个invite回调 + */ + void once(InviteSessionType type, Integer channelId, String stream, ErrorCallback callback); + + /** + * 调用一个invite回调 + */ + void call(InviteSessionType type, Integer channelId, String stream, int code, String msg, StreamInfo data); + + /** + * 清空一个设备的所有invite信息 + */ + void clearInviteInfo(String deviceId); + + /** + * 统计同一个zlm下的国标收流个数 + */ + int getStreamInfoCount(String mediaServerId); + + + /** + * 获取MediaServer下的流信息 + */ + InviteInfo getInviteInfoBySSRC(String ssrc); + + /** + * 更新ssrc + */ + InviteInfo updateInviteInfoForSSRC(InviteInfo inviteInfo, String ssrcInResponse); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPTZService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPTZService.java new file mode 100755 index 0000000..2a871d8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPTZService.java @@ -0,0 +1,21 @@ +package com.genersoft.iot.vmp.gb28181.service; + + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Preset; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +import java.util.List; + +public interface IPTZService { + + void ptz(Device device, String channelId, int cmdCode, int horizonSpeed, int verticalSpeed, int zoomSpeed); + + void frontEndCommand(Device device, String channelId, int cmdCode, int parameter1, int parameter2, int combindCode2); + + void frontEndCommand(CommonGBChannel channel, Integer cmdCode, Integer parameter1, Integer parameter2, Integer combindCode2); + + void queryPresetList(CommonGBChannel channel, ErrorCallback> callback); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlatformChannelService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlatformChannelService.java new file mode 100755 index 0000000..0151b63 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlatformChannelService.java @@ -0,0 +1,53 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.github.pagehelper.PageInfo; + +import java.util.List; + +/** + * 平台关联通道管理 + * @author lin + */ +public interface IPlatformChannelService { + + PageInfo queryChannelList(int page, int count, String query, Integer channelType, Boolean online, Integer platformId, Boolean hasShare); + + int addAllChannel(Integer platformId); + + int removeAllChannel(Integer platformId); + + int addChannels(Integer platformId, List channelIds); + + int removeChannels(Integer platformId, List channelIds); + + void removeChannels(List ids); + + void removeChannel(int gbId); + + List queryByPlatform(Platform platform); + + void pushChannel(Integer platformId); + + void addChannelByDevice(Integer platformId, List deviceIds); + + void removeChannelByDevice(Integer platformId, List deviceIds); + + void updateCustomChannel(PlatformChannel channel); + + void checkGroupRemove(List channelList, List groups); + + void checkGroupAdd(List channelList); + + List queryPlatFormListByChannelDeviceId(Integer channelId, List platforms); + + CommonGBChannel queryChannelByPlatformIdAndChannelId(Integer platformId, Integer channelId); + + List queryChannelByPlatformIdAndChannelIds(Integer platformId, List channelIds); + + void checkRegionAdd(List channelList); + + void checkRegionRemove(List channelList, List regionList); + + List queryByPlatformBySharChannelId(String gbId); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlatformService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlatformService.java new file mode 100755 index 0000000..b0bea7d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlatformService.java @@ -0,0 +1,85 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.service.bean.InviteTimeOutCallback; +import com.github.pagehelper.PageInfo; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.List; + +/** + * 国标平台的业务类 + * @author lin + */ +public interface IPlatformService { + + Platform queryPlatformByServerGBId(String platformGbId); + + /** + * 分页获取上级平台 + * @param page + * @param count + * @return + */ + PageInfo queryPlatformList(int page, int count, String query); + + /** + * 添加级联平台 + * @param parentPlatform 级联平台 + */ + boolean add(Platform parentPlatform); + + /** + * 添加级联平台 + * @param parentPlatform 级联平台 + */ + boolean update(Platform parentPlatform); + + /** + * 平台上线 + * @param parentPlatform 平台信息 + */ + void online(Platform parentPlatform, SipTransactionInfo sipTransactionInfo); + + /** + * 平台离线 + * @param parentPlatform 平台信息 + */ + void offline(Platform parentPlatform); + + /** + * 向上级平台发送位置订阅 + * @param platformId 平台 + */ + void sendNotifyMobilePosition(String platformId); + + /** + * 向上级发送语音喊话的消息 + */ + void broadcastInvite(Platform platform, CommonGBChannel channel, String sourceId, MediaServer mediaServerItem, HookSubscribe.Event hookEvent, + SipSubscribe.Event errorEvent, InviteTimeOutCallback timeoutCallback) throws InvalidArgumentException, ParseException, SipException; + + /** + * 语音喊话回复BYE + */ + void stopBroadcast(Platform platform, CommonGBChannel channel, String app, String stream, boolean sendBye, MediaServer mediaServerItem); + + void addSimulatedSubscribeInfo(Platform parentPlatform); + + Platform queryOne(Integer platformId); + + List queryEnablePlatformList(String serverId); + + void delete(Integer platformId, CommonCallback callback); + + List queryAll(String serverId); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlayService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlayService.java new file mode 100755 index 0000000..a1f234f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlayService.java @@ -0,0 +1,85 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.exception.ServiceException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.controller.bean.AudioBroadcastEvent; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult; +import gov.nist.javax.sip.message.SIPResponse; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import javax.sip.header.CallIdHeader; +import java.text.ParseException; + +/** + * 点播处理 + */ +public interface IPlayService { + + SSRCInfo play(MediaServer mediaServerItem, String deviceId, String channelId, String ssrc, ErrorCallback callback); + + void play(Device device, DeviceChannel channel, ErrorCallback callback); + + StreamInfo onPublishHandlerForPlay(MediaServer mediaServerItem, MediaInfo mediaInfo, Device device, DeviceChannel channel); + + MediaServer getNewMediaServerItem(Device device); + + void playBack(Device device, DeviceChannel channel, String startTime, String endTime, ErrorCallback callback); + void zlmServerOffline(MediaServer mediaServer); + + void download(Device device, DeviceChannel channel, String startTime, String endTime, int downloadSpeed, ErrorCallback callback); + + StreamInfo getDownLoadInfo(Device device, DeviceChannel channel, String stream); + + void zlmServerOnline(MediaServer mediaServer); + + AudioBroadcastResult audioBroadcast(String deviceId, String channelDeviceId, Boolean broadcastMode); + + boolean audioBroadcastCmd(Device device, DeviceChannel channel, MediaServer mediaServerItem, String app, String stream, int timeout, boolean isFromPlatform, AudioBroadcastEvent event) throws InvalidArgumentException, ParseException, SipException; + + boolean audioBroadcastInUse(Device device, DeviceChannel channel); + + void stopAudioBroadcast(Device device, DeviceChannel channel); + + void playbackPause(String streamId) throws ServiceException, InvalidArgumentException, ParseException, SipException; + + void playbackResume(String streamId) throws ServiceException, InvalidArgumentException, ParseException, SipException; + + void playbackSeek(String streamId, long seekTime) throws InvalidArgumentException, ParseException, SipException; + + void playbackSpeed(String streamId, double speed) throws InvalidArgumentException, ParseException, SipException; + + void startPushStream(SendRtpInfo sendRtpItem, DeviceChannel channel, SIPResponse sipResponse, Platform platform, CallIdHeader callIdHeader); + + void startSendRtpStreamFailHand(SendRtpInfo sendRtpItem, Platform platform, CallIdHeader callIdHeader); + + void talkCmd(Device device, DeviceChannel channel, MediaServer mediaServerItem, String stream, AudioBroadcastEvent event); + + void stopTalk(Device device, DeviceChannel channel, Boolean streamIsReady); + + void getSnap(String deviceId, String channelId, String fileName, ErrorCallback errorCallback); + + void stop(InviteSessionType type, Device device, DeviceChannel channel, String stream); + + void stop(InviteInfo inviteInfo); + + void play(CommonGBChannel channel, Boolean record, ErrorCallback callback); + + void stopPlay(InviteSessionType inviteSessionType, CommonGBChannel channel); + + void stop(InviteSessionType inviteSessionType, CommonGBChannel channel, String stream); + + void playBack(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback callback); + + void download(CommonGBChannel channel, Long startTime, Long stopTime, Integer downloadSpeed, ErrorCallback callback); + + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IRegionService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IRegionService.java new file mode 100644 index 0000000..c4878fb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IRegionService.java @@ -0,0 +1,45 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.gb28181.bean.Region; +import com.genersoft.iot.vmp.gb28181.bean.RegionTree; +import com.github.pagehelper.PageInfo; + +import java.util.List; + + +public interface IRegionService { + + void add(Region region); + + boolean deleteByDeviceId(Integer regionDeviceId); + + /** + * 查询区划列表 + */ + PageInfo query(String query, int page, int count); + + /** + * 更新区域 + */ + void update(Region region); + + List getAllChild(String parent); + + Region queryRegionByDeviceId(String regionDeviceId); + + List queryForTree(Integer parent, Boolean hasChannel); + + void syncFromChannel(); + + boolean delete(int id); + + boolean batchAdd(List regionList); + + List getPath(String deviceId); + + String getDescription(String civilCode); + + void addByCivilCode(String civilCode); + + PageInfo queryList(int page, int count, String query); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourceDownloadService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourceDownloadService.java new file mode 100644 index 0000000..26c4064 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourceDownloadService.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +/** + * 资源能力接入-录像下载 + */ +public interface ISourceDownloadService { + + void download(CommonGBChannel channel, Long startTime, Long stopTime, Integer downloadSpeed, ErrorCallback callback); + + void stopDownload(CommonGBChannel channel, String stream); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePTZService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePTZService.java new file mode 100644 index 0000000..9679fb5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePTZService.java @@ -0,0 +1,28 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +import java.util.List; + +/** + * 资源能力接入-云台控制 + */ +public interface ISourcePTZService { + + void ptz(CommonGBChannel channel, FrontEndControlCodeForPTZ frontEndControlCode, ErrorCallback callback); + + void preset(CommonGBChannel channel, FrontEndControlCodeForPreset frontEndControlCode, ErrorCallback callback); + + void fi(CommonGBChannel channel, FrontEndControlCodeForFI frontEndControlCode, ErrorCallback callback); + + void tour(CommonGBChannel channel, FrontEndControlCodeForTour frontEndControlCode, ErrorCallback callback); + + void scan(CommonGBChannel channel, FrontEndControlCodeForScan frontEndControlCode, ErrorCallback callback); + + void auxiliary(CommonGBChannel channel, FrontEndControlCodeForAuxiliary frontEndControlCode, ErrorCallback callback); + + void wiper(CommonGBChannel channel, FrontEndControlCodeForWiper frontEndControlCode, ErrorCallback callback); + + void queryPreset(CommonGBChannel channel, ErrorCallback> callback); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePlayService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePlayService.java new file mode 100644 index 0000000..5c125b7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePlayService.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +/** + * 资源能力接入-实时录像 + */ +public interface ISourcePlayService { + + void play(CommonGBChannel channel, Platform platform, Boolean record, ErrorCallback callback); + + void stopPlay(CommonGBChannel channel); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePlaybackService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePlaybackService.java new file mode 100644 index 0000000..b271142 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePlaybackService.java @@ -0,0 +1,28 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.CommonRecordInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +import java.util.List; + +/** + * 资源能力接入-录像回放 + */ +public interface ISourcePlaybackService { + + void playback(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback callback); + + void stopPlayback(CommonGBChannel channel, String stream); + + void playbackPause(CommonGBChannel channel, String stream); + + void playbackResume(CommonGBChannel channel, String stream); + + void playbackSeek(CommonGBChannel channel, String stream, long seekTime); + + void playbackSpeed(CommonGBChannel channel, String stream, Double speed); + + void queryRecord(CommonGBChannel channel, String startTime, String endTime, ErrorCallback> callback); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceAlarmServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceAlarmServiceImpl.java new file mode 100755 index 0000000..7f73ddf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceAlarmServiceImpl.java @@ -0,0 +1,35 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm; +import com.genersoft.iot.vmp.gb28181.dao.DeviceAlarmMapper; +import com.genersoft.iot.vmp.gb28181.service.IDeviceAlarmService; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class DeviceAlarmServiceImpl implements IDeviceAlarmService { + + @Autowired + private DeviceAlarmMapper deviceAlarmMapper; + + @Override + public PageInfo getAllAlarm(int page, int count, String deviceId, String channelId, String alarmPriority, String alarmMethod, String alarmType, String startTime, String endTime) { + PageHelper.startPage(page, count); + List all = deviceAlarmMapper.query(deviceId, channelId, alarmPriority, alarmMethod, alarmType, startTime, endTime); + return new PageInfo<>(all); + } + + @Override + public void add(DeviceAlarm deviceAlarm) { + deviceAlarmMapper.add(deviceAlarm); + } + + @Override + public int clearAlarmBeforeTime(Integer id, List deviceIdList, String time) { + return deviceAlarmMapper.clearAlarmBeforeTime(id, deviceIdList, time); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java new file mode 100755 index 0000000..73e316d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java @@ -0,0 +1,685 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.common.enums.DeviceControlType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.dao.DeviceChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.DeviceMapper; +import com.genersoft.iot.vmp.gb28181.dao.DeviceMobilePositionMapper; +import com.genersoft.iot.vmp.gb28181.dao.PlatformChannelMapper; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEndEvent; +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.Coordtransform; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo; +import com.genersoft.iot.vmp.web.gb28181.dto.DeviceChannelExtend; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +/** + * @author lin + */ +@Slf4j +@Service +public class DeviceChannelServiceImpl implements IDeviceChannelService { + + @Autowired + private EventPublisher eventPublisher; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private DeviceChannelMapper channelMapper; + + @Autowired + private PlatformChannelMapper platformChannelMapper; + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private DeviceMobilePositionMapper deviceMobilePositionMapper; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IPlatformChannelService platformChannelService; + + @Autowired + private IRedisRpcPlayService redisRpcPlayService; + + @Autowired + private ISIPCommander commander; + + // 记录录像查询的结果等待 + private final Map> topicSubscribers = new ConcurrentHashMap<>(); + + /** + * 监听录像查询结束事件 + */ + @Async("taskExecutor") + @org.springframework.context.event.EventListener + public void onApplicationEvent(RecordInfoEndEvent event) { + SynchronousQueue queue = topicSubscribers.get("record" + event.getRecordInfo().getSn()); + if (queue != null) { + queue.offer(event.getRecordInfo()); + } + } + + @Autowired + private ISIPCommander cmder; + + + @Override + public int updateChannels(Device device, List channels) { + if (CollectionUtils.isEmpty(channels)) { + return 0; + } + List addChannels = new ArrayList<>(); + List updateChannels = new ArrayList<>(); + HashMap channelsInStore = new HashMap<>(); + int result = 0; + List channelList = channelMapper.queryChannelsByDeviceDbId(device.getId()); + if (channelList.isEmpty()) { + for (DeviceChannel channel : channels) { + channel.setDataDeviceId(device.getId()); + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + if (inviteInfo != null && inviteInfo.getStreamInfo() != null) { + channel.setStreamId(inviteInfo.getStreamInfo().getStream()); + } + String now = DateUtil.getNow(); + channel.setUpdateTime(now); + channel.setCreateTime(now); + addChannels.add(channel); + } + }else { + for (DeviceChannel deviceChannel : channelList) { + channelsInStore.put(deviceChannel.getDataDeviceId() + deviceChannel.getDeviceId(), deviceChannel); + } + for (DeviceChannel channel : channels) { + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + if (inviteInfo != null && inviteInfo.getStreamInfo() != null) { + channel.setStreamId(inviteInfo.getStreamInfo().getStream()); + } + String now = DateUtil.getNow(); + channel.setUpdateTime(now); + DeviceChannel deviceChannelInDb = channelsInStore.get(channel.getDataDeviceId() + channel.getDeviceId()); + if ( deviceChannelInDb != null) { + channel.setId(deviceChannelInDb.getId()); + channel.setUpdateTime(now); + updateChannels.add(channel); + }else { + channel.setCreateTime(now); + channel.setUpdateTime(now); + addChannels.add(channel); + } + } + } + Set channelSet = new HashSet<>(); + // 滤重 + List addChannelList = new ArrayList<>(); + List updateChannelList = new ArrayList<>(); + addChannels.forEach(channel -> { + if (channelSet.add(channel.getDeviceId())) { + addChannelList.add(channel); + } + }); + channelSet.clear(); + updateChannels.forEach(channel -> { + if (channelSet.add(channel.getDeviceId())) { + updateChannelList.add(channel); + } + }); + + int limitCount = 500; + if (!addChannelList.isEmpty()) { + if (addChannelList.size() > limitCount) { + for (int i = 0; i < addChannelList.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > addChannelList.size()) { + toIndex = addChannelList.size(); + } + result += channelMapper.batchAdd(addChannelList.subList(i, toIndex)); + } + }else { + result += channelMapper.batchAdd(addChannelList); + } + } + if (!updateChannelList.isEmpty()) { + if (updateChannelList.size() > limitCount) { + for (int i = 0; i < updateChannelList.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > updateChannelList.size()) { + toIndex = updateChannelList.size(); + } + result += channelMapper.batchUpdate(updateChannelList.subList(i, toIndex)); + } + }else { + result += channelMapper.batchUpdate(updateChannelList); + } + } + return result; + } + + @Override + public ResourceBaseInfo getOverview() { + int online = channelMapper.getOnlineCount(); + int total = channelMapper.getAllChannelCount(); + return new ResourceBaseInfo(total, online); + } + + @Override + public void online(DeviceChannel channel) { + channelMapper.online(channel.getId()); + } + + @Override + public void offline(DeviceChannel channel) { + channelMapper.offline(channel.getId()); + } + + @Override + public void deleteForNotify(DeviceChannel channel) { + channelMapper.deleteForNotify(channel); + } + + @Override + public DeviceChannel getOne(String deviceId, String channelId){ + Device device = deviceMapper.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId); + } + return channelMapper.getOneByDeviceId(device.getId(), channelId); + } + + @Override + public DeviceChannel getOneForSource(String deviceId, String channelId){ + Device device = deviceMapper.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId); + } + return channelMapper.getOneByDeviceIdForSource(device.getId(), channelId); + } + + @Override + public DeviceChannel getOneForSource(int deviceDbId, String channelId) { + return channelMapper.getOneByDeviceIdForSource(deviceDbId, channelId); + } + + @Override + public DeviceChannel getOneBySourceId(int deviceDbId, String channelId) { + return channelMapper.getOneBySourceChannelId(deviceDbId, channelId); + } + + @Override + public void updateChannelStreamIdentification(DeviceChannel channel) { + Assert.hasLength(channel.getStreamIdentification(), "码流标识必须存在"); + if (ObjectUtils.isEmpty(channel.getStreamIdentification())) { + log.info("[重置通道码流类型] 设备: {}, 码流: {}", channel.getDeviceId(), channel.getStreamIdentification()); + }else { + log.info("[更新通道码流类型] 设备: {}, 通道:{}, 码流: {}", channel.getDeviceId(), channel.getDeviceId(), + channel.getStreamIdentification()); + } + if (channel.getId() > 0) { + channelMapper.updateChannelStreamIdentification(channel); + }else { + channelMapper.updateAllChannelStreamIdentification(channel.getStreamIdentification()); + } + } + + @Override + public List queryChaneListByDeviceId(String deviceId) { + Device device = deviceMapper.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道:" + deviceId); + } + return channelMapper.queryChannelsByDeviceDbId(device.getId()); + } + + @Override + public List queryChaneIdListByDeviceDbIds(List deviceDbIds) { + return channelMapper.queryChaneIdListByDeviceDbIds(deviceDbIds); + } + + @Override + public void handlePtzCmd(@NotNull Integer dataDeviceId, @NotNull Integer gbId, Element rootElement, DeviceControlType type, ErrorCallback callback) { + + // 根据通道ID,获取所属设备 + Device device = deviceMapper.query(dataDeviceId); + if (device == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道所属设备不存在, 设备ID: {}", dataDeviceId); + callback.run(Response.NOT_FOUND, "device not found", null); + return; + } + + DeviceChannel deviceChannel = channelMapper.getOneForSource(gbId); + if (deviceChannel == null) { + log.warn("[deviceControl] 未找到设备原始通道, 设备: {}({}),通道编号:{}", device.getName(), + device.getDeviceId(), gbId); + callback.run(Response.NOT_FOUND, "channel not found", null); + return; + } + log.info("[deviceControl] 命令: {}, 设备: {}({}), 通道{}({}", type, device.getName(), device.getDeviceId(), + deviceChannel.getName(), deviceChannel.getDeviceId()); + String cmdString = getText(rootElement, type.getVal()); + try { + cmder.fronEndCmd(device, deviceChannel.getDeviceId(), cmdString, errorResult->{ + callback.run(errorResult.statusCode, errorResult.msg, null); + }, errorResult->{ + callback.run(errorResult.statusCode, errorResult.msg, null); + }); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 云台/前端: {}", e.getMessage()); + } + } + + @Override + public void updateChannelGPS(Device device, DeviceChannel deviceChannel, MobilePosition mobilePosition) { + + if (device.getGeoCoordSys().equalsIgnoreCase("GCJ02")) { + Double[] wgs84Position = Coordtransform.GCJ02ToWGS84(mobilePosition.getLongitude(), mobilePosition.getLatitude()); + mobilePosition.setLongitude(wgs84Position[0]); + mobilePosition.setLatitude(wgs84Position[1]); + + Double[] wgs84PositionForChannel = Coordtransform.GCJ02ToWGS84(deviceChannel.getLongitude(), deviceChannel.getLatitude()); + deviceChannel.setGbLongitude(wgs84PositionForChannel[0]); + deviceChannel.setGbLatitude(wgs84PositionForChannel[1]); + } + + if (userSetting.getSavePositionHistory()) { + deviceMobilePositionMapper.insertNewPosition(mobilePosition); + } + + if (deviceChannel.getDeviceId().equals(device.getDeviceId())) { + deviceChannel.setDeviceId(null); + } + if (deviceChannel.getGpsTime() == null) { + deviceChannel.setGpsTime(DateUtil.getNow()); + } + + int updated = channelMapper.updatePosition(deviceChannel); + if (updated == 0) { + return; + } + + List deviceChannels = new ArrayList<>(); + if (deviceChannel.getDeviceId() == null) { + // 有的设备这里上报的deviceId与通道Id是一样,这种情况更新设备下的全部通道 + List deviceChannelsInDb = queryChaneListByDeviceId(device.getDeviceId()); + deviceChannels.addAll(deviceChannelsInDb); + }else { + deviceChannels.add(deviceChannel); + } + if (deviceChannels.isEmpty()) { + return; + } + if (deviceChannels.size() > 100) { + log.warn("[更新通道位置信息后发送通知] 设备可能是平台,上报的位置信息未标明通道编号," + + "导致所有通道被更新位置, deviceId:{}", device.getDeviceId()); + } + for (DeviceChannel channel : deviceChannels) { + // 向关联了该通道并且开启移动位置订阅的上级平台发送移动位置订阅消息 + mobilePosition.setChannelId(channel.getId()); + mobilePosition.setChannelDeviceId(channel.getDeviceId()); + try { + eventPublisher.mobilePositionEventPublish(mobilePosition); + }catch (Exception e) { + log.error("[向上级转发移动位置失败] ", e); + } + } + } + + @Override + public void startPlay(Integer channelId, String stream) { + channelMapper.startPlay(channelId, stream); + } + + @Override + public void stopPlay(Integer channelId) { + channelMapper.stopPlayById(channelId); + } + + @Override + public void cleanChannelsForDevice(int deviceId) { + channelMapper.cleanChannelsByDeviceId(deviceId); + } + + @Override + @Transactional + public boolean resetChannels(int deviceDbId, List deviceChannelList) { + if (CollectionUtils.isEmpty(deviceChannelList)) { + return false; + } + List allChannels = channelMapper.queryAllChannelsForRefresh(deviceDbId); + Map allChannelMap = new HashMap<>(); + if (!allChannels.isEmpty()) { + for (DeviceChannel deviceChannel : allChannels) { + allChannelMap.put(deviceChannel.getDataDeviceId() + deviceChannel.getDeviceId(), deviceChannel); + } + } + // 数据去重 + List channels = new ArrayList<>(); + + List updateChannels = new ArrayList<>(); + List addChannels = new ArrayList<>(); + List deleteChannels = new ArrayList<>(); + StringBuilder stringBuilder = new StringBuilder(); + Map subContMap = new HashMap<>(); + + for (DeviceChannel deviceChannel : deviceChannelList) { + DeviceChannel channelInDb = allChannelMap.get(deviceChannel.getDataDeviceId() + deviceChannel.getDeviceId()); + if (channelInDb != null) { + deviceChannel.setStreamId(channelInDb.getStreamId()); + deviceChannel.setHasAudio(channelInDb.isHasAudio()); + deviceChannel.setId(channelInDb.getId()); + if (channelInDb.getStatus() != null && !channelInDb.getStatus().equalsIgnoreCase(deviceChannel.getStatus())){ + List platformList = platformChannelMapper.queryParentPlatformByChannelId(deviceChannel.getDeviceId()); + if (!CollectionUtils.isEmpty(platformList)){ + platformList.forEach(platform->{ + eventPublisher.catalogEventPublish(platform, deviceChannel.buildCommonGBChannelForStatus(), deviceChannel.getStatus().equals("ON")? CatalogEvent.ON:CatalogEvent.OFF); + }); + } + } + deviceChannel.setUpdateTime(DateUtil.getNow()); + updateChannels.add(deviceChannel); + }else { + deviceChannel.setCreateTime(DateUtil.getNow()); + deviceChannel.setUpdateTime(DateUtil.getNow()); + addChannels.add(deviceChannel); + } + allChannelMap.remove(deviceChannel.getDataDeviceId() + deviceChannel.getDeviceId()); + channels.add(deviceChannel); + if (!ObjectUtils.isEmpty(deviceChannel.getParentId())) { + if (subContMap.get(deviceChannel.getParentId()) == null) { + subContMap.put(deviceChannel.getParentId(), 1); + }else { + Integer count = subContMap.get(deviceChannel.getParentId()); + subContMap.put(deviceChannel.getParentId(), count++); + } + } + } + deleteChannels.addAll(allChannelMap.values()); + if (!channels.isEmpty()) { + for (DeviceChannel channel : channels) { + if (subContMap.get(channel.getDeviceId()) != null){ + Integer count = subContMap.get(channel.getDeviceId()); + if (count > 0) { + channel.setSubCount(count); + channel.setParental(1); + } + } + } + } + + if (stringBuilder.length() > 0) { + log.info("[目录查询]收到的数据存在重复: {}" , stringBuilder); + } + if(CollectionUtils.isEmpty(channels)){ + log.info("通道重设,数据为空={}" , deviceChannelList); + return false; + } + int limitCount = 500; + if (!addChannels.isEmpty()) { + if (addChannels.size() > limitCount) { + for (int i = 0; i < addChannels.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > addChannels.size()) { + toIndex = addChannels.size(); + } + channelMapper.batchAdd(addChannels.subList(i, toIndex)); + } + }else { + channelMapper.batchAdd(addChannels); + } + } + if (!updateChannels.isEmpty()) { + if (updateChannels.size() > limitCount) { + for (int i = 0; i < updateChannels.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > updateChannels.size()) { + toIndex = updateChannels.size(); + } + channelMapper.batchUpdate(updateChannels.subList(i, toIndex)); + } + }else { + channelMapper.batchUpdate(updateChannels); + } + // 不对收到的通道做比较,已确定是否真的发生变化,所以不发送更新通知 + + } + if (!deleteChannels.isEmpty()) { + try { + // 这些通道可能关联了,上级平台需要删除同时发送消息 + List ids = new ArrayList<>(); + deleteChannels.stream().forEach(deviceChannel -> { + ids.add(deviceChannel.getId()); + }); + platformChannelService.removeChannels(ids); + }catch (Exception e) { + log.error("[移除通道国标级联共享失败]", e); + } + if (deleteChannels.size() > limitCount) { + for (int i = 0; i < deleteChannels.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > deleteChannels.size()) { + toIndex = deleteChannels.size(); + } + channelMapper.batchDel(deleteChannels.subList(i, toIndex)); + } + }else { + channelMapper.batchDel(deleteChannels); + } + } + return true; + + } + + @Override + public PageInfo getSubChannels(int deviceDbId, String channelId, String query, Boolean channelType, Boolean online, int page, int count) { + PageHelper.startPage(page, count); + String civilCode = null; + String parentId = null; + String businessGroupId = null; + if (channelId.length() <= 8) { + civilCode = channelId; + }else { + GbCode decode = GbCode.decode(channelId); + if (Integer.parseInt(decode.getTypeCode()) == 215) { + businessGroupId = channelId; + }else { + parentId = channelId; + } + } + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = channelMapper.queryChannels(deviceDbId, civilCode, businessGroupId, parentId, query, false, channelType, online, null, null); + return new PageInfo<>(all); + } + + @Override + public List queryChannelExtendsByDeviceId(String deviceId, List channelIds, Boolean online) { + return channelMapper.queryChannelsWithDeviceInfo(deviceId, null,null, null, online,channelIds); + } + + @Override + public PageInfo queryChannelsByDeviceId(String deviceId, String query, Boolean hasSubChannel, Boolean online, int page, int count) { + Device device = deviceMapper.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId); + } + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + PageHelper.startPage(page, count); + List all = channelMapper.queryChannels(device.getId(), null, null, null, query, false, hasSubChannel, online, null, null); + return new PageInfo<>(all); + } + + @Override + public PageInfo queryChannels(String query, Boolean queryParent, Boolean hasSubChannel, Boolean online, Boolean hasStream, int page, int count) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = channelMapper.queryChannels(null, null, null, null, query, queryParent, hasSubChannel, online, null, hasStream); + return new PageInfo<>(all); + } + + @Override + public List queryDeviceWithAsMessageChannel() { + return deviceMapper.queryDeviceWithAsMessageChannel(); + } + + @Override + public DeviceChannel getRawChannel(int id) { + return deviceMapper.getRawChannel(id); + } + + @Override + public DeviceChannel getOneById(Integer channelId) { + return channelMapper.getOne(channelId); + } + + @Override + public DeviceChannel getOneForSourceById(Integer channelId) { + return channelMapper.getOneForSource(channelId); + } + + @Override + public DeviceChannel getBroadcastChannel(int deviceDbId) { + List channels = channelMapper.queryChannelsByDeviceDbId(deviceDbId); + if (channels.size() == 1) { + return channels.get(0); + } + for (DeviceChannel channel : channels) { + // 获取137类型的 + if (SipUtils.isFrontEnd(channel.getDeviceId())) { + return channel; + } + } + return null; + } + + @Override + public void changeAudio(Integer channelId, Boolean audio) { + channelMapper.changeAudio(channelId, audio); + } + + @Override + public void updateChannelStatusForNotify(DeviceChannel channel) { + channelMapper.updateStatus(channel); + } + + @Override + public void addChannel(DeviceChannel channel) { + channel.setDataType(ChannelDataType.GB28181); + channel.setDataDeviceId(channel.getDataDeviceId()); + channelMapper.add(channel); + } + + @Override + public void updateChannelForNotify(DeviceChannel channel) { + channelMapper.updateChannelForNotify(channel); + } + + @Override + public void queryRecordInfo(Device device, DeviceChannel channel, String startTime, String endTime, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())){ + redisRpcPlayService.queryRecordInfo(device.getServerId(), channel.getId(), startTime, endTime, callback); + return; + } + try { + int sn = (int)((Math.random()*9+1)*100000); + commander.recordInfoQuery(device, channel.getDeviceId(), startTime, endTime, sn, null, null, eventResult -> { + try { + // 消息发送成功, 监听等待数据到来 + SynchronousQueue queue = new SynchronousQueue<>(); + topicSubscribers.put("record" + sn, queue); + RecordInfo recordInfo = queue.poll(userSetting.getRecordInfoTimeout(), TimeUnit.MILLISECONDS); + if (recordInfo != null) { + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), recordInfo); + }else { + callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), recordInfo); + } + } catch (InterruptedException e) { + callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null); + } finally { + this.topicSubscribers.remove("record" + sn); + } + + }, (eventResult -> { + callback.run(ErrorCode.ERROR100.getCode(), "查询录像失败, status: " + eventResult.statusCode + ", message: " + eventResult.msg, null); + })); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 查询录像: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void queryRecordInfo(CommonGBChannel channel, String startTime, String endTime, ErrorCallback callback) { + if (channel.getDataType() != ChannelDataType.GB28181){ + // 只支持国标的语音喊话 + log.warn("[INFO 消息] 非国标设备, 通道ID: {}", channel.getGbId()); + callback.run(ErrorCode.ERROR100.getCode(), "非国标设备", null); + return; + } + Device device = deviceMapper.query(channel.getDataDeviceId()); + if (device == null) { + log.warn("[点播] 未找到通道{}的设备信息", channel); + callback.run(ErrorCode.ERROR100.getCode(), "设备不存在", null); + return; + } + DeviceChannel deviceChannel = getOneForSourceById(channel.getGbId()); + queryRecordInfo(device, deviceChannel, startTime, endTime, callback); + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java new file mode 100755 index 0000000..f20d9a2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java @@ -0,0 +1,1280 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.DeviceChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.DeviceMapper; +import com.genersoft.iot.vmp.gb28181.dao.PlatformChannelMapper; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.channel.ChannelEvent; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.session.AudioBroadcastManager; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.task.deviceStatus.DeviceStatusTask; +import com.genersoft.iot.vmp.gb28181.task.deviceStatus.DeviceStatusTaskInfo; +import com.genersoft.iot.vmp.gb28181.task.deviceStatus.DeviceStatusTaskRunner; +import com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.SubscribeTask; +import com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.SubscribeTaskInfo; +import com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.SubscribeTaskRunner; +import com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.impl.SubscribeTaskForCatalog; +import com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.impl.SubscribeTaskForMobilPosition; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd.CatalogResponseMessageHandler; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import gov.nist.javax.sip.message.SIPResponse; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import javax.sip.InvalidArgumentException; +import javax.sip.ResponseEvent; +import javax.sip.SipException; +import java.text.ParseException; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; + +/** + * 设备业务(目录订阅) + */ +@Slf4j +@Service +@Order(value=16) +public class DeviceServiceImpl implements IDeviceService, CommandLineRunner { + + @Autowired + private ISIPCommander sipCommander; + + @Autowired + private CatalogResponseMessageHandler catalogResponseMessageHandler; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private PlatformChannelMapper platformChannelMapper; + + @Autowired + private DeviceChannelMapper deviceChannelMapper; + + @Autowired + private CommonGBChannelMapper commonGBChannelMapper; + + @Autowired + private EventPublisher eventPublisher; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private ISIPCommander commander; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private AudioBroadcastManager audioBroadcastManager; + + @Autowired + private IRedisRpcService redisRpcService; + + @Autowired + private SubscribeTaskRunner subscribeTaskRunner; + + @Autowired + private DeviceStatusTaskRunner deviceStatusTaskRunner; + + private Device getDeviceByDeviceIdFromDb(String deviceId) { + return deviceMapper.getDeviceByDeviceId(deviceId); + } + + @Override + public void run(String... args) throws Exception { + + // 清理数据库不存在但是redis中存在的数据 + List devicesInDb = getAll(); + if (devicesInDb.isEmpty()) { + redisCatchStorage.removeAllDevice(); + }else { + List devicesInRedis = redisCatchStorage.getAllDevices(); + if (!devicesInRedis.isEmpty()) { + Map deviceMapInDb = new HashMap<>(); + devicesInDb.parallelStream().forEach(device -> { + deviceMapInDb.put(device.getDeviceId(), device); + }); + devicesInRedis.parallelStream().forEach(device -> { + if (deviceMapInDb.get(device.getDeviceId()) == null + && userSetting.getServerId().equals(device.getServerId())) { + redisCatchStorage.removeDevice(device.getDeviceId()); + } + }); + } + } + + // 重置cseq计数 + redisCatchStorage.resetAllCSEQ(); + // 处理设备状态 + List allTaskInfo = deviceStatusTaskRunner.getAllTaskInfo(); + List onlineDeviceIds = new ArrayList<>(); + if (!allTaskInfo.isEmpty()) { + for (DeviceStatusTaskInfo taskInfo : allTaskInfo) { + Device device = getDeviceByDeviceId(taskInfo.getDeviceId()); + if (device == null) { + deviceStatusTaskRunner.removeTask(taskInfo.getDeviceId()); + continue; + } + // 恢复定时任务, TCP因为连接已经断开必须等待设备重新连接 + DeviceStatusTask deviceStatusTask = DeviceStatusTask.getInstance(taskInfo.getDeviceId(), + taskInfo.getTransactionInfo(), taskInfo.getExpireTime() + 1000 + System.currentTimeMillis(), this::deviceStatusExpire); + deviceStatusTaskRunner.addTask(deviceStatusTask); + onlineDeviceIds.add(taskInfo.getDeviceId()); + } + // 除了记录的设备以外, 其他设备全部离线 + List onlineDevice = getAllOnlineDevice(userSetting.getServerId()); + if (!onlineDevice.isEmpty()) { + List offlineDevices = new ArrayList<>(); + for (Device device : onlineDevice) { + if (!onlineDeviceIds.contains(device.getDeviceId())) { + // 此设备需要离线 + device.setOnLine(false); + // 清理离线设备的相关缓存 + cleanOfflineDevice(device); + // 更新数据库 + offlineDevices.add(device); + } + } + if (!offlineDevices.isEmpty()) { + offlineByIds(offlineDevices); + } + } + }else { + // 所有设备全部离线 + List onlineDevice = getAllOnlineDevice(userSetting.getServerId()); + for (Device device : onlineDevice) { + // 此设备需要离线 + device.setOnLine(false); + // 清理离线设备的相关缓存 + cleanOfflineDevice(device); + } + offlineByIds(onlineDevice); + } + + // 处理订阅任务 + List taskInfoList = subscribeTaskRunner.getAllTaskInfo(); + if (!taskInfoList.isEmpty()) { + for (SubscribeTaskInfo taskInfo : taskInfoList) { + if (taskInfo == null) { + continue; + } + Device device = getDeviceByDeviceId(taskInfo.getDeviceId()); + if (device == null || !device.isOnLine() || !onlineDeviceIds.contains(taskInfo.getDeviceId())) { + subscribeTaskRunner.removeSubscribe(taskInfo.getKey()); + continue; + } + if (SubscribeTaskForCatalog.name.equals(taskInfo.getName())) { + device.setSubscribeCycleForCatalog((int)taskInfo.getExpireTime()); + SubscribeTask subscribeTask = SubscribeTaskForCatalog.getInstance(device, this::catalogSubscribeExpire, taskInfo.getTransactionInfo()); + if (subscribeTask != null) { + subscribeTaskRunner.addSubscribe(subscribeTask); + } + }else if (SubscribeTaskForMobilPosition.name.equals(taskInfo.getName())) { + device.setSubscribeCycleForMobilePosition((int)taskInfo.getExpireTime()); + SubscribeTask subscribeTask = SubscribeTaskForMobilPosition.getInstance(device, this::mobilPositionSubscribeExpire, taskInfo.getTransactionInfo()); + if (subscribeTask != null) { + subscribeTaskRunner.addSubscribe(subscribeTask); + } + } + } + } + } + + private void offlineByIds(List offlineDevices) { + if (offlineDevices.isEmpty()) { + log.info("[更新多个离线设备信息] 参数为空"); + return; + } + deviceMapper.offlineByList(offlineDevices); + for (Device device : offlineDevices) { + device.setOnLine(false); + redisCatchStorage.updateDevice(device); + } + } + + private void cleanOfflineDevice(Device device) { + if (subscribeTaskRunner.containsKey(SubscribeTaskForCatalog.getKey(device))) { + subscribeTaskRunner.removeSubscribe(SubscribeTaskForCatalog.getKey(device)); + } + if (subscribeTaskRunner.containsKey(SubscribeTaskForMobilPosition.getKey(device))) { + subscribeTaskRunner.removeSubscribe(SubscribeTaskForMobilPosition.getKey(device)); + } + // 离线释放所有ssrc + List ssrcTransactions = sessionManager.getSsrcTransactionByDeviceId(device.getDeviceId()); + if (ssrcTransactions != null && !ssrcTransactions.isEmpty()) { + for (SsrcTransaction ssrcTransaction : ssrcTransactions) { + mediaServerService.releaseSsrc(ssrcTransaction.getMediaServerId(), ssrcTransaction.getSsrc()); + mediaServerService.closeRTPServer(ssrcTransaction.getMediaServerId(), ssrcTransaction.getStream()); + sessionManager.removeByCallId(ssrcTransaction.getCallId()); + } + } + // 移除订阅 + removeCatalogSubscribe(device, null); + removeMobilePositionSubscribe(device, null); + + List audioBroadcastCatches = audioBroadcastManager.getByDeviceId(device.getDeviceId()); + if (!audioBroadcastCatches.isEmpty()) { + for (AudioBroadcastCatch audioBroadcastCatch : audioBroadcastCatches) { + + SendRtpInfo sendRtpItem = sendRtpServerService.queryByChannelId(audioBroadcastCatch.getChannelId(), device.getDeviceId()); + if (sendRtpItem != null) { + sendRtpServerService.delete(sendRtpItem); + MediaServer mediaInfo = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + mediaServerService.stopSendRtp(mediaInfo, sendRtpItem.getApp(), sendRtpItem.getStream(), null); + } + + audioBroadcastManager.del(audioBroadcastCatch.getChannelId()); + } + } + } + + private void deviceStatusExpire(String deviceId, SipTransactionInfo transactionInfo) { + log.info("[设备状态] 到期, 编号: {}", deviceId); + offline(deviceId, "保活到期"); + } + + @Override + public void online(Device device, SipTransactionInfo sipTransactionInfo) { + log.info("[设备上线] deviceId:{}->{}:{}", device.getDeviceId(), device.getIp(), device.getPort()); + Device deviceInRedis = redisCatchStorage.getDevice(device.getDeviceId()); + Device deviceInDb = getDeviceByDeviceIdFromDb(device.getDeviceId()); + + String now = DateUtil.getNow(); + if (deviceInRedis != null && deviceInDb == null) { + // redis 存在脏数据 + inviteStreamService.clearInviteInfo(device.getDeviceId()); + } + device.setUpdateTime(now); + device.setKeepaliveTime(now); + if (device.getHeartBeatCount() == null) { + // 读取设备配置, 获取心跳间隔和心跳超时次数, 在次之前暂时设置为默认值 + device.setHeartBeatCount(3); + device.setHeartBeatInterval(60); + device.setPositionCapability(0); + + } + if (sipTransactionInfo != null) { + device.setSipTransactionInfo(sipTransactionInfo); + }else { + if (deviceInRedis != null) { + device.setSipTransactionInfo(deviceInRedis.getSipTransactionInfo()); + } + } + + // 第一次上线 或则设备之前是离线状态--进行通道同步和设备信息查询 + if (deviceInDb == null) { + device.setOnLine(true); + device.setCreateTime(now); + device.setUpdateTime(now); + log.info("[设备上线,首次注册]: {},查询设备信息以及通道信息", device.getDeviceId()); + if(device.getStreamMode() == null) { + device.setStreamMode("TCP-PASSIVE"); + } + deviceMapper.add(device); + redisCatchStorage.updateDevice(device); + try { + commander.deviceInfoQuery(device, null); + commander.deviceConfigQuery(device, null, "BasicParam", null); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 查询设备信息: {}", e.getMessage()); + } + sync(device); + }else { + device.setServerId(userSetting.getServerId()); + if(!deviceInDb.isOnLine()){ + device.setOnLine(true); + device.setCreateTime(now); + deviceMapper.update(device); + redisCatchStorage.updateDevice(device); + if (userSetting.getSyncChannelOnDeviceOnline()) { + log.info("[设备上线,离线状态下重新注册]: {},查询设备信息以及通道信息", device.getDeviceId()); + try { + commander.deviceInfoQuery(device, null); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 查询设备信息: {}", e.getMessage()); + } + sync(device); + }else { + if (isDevice(device.getDeviceId())) { + sync(device); + } + } + // 上线添加订阅 + if (device.getSubscribeCycleForCatalog() > 0 && !subscribeTaskRunner.containsKey(SubscribeTaskForCatalog.getKey(device))) { + // 查询在线设备那些开启了订阅,为设备开启定时的目录订阅 + addCatalogSubscribe(device, null); + } + if (device.getSubscribeCycleForMobilePosition() > 0 && !subscribeTaskRunner.containsKey(SubscribeTaskForMobilPosition.getKey(device))) { + addMobilePositionSubscribe(device, null); + } + + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), null, true); + } + }else { + deviceMapper.update(device); + redisCatchStorage.updateDevice(device); + } + if (deviceChannelMapper.queryChannelsByDeviceDbId(device.getId()).isEmpty()) { + log.info("[设备上线]: {},通道数为0,查询通道信息", device.getDeviceId()); + sync(device); + } + } + long expiresTime = Math.min(device.getExpires(), device.getHeartBeatInterval() * device.getHeartBeatCount()) * 1000L; + if (deviceStatusTaskRunner.containsKey(device.getDeviceId())) { + if (sipTransactionInfo == null) { + deviceStatusTaskRunner.updateDelay(device.getDeviceId(), expiresTime + System.currentTimeMillis()); + }else { + deviceStatusTaskRunner.removeTask(device.getDeviceId()); + DeviceStatusTask task = DeviceStatusTask.getInstance(device.getDeviceId(), sipTransactionInfo, expiresTime + System.currentTimeMillis(), this::deviceStatusExpire); + deviceStatusTaskRunner.addTask(task); + } + }else { + DeviceStatusTask task = DeviceStatusTask.getInstance(device.getDeviceId(), sipTransactionInfo, expiresTime + System.currentTimeMillis(), this::deviceStatusExpire); + deviceStatusTaskRunner.addTask(task); + } + + } + + @Override + @Transactional + public void offline(String deviceId, String reason) { + Device device = getDeviceByDeviceIdFromDb(deviceId); + if (device == null) { + log.warn("[设备不存在] device:{}", deviceId); + return; + } + + // 主动查询设备状态, 没有HostAddress无法发送请求,可能是手动添加的设备 + if (device.getHostAddress() != null) { + Boolean deviceStatus = getDeviceStatus(device); + if (deviceStatus != null && deviceStatus) { + log.info("[设备离线] 主动探测发现设备在线,暂不处理 device:{}", deviceId); + online(device, null); + return; + } + } + log.info("[设备离线] {}, device:{}, 心跳间隔: {},心跳超时次数: {}, 上次心跳时间:{}, 上次注册时间: {}", reason, deviceId, + device.getHeartBeatInterval(), device.getHeartBeatCount(), device.getKeepaliveTime(), device.getRegisterTime()); + device.setOnLine(false); + cleanOfflineDevice(device); + redisCatchStorage.updateDevice(device); + deviceMapper.update(device); + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), null, false); + } + if (isDevice(deviceId)) { + channelOfflineByDevice(device); + } + } + + private void channelOfflineByDevice(Device device) { + // 进行通道离线 + List channelList = commonGBChannelMapper.queryOnlineListsByGbDeviceId(device.getId()); + if (channelList.isEmpty()) { + return; + } + deviceChannelMapper.offlineByDeviceId(device.getId()); + // 发送通道离线通知 + eventPublisher.channelEventPublish(channelList, ChannelEvent.ChannelEventMessageType.OFF); + } + + private boolean isDevice(String deviceId) { + GbCode decode = GbCode.decode(deviceId); + if (decode == null) { + return true; + } + int code = Integer.parseInt(decode.getTypeCode()); + return code <= 199; + } + + // 订阅丢失检查 + @Scheduled(fixedDelay = 10, timeUnit = TimeUnit.SECONDS) + public void lostCheckForSubscribe(){ + // 获取所有设备 + List deviceList = redisCatchStorage.getAllDevices(); + if (deviceList.isEmpty()) { + return; + } + for (Device device : deviceList) { + if (device == null || !device.isOnLine() || !userSetting.getServerId().equals(device.getServerId())) { + continue; + } + if (device.getSubscribeCycleForCatalog() > 0 && !subscribeTaskRunner.containsKey(SubscribeTaskForCatalog.getKey(device))) { + log.debug("[订阅丢失] 目录订阅, 编号: {}, 重新发起订阅", device.getDeviceId()); + addCatalogSubscribe(device, null); + } + if (device.getSubscribeCycleForMobilePosition() > 0 && !subscribeTaskRunner.containsKey(SubscribeTaskForMobilPosition.getKey(device))) { + log.debug("[订阅丢失] 移动位置订阅, 编号: {}, 重新发起订阅", device.getDeviceId()); + addMobilePositionSubscribe(device, null); + } + } + } + + // 设备状态丢失检查 + @Scheduled(fixedDelay = 30, timeUnit = TimeUnit.SECONDS) + public void lostCheckForStatus(){ + // 获取所有设备 + List deviceList = redisCatchStorage.getAllDevices(); + if (deviceList.isEmpty()) { + return; + } + for (Device device : deviceList) { + if (device == null || !device.isOnLine() || !userSetting.getServerId().equals(device.getServerId())) { + continue; + } + if (!deviceStatusTaskRunner.containsKey(device.getDeviceId())) { + log.debug("[状态丢失] 执行设备离线, 编号: {},", device.getDeviceId()); + offline(device.getDeviceId(), ""); + } + } + } + + private void catalogSubscribeExpire(String deviceId, SipTransactionInfo transactionInfo) { + log.info("[目录订阅] 到期, 编号: {}", deviceId); + Device device = getDeviceByDeviceId(deviceId); + if (device == null) { + log.info("[目录订阅] 到期, 编号: {}, 设备不存在, 忽略", deviceId); + return; + } + if (device.isOnLine() && device.getSubscribeCycleForCatalog() > 0) { + addCatalogSubscribe(device, transactionInfo); + } + } + + private void mobilPositionSubscribeExpire(String deviceId, SipTransactionInfo transactionInfo) { + log.info("[移动位置订阅] 到期, 编号: {}", deviceId); + Device device = getDeviceByDeviceId(deviceId); + if (device == null) { + log.info("[移动位置订阅] 到期, 编号: {}, 设备不存在, 忽略", deviceId); + return; + } + if (device.isOnLine() && device.getSubscribeCycleForMobilePosition() > 0) { + addMobilePositionSubscribe(device, transactionInfo); + } + } + + @Override + public boolean addCatalogSubscribe(@NotNull Device device, SipTransactionInfo transactionInfo) { + if (device == null || device.getSubscribeCycleForCatalog() < 0) { + return false; + } + if (transactionInfo == null) { + log.info("[添加目录订阅] 设备 {}", device.getDeviceId()); + }else { + log.info("[目录订阅续期] 设备 {}", device.getDeviceId()); + } + try { + sipCommander.catalogSubscribe(device, transactionInfo, eventResult -> { + ResponseEvent event = (ResponseEvent) eventResult.event; + // 成功 + log.info("[目录订阅]成功: {}", device.getDeviceId()); + if (!subscribeTaskRunner.containsKey(SubscribeTaskForCatalog.getKey(device))) { + SIPResponse response = (SIPResponse) event.getResponse(); + SipTransactionInfo transactionInfoForResponse = new SipTransactionInfo(response); + SubscribeTask subscribeTask = SubscribeTaskForCatalog.getInstance(device, this::catalogSubscribeExpire, transactionInfoForResponse); + if (subscribeTask != null) { + subscribeTaskRunner.addSubscribe(subscribeTask); + } + }else { + subscribeTaskRunner.updateDelay(SubscribeTaskForCatalog.getKey(device), (device.getSubscribeCycleForCatalog() * 1000L - 500L) + System.currentTimeMillis()); + } + + },eventResult -> { + // 失败 + log.warn("[目录订阅]失败,信令发送失败: {}-{} ", device.getDeviceId(), eventResult.msg); + }); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 目录订阅: {}", e.getMessage()); + return false; + } + return true; + } + + @Override + public boolean removeCatalogSubscribe(@NotNull Device device, CommonCallback callback) { + String key = SubscribeTaskForCatalog.getKey(device); + if (subscribeTaskRunner.containsKey(key)) { + log.info("[移除目录订阅]: {}", device.getDeviceId()); + SipTransactionInfo transactionInfo = subscribeTaskRunner.getTransactionInfo(key); + if (transactionInfo == null) { + log.warn("[移除目录订阅] 未找到事务信息,{}", device.getDeviceId()); + } + try { + device.setSubscribeCycleForCatalog(0); + sipCommander.catalogSubscribe(device, transactionInfo, eventResult -> { + // 成功 + log.info("[取消目录订阅]成功: {}", device.getDeviceId()); + subscribeTaskRunner.removeSubscribe(SubscribeTaskForCatalog.getKey(device)); + if (callback != null) { + callback.run(true); + } + },eventResult -> { + // 失败 + log.warn("[取消目录订阅]失败,信令发送失败: {}-{} ", device.getDeviceId(), eventResult.msg); + }); + }catch (Exception e) { + // 失败 + log.warn("[取消目录订阅]失败: {}-{} ", device.getDeviceId(), e.getMessage()); + } + } + return true; + } + + @Override + public boolean addMobilePositionSubscribe(@NotNull Device device, SipTransactionInfo transactionInfo) { + if (transactionInfo == null) { + log.info("[添加移动位置订阅] 设备 {}", device.getDeviceId()); + }else { + log.info("[移动位置订阅续期] 设备 {}", device.getDeviceId()); + } + try { + sipCommander.mobilePositionSubscribe(device, transactionInfo, eventResult -> { + ResponseEvent event = (ResponseEvent) eventResult.event; + // 成功 + log.info("[移动位置订阅]成功: {}", device.getDeviceId()); + if (!subscribeTaskRunner.containsKey(SubscribeTaskForMobilPosition.getKey(device))) { + SIPResponse response = (SIPResponse) event.getResponse(); + SipTransactionInfo transactionInfoForResponse = new SipTransactionInfo(response); + SubscribeTask subscribeTask = SubscribeTaskForMobilPosition.getInstance(device, this::mobilPositionSubscribeExpire, transactionInfoForResponse); + if (subscribeTask != null) { + subscribeTaskRunner.addSubscribe(subscribeTask); + } + }else { + subscribeTaskRunner.updateDelay(SubscribeTaskForMobilPosition.getKey(device), (device.getSubscribeCycleForMobilePosition() * 1000L - 500L) + System.currentTimeMillis()); + } + + },eventResult -> { + // 失败 + log.warn("[移动位置订阅]失败,信令发送失败: {}-{} ", device.getDeviceId(), eventResult.msg); + }); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 移动位置订阅: {}", e.getMessage()); + return false; + } + return true; + } + + @Override + public boolean removeMobilePositionSubscribe(Device device, CommonCallback callback) { + + String key = SubscribeTaskForMobilPosition.getKey(device); + if (subscribeTaskRunner.containsKey(key)) { + log.info("[移除移动位置订阅]: {}", device.getDeviceId()); + SipTransactionInfo transactionInfo = subscribeTaskRunner.getTransactionInfo(key); + if (transactionInfo == null) { + log.warn("[移除移动位置订阅] 未找到事务信息,{}", device.getDeviceId()); + } + try { + device.setSubscribeCycleForMobilePosition(0); + sipCommander.mobilePositionSubscribe(device, transactionInfo, eventResult -> { + // 成功 + log.info("[取消移动位置订阅]成功: {}", device.getDeviceId()); + subscribeTaskRunner.removeSubscribe(SubscribeTaskForMobilPosition.getKey(device)); + if (callback != null) { + callback.run(true); + } + },eventResult -> { + // 失败 + log.warn("[取消移动位置订阅]失败,信令发送失败: {}-{} ", device.getDeviceId(), eventResult.msg); + }); + }catch (Exception e) { + // 失败 + log.warn("[取消移动位置订阅]失败: {}-{} ", device.getDeviceId(), e.getMessage()); + } + } + return true; + } + + @Override + public SyncStatus getChannelSyncStatus(String deviceId) { + Device device = deviceMapper.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR404.getCode(), "设备不存在"); + } + if (!userSetting.getServerId().equals(device.getServerId())) { + return redisRpcService.getChannelSyncStatus(device.getServerId(), deviceId); + } + return catalogResponseMessageHandler.getChannelSyncProgress(deviceId); + } + + @Override + public Boolean isSyncRunning(String deviceId) { + return catalogResponseMessageHandler.isSyncRunning(deviceId); + } + + @Override + public void sync(Device device) { + if (catalogResponseMessageHandler.isSyncRunning(device.getDeviceId())) { + SyncStatus syncStatus = catalogResponseMessageHandler.getChannelSyncProgress(device.getDeviceId()); + log.info("[同步通道] 同步已存在, 设备: {}, 同步信息: {}", device.getDeviceId(), JSON.toJSON(syncStatus)); + return; + } + int sn = (int)((Math.random()*9+1)*100000); + catalogResponseMessageHandler.setChannelSyncReady(device, sn); + try { + sipCommander.catalogQuery(device, sn, event -> { + String errorMsg = String.format("同步通道失败,错误码: %s, %s", event.statusCode, event.msg); + log.info("[同步通道]失败,编号: {}, 错误码: {}, {}", device.getDeviceId(), event.statusCode, event.msg); + catalogResponseMessageHandler.setChannelSyncEnd(device.getDeviceId(), sn, errorMsg); + }); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[同步通道], 信令发送失败:{}", e.getMessage() ); + String errorMsg = String.format("同步通道失败,信令发送失败: %s", e.getMessage()); + catalogResponseMessageHandler.setChannelSyncEnd(device.getDeviceId(), sn, errorMsg); + } + } + + @Override + public Device getDeviceByDeviceId(String deviceId) { + Device device = redisCatchStorage.getDevice(deviceId); + if (device == null) { + device = getDeviceByDeviceIdFromDb(deviceId); + if (device != null) { + redisCatchStorage.updateDevice(device); + } + } + return device; + } + + @Override + public List getAllOnlineDevice(String serverId) { + return deviceMapper.getOnlineDevicesByServerId(serverId); + } + + @Override + public List getAllByStatus(Boolean status) { + return deviceMapper.getDevices(ChannelDataType.GB28181, status); + } + + @Override + public boolean expire(Device device) { + Instant registerTimeDate = Instant.from(DateUtil.formatter.parse(device.getRegisterTime())); + Instant expireInstant = registerTimeDate.plusMillis(TimeUnit.SECONDS.toMillis(device.getExpires())); + return expireInstant.isBefore(Instant.now()); + } + + @Override + public Boolean getDeviceStatus(@NotNull Device device) { + SynchronousQueue queue = new SynchronousQueue<>(); + try { + sipCommander.deviceStatusQuery(device, ((code, msg, data) -> { + queue.offer(msg); + })); + String data = queue.poll(10, TimeUnit.SECONDS); + if (data != null && "ONLINE".equalsIgnoreCase(data.trim())) { + return Boolean.TRUE; + }else { + return Boolean.FALSE; + } + + } catch (InvalidArgumentException | SipException | ParseException | InterruptedException e) { + log.error("[命令发送失败] 设备状态查询: {}", e.getMessage()); + } + return null; + } + + @Override + public Device getDeviceByHostAndPort(String host, int port) { + return deviceMapper.getDeviceByHostAndPort(host, port); + } + + @Override + public void updateDevice(Device device) { + + device.setCharset(device.getCharset() == null ? "" : device.getCharset().toUpperCase()); + device.setUpdateTime(DateUtil.getNow()); + if (deviceMapper.update(device) > 0) { + redisCatchStorage.updateDevice(device); + } + } + + @Transactional + @Override + public void updateDeviceList(List deviceList) { + if (deviceList.isEmpty()){ + log.info("[批量更新设备] 列表为空,更细失败"); + return; + } + if (deviceList.size() == 1) { + updateDevice(deviceList.get(0)); + }else { + for (Device device : deviceList) { + device.setCharset(device.getCharset() == null ? "" : device.getCharset().toUpperCase()); + device.setUpdateTime(DateUtil.getNow()); + } + int limitCount = 300; + if (!deviceList.isEmpty()) { + if (deviceList.size() > limitCount) { + for (int i = 0; i < deviceList.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > deviceList.size()) { + toIndex = deviceList.size(); + } + deviceMapper.batchUpdate(deviceList.subList(i, toIndex)); + } + }else { + deviceMapper.batchUpdate(deviceList); + } + for (Device device : deviceList) { + redisCatchStorage.updateDevice(device); + } + } + } + } + + @Override + public boolean isExist(String deviceId) { + return getDeviceByDeviceIdFromDb(deviceId) != null; + } + + @Override + public void addCustomDevice(Device device) { + device.setOnLine(false); + device.setCreateTime(DateUtil.getNow()); + device.setUpdateTime(DateUtil.getNow()); + if(device.getStreamMode() == null) { + device.setStreamMode("TCP-PASSIVE"); + } + deviceMapper.addCustomDevice(device); + } + + @Override + public void updateCustomDevice(Device device) { + // 订阅状态的修改使用一个单独方法控制,此处不再进行状态修改 + Device deviceInStore = deviceMapper.query(device.getId()); + if (deviceInStore == null) { + log.warn("更新设备时未找到设备信息"); + return; + } + if (deviceInStore.getGeoCoordSys() != null) { + // 坐标系变化,需要重新计算GCJ02坐标和WGS84坐标 + if (!deviceInStore.getGeoCoordSys().equals(device.getGeoCoordSys())) { + deviceInStore.setGeoCoordSys(device.getGeoCoordSys()); + } + }else { + deviceInStore.setGeoCoordSys("WGS84"); + } + if (device.getCharset() == null) { + deviceInStore.setCharset("GB2312"); + } + + deviceMapper.updateCustom(device); + redisCatchStorage.updateDevice(device); + } + + @Override + @Transactional + public boolean delete(String deviceId) { + Device device = getDeviceByDeviceIdFromDb(deviceId); + Assert.notNull(device, "未找到设备"); + if (subscribeTaskRunner.containsKey(SubscribeTaskForCatalog.getKey(device))) { + removeCatalogSubscribe(device, null); + } + if (subscribeTaskRunner.containsKey(SubscribeTaskForMobilPosition.getKey(device))) { + removeMobilePositionSubscribe(device, null); + } + if (deviceStatusTaskRunner.containsKey(deviceId)) { + deviceStatusTaskRunner.removeTask(deviceId); + } + List commonGBChannels = commonGBChannelMapper.queryByDataTypeAndDeviceIds(1, List.of(device.getId())); + + try { + // 发送catalog + eventPublisher.channelEventPublish(commonGBChannels, ChannelEvent.ChannelEventMessageType.DEL); + } catch (Exception e) { + log.warn("[多个通道删除] 发送失败,数量:{}", commonGBChannels.size(), e); + } + + platformChannelMapper.delChannelForDeviceId(deviceId); + deviceChannelMapper.cleanChannelsByDeviceId(device.getId()); + deviceMapper.del(deviceId); + redisCatchStorage.removeDevice(deviceId); + inviteStreamService.clearInviteInfo(deviceId); + return true; + } + + @Override + public ResourceBaseInfo getOverview() { + List onlineDevices = deviceMapper.getOnlineDevices(); + List all = deviceMapper.getAll(); + return new ResourceBaseInfo(all.size(), onlineDevices.size()); + } + + @Override + public List getAll() { + return deviceMapper.getAll(); + } + + @Override + public PageInfo getAll(int page, int count, String query, Boolean status) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = deviceMapper.getDeviceList(ChannelDataType.GB28181, query, status); + return new PageInfo<>(all); + } + + @Override + public Device getDevice(Integer id) { + return deviceMapper.query(id); + } + + @Override + public Device getDeviceByChannelId(Integer channelId) { + return deviceMapper.queryByChannelId(ChannelDataType.GB28181,channelId); + } + + @Override + public Device getDeviceBySourceChannelDeviceId(String channelId) { + return deviceMapper.getDeviceBySourceChannelDeviceId(ChannelDataType.GB28181,channelId); + } + + @Override + public void subscribeCatalog(int id, int cycle) { + Device device = deviceMapper.query(id); + Assert.notNull(device, "未找到设备"); + Assert.isTrue(device.isOnLine(), "设备已离线"); + if (device.getSubscribeCycleForCatalog() == cycle) { + return; + } + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcService.subscribeCatalog(id, cycle); + return; + } + // 目录订阅相关的信息 + if (device.getSubscribeCycleForCatalog() > 0) { + // 订阅周期不同,则先取消 + removeCatalogSubscribe(device, result->{ + device.setSubscribeCycleForCatalog(cycle); + updateDevice(device); + if (cycle > 0) { + // 开启订阅 + addCatalogSubscribe(device, null); + } + }); + }else { + // 开启订阅 + device.setSubscribeCycleForCatalog(cycle); + updateDevice(device); + addCatalogSubscribe(device, null); + } + } + + @Override + public void subscribeMobilePosition(int id, int cycle, int interval) { + Device device = deviceMapper.query(id); + Assert.notNull(device, "未找到设备"); + if (!device.isOnLine()) { + // 开启订阅 + device.setSubscribeCycleForMobilePosition(cycle); + device.setMobilePositionSubmissionInterval(interval); + updateDevice(device); + if (subscribeTaskRunner.containsKey(SubscribeTaskForMobilPosition.getKey(device))) { + subscribeTaskRunner.removeSubscribe(SubscribeTaskForMobilPosition.getKey(device)); + } + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备已离线"); + } + + if (device.getSubscribeCycleForMobilePosition() == cycle) { + return; + } + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcService.subscribeMobilePosition(id, cycle, interval); + return; + } + // 目录订阅相关的信息 + if (device.getSubscribeCycleForMobilePosition() > 0) { + // 订阅周期已经开启,则先取消 + removeMobilePositionSubscribe(device, result->{ + // 开启订阅 + device.setSubscribeCycleForMobilePosition(cycle); + device.setMobilePositionSubmissionInterval(interval); + updateDevice(device); + if (cycle > 0) { + addMobilePositionSubscribe(device, null); + } + }); + }else { + // 订阅未开启 + device.setSubscribeCycleForMobilePosition(cycle); + device.setMobilePositionSubmissionInterval(interval); + updateDevice(device); + // 开启订阅 + addMobilePositionSubscribe(device, null); + } + } + + @Override + public void updateDeviceHeartInfo(Device device) { + Device deviceInDb = deviceMapper.query(device.getId()); + if (deviceInDb == null) { + return; + } + if (!Objects.equals(deviceInDb.getHeartBeatCount(), device.getHeartBeatCount()) + || !Objects.equals(deviceInDb.getHeartBeatInterval(), device.getHeartBeatInterval())) { + + deviceInDb.setHeartBeatCount(device.getHeartBeatCount()); + deviceInDb.setHeartBeatInterval(device.getHeartBeatInterval()); + deviceInDb.setPositionCapability(device.getPositionCapability()); + updateDevice(deviceInDb); + + long expiresTime = Math.min(device.getExpires(), device.getHeartBeatInterval() * device.getHeartBeatCount()) * 1000L; + if (deviceStatusTaskRunner.containsKey(device.getDeviceId())) { + deviceStatusTaskRunner.updateDelay(device.getDeviceId(), expiresTime + System.currentTimeMillis()); + } + } + } + + @Override + public WVPResult devicesSync(Device device) { + if (device.getServerId() != null && !userSetting.getServerId().equals(device.getServerId())) { + return redisRpcService.devicesSync(device.getServerId(), device.getDeviceId()); + } + // 已存在则返回进度 + if (isSyncRunning(device.getDeviceId())) { + SyncStatus channelSyncStatus = getChannelSyncStatus(device.getDeviceId()); + WVPResult wvpResult = new WVPResult(); + if (channelSyncStatus.getErrorMsg() != null) { + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg(channelSyncStatus.getErrorMsg()); + }else if (channelSyncStatus.getTotal() == null || channelSyncStatus.getTotal() == 0){ + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg("等待通道信息..."); + }else { + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); + wvpResult.setData(channelSyncStatus); + } + return wvpResult; + } + sync(device); + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(0); + wvpResult.setMsg("开始同步"); + return wvpResult; + } + + @Override + public void deviceBasicConfig(Device device, BasicParam basicParam, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.deviceBasicConfig(device.getServerId(), device, basicParam); + if (result.getCode() == ErrorCode.SUCCESS.getCode()) { + callback.run(result.getCode(), result.getMsg(), result.getData()); + } + return; + } + + try { + sipCommander.deviceBasicConfigCmd(device, basicParam, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 设备配置: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage()); + } + } + + @Override + public void deviceConfigQuery(Device device, String channelId, String configType, ErrorCallback callback) { + + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.deviceConfigQuery(device.getServerId(), device, channelId, configType); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + + try { + sipCommander.deviceConfigQuery(device, channelId, configType, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 获取设备配置: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage()); + } + } + + @Override + public void teleboot(Device device) { + + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcService.teleboot(device.getServerId(), device); + } + try { + sipCommander.teleBootCmd(device); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 远程启动: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void record(Device device, String channelId, String recordCmdStr, ErrorCallback callback) { + + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.recordControl(device.getServerId(), device, channelId, recordCmdStr); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + + try { + sipCommander.recordCmd(device, channelId, recordCmdStr, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 开始/停止录像: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage()); + } + } + + @Override + public void guard(Device device, String guardCmdStr, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.guard(device.getServerId(), device, guardCmdStr); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + + try { + sipCommander.guardCmd(device, guardCmdStr, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 布防/撤防操作: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage()); + } + } + + @Override + public void resetAlarm(Device device, String channelId, String alarmMethod, String alarmType, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.resetAlarm(device.getServerId(), device, channelId, alarmMethod, alarmType); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + try { + sipCommander.alarmResetCmd(device, alarmMethod, alarmType, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 布防/撤防操作: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage()); + } + + } + + @Override + public void iFrame(Device device, String channelId) { + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcService.iFrame(device.getServerId(), device, channelId); + return; + } + + try { + sipCommander.iFrameCmd(device, channelId); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 强制关键帧操作: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage()); + } + } + + @Override + public void homePosition(Device device, String channelId, Boolean enabled, Integer resetTime, Integer presetIndex, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.homePosition(device.getServerId(), device, channelId, enabled, resetTime, presetIndex); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + + try { + sipCommander.homePositionCmd(device, channelId, enabled, resetTime, presetIndex, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 看守位控制: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void dragZoomIn(Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcService.dragZoomIn(device.getServerId(), device, channelId, length, width, midpointx, midpointy, lengthx, lengthy); + return; + } + + StringBuffer cmdXml = new StringBuffer(200); + cmdXml.append("\r\n"); + cmdXml.append("" + length+ "\r\n"); + cmdXml.append("" + width+ "\r\n"); + cmdXml.append("" + midpointx+ "\r\n"); + cmdXml.append("" + midpointy+ "\r\n"); + cmdXml.append("" + lengthx+ "\r\n"); + cmdXml.append("" + lengthy+ "\r\n"); + cmdXml.append("\r\n"); + try { + sipCommander.dragZoomCmd(device, channelId, cmdXml.toString(), callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 拉框放大: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void dragZoomOut(Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcService.dragZoomOut(device.getServerId(), device, channelId, length, width, midpointx, midpointy, lengthx, lengthy); + return; + } + + StringBuffer cmdXml = new StringBuffer(200); + cmdXml.append("\r\n"); + cmdXml.append("" + length+ "\r\n"); + cmdXml.append("" + width+ "\r\n"); + cmdXml.append("" + midpointx+ "\r\n"); + cmdXml.append("" + midpointy+ "\r\n"); + cmdXml.append("" + lengthx+ "\r\n"); + cmdXml.append("" + lengthy+ "\r\n"); + cmdXml.append("\r\n"); + try { + sipCommander.dragZoomCmd(device, channelId, cmdXml.toString(), callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 拉框放大: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void deviceStatus(Device device, ErrorCallback callback) { + + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.deviceStatus(device.getServerId(), device); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + try { + sipCommander.deviceStatusQuery(device, (code, msg, data) -> { + if ("ONLINE".equalsIgnoreCase(data.trim())) { + online(device, null); + }else { + offline(device.getDeviceId(), "设备状态查询结果:" + data.trim()); + } + if (callback != null) { + callback.run(code, msg, data); + } + }); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 获取设备状态: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + + @Override + public void alarm(Device device, String startPriority, String endPriority, String alarmMethod, String alarmType, String startTime, String endTime, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.alarm(device.getServerId(), device, startPriority, endPriority, alarmMethod, alarmType, startTime, endTime); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + + String startAlarmTime = ""; + if (startTime != null) { + startAlarmTime = DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(startTime); + } + String endAlarmTime = ""; + if (startTime != null) { + endAlarmTime = DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(endTime); + } + + try { + sipCommander.alarmInfoQuery(device, startPriority, endPriority, alarmMethod, alarmType, startAlarmTime, endAlarmTime, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 获取设备状态: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void deviceInfo(Device device, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.deviceInfo(device.getServerId(), device); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + + try { + sipCommander.deviceInfoQuery(device, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 获取设备信息: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void queryPreset(Device device, String channelId, ErrorCallback> callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult> result = redisRpcService.queryPreset(device.getServerId(), device, channelId); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + + try { + sipCommander.presetQuery(device, channelId, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 预制位查询: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelControlServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelControlServiceImpl.java new file mode 100644 index 0000000..3409a84 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelControlServiceImpl.java @@ -0,0 +1,128 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelControlService; +import com.genersoft.iot.vmp.gb28181.service.ISourcePTZService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sip.message.Response; +import java.util.List; +import java.util.Map; + +@Service +@Slf4j +public class GbChannelControlServiceImpl implements IGbChannelControlService { + + + @Autowired + private Map sourcePTZServiceMap; + + + @Override + public void ptz(CommonGBChannel channel, FrontEndControlCodeForPTZ frontEndControlCode, ErrorCallback callback) { + log.info("[通用通道] 云台控制, 类型: {}, 编号:{}", channel.getDataType(), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType); + if (sourcePTZService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型: {} 不支持云台控制", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourcePTZService.ptz(channel, frontEndControlCode, callback); + } + + @Override + public void preset(CommonGBChannel channel, FrontEndControlCodeForPreset frontEndControlCode, ErrorCallback callback) { + log.info("[通用通道] 预置位控制, 类型: {}, 编号:{}", channel.getDataType(), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType); + if (sourcePTZService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型: {} 不支持预置位控制", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourcePTZService.preset(channel, frontEndControlCode, callback); + } + + @Override + public void fi(CommonGBChannel channel, FrontEndControlCodeForFI frontEndControlCode, ErrorCallback callback) { + log.info("[通用通道] FI指令, 类型: {}, 编号:{}", channel.getDataType(), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType); + if (sourcePTZService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型: {} 不支持FI指令", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourcePTZService.fi(channel, frontEndControlCode, callback); + } + + @Override + public void tour(CommonGBChannel channel, FrontEndControlCodeForTour frontEndControlCode, ErrorCallback callback) { + log.info("[通用通道] 巡航指令, 类型: {}, 编号:{}", channel.getDataType(), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType); + if (sourcePTZService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型: {} 不支持巡航指令", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourcePTZService.tour(channel, frontEndControlCode, callback); + } + + @Override + public void scan(CommonGBChannel channel, FrontEndControlCodeForScan frontEndControlCode, ErrorCallback callback) { + log.info("[通用通道] 扫描指令, 类型: {}, 编号:{}", channel.getDataType(), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType); + if (sourcePTZService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型: {} 不支持扫描指令", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourcePTZService.scan(channel, frontEndControlCode, callback); + } + + @Override + public void auxiliary(CommonGBChannel channel, FrontEndControlCodeForAuxiliary frontEndControlCode, ErrorCallback callback) { + log.info("[通用通道] 辅助开关控制指令, 类型: {}, 编号:{}", channel.getDataType(), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType); + if (sourcePTZService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型: {} 不支持辅助开关控制指令", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourcePTZService.auxiliary(channel, frontEndControlCode, callback); + } + + @Override + public void wiper(CommonGBChannel channel, FrontEndControlCodeForWiper frontEndControlCode, ErrorCallback callback) { + log.info("[通用通道] 雨刷控制, 类型: {}, 编号:{}", channel.getDataType(), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType); + if (sourcePTZService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型: {} 不支持雨刷控制", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourcePTZService.wiper(channel, frontEndControlCode, callback); + } + + @Override + public void queryPreset(CommonGBChannel channel, ErrorCallback> callback) { + log.info("[通用通道] 预置位查询, 类型: {}, 编号:{}", channel.getDataType(), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType); + if (sourcePTZService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型: {} 不支持预置位查询", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourcePTZService.queryPreset(channel, callback); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java new file mode 100644 index 0000000..5157198 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java @@ -0,0 +1,240 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService; +import com.genersoft.iot.vmp.gb28181.service.ISourceDownloadService; +import com.genersoft.iot.vmp.gb28181.service.ISourcePlayService; +import com.genersoft.iot.vmp.gb28181.service.ISourcePlaybackService; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078PlayService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sip.message.Response; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +public class GbChannelPlayServiceImpl implements IGbChannelPlayService { + + @Autowired + private UserSetting userSetting; + + @Autowired + private CommonGBChannelMapper channelMapper; + + @Autowired + private Map sourcePlayServiceMap; + + @Autowired + private Ijt1078PlayService jt1078PlayService; + + @Autowired + private Map sourcePlaybackServiceMap; + + @Autowired + private Map sourceDownloadServiceMap; + + + @Override + public void startInvite(CommonGBChannel channel, InviteMessageInfo inviteInfo, Platform platform, ErrorCallback callback) { + if (channel == null || inviteInfo == null || callback == null || channel.getDataType() == null) { + log.warn("[通用通道点播] 参数异常, channel: {}, inviteInfo: {}, callback: {}", channel != null, inviteInfo != null, callback != null); + throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + log.info("[点播通用通道] 类型:{}, 通道: {}({})", inviteInfo.getSessionName(), channel.getGbName(), channel.getGbDeviceId()); + + if ("Play".equalsIgnoreCase(inviteInfo.getSessionName())) { + play(channel, platform, userSetting.getRecordSip(), callback); + }else if ("Playback".equals(inviteInfo.getSessionName())) { + playback(channel, inviteInfo.getStartTime(), inviteInfo.getStopTime(), callback); + }else if ("Download".equals(inviteInfo.getSessionName())) { + Integer downloadSpeed = Integer.parseInt(inviteInfo.getDownloadSpeed()); + // 国标通道 + download(channel, inviteInfo.getStartTime(), inviteInfo.getStopTime(), downloadSpeed, callback); + }else { + // 不支持的点播方式 + log.error("[点播通用通道] 不支持的点播方式:{}, {}({})", inviteInfo.getSessionName(), channel.getGbName(), channel.getGbDeviceId()); + throw new PlayException(Response.BAD_REQUEST, "bad request"); + } + } + + @Override + public void stopInvite(InviteSessionType type, CommonGBChannel channel, String stream) { + switch (type) { + case PLAY: + stopPlay(channel); + break; + case PLAYBACK: + stopPlayback(channel, stream); + break; + case DOWNLOAD: + stopDownload(channel, stream); + break; + default: + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持此类型请求", type); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + } + + + + @Override + public void play(CommonGBChannel channel, Platform platform, Boolean record, ErrorCallback callback) { + log.info("[通用通道] 播放, 类型: {}, 编号:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePlayService sourceChannelPlayService = sourcePlayServiceMap.get(ChannelDataType.PLAY_SERVICE + dataType); + if (sourceChannelPlayService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持实时流预览", ChannelDataType.getDateTypeDesc(channel.getDataType())); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourceChannelPlayService.play(channel, platform, record, (code, msg, data) -> { + if (code == InviteErrorCode.SUCCESS.getCode()) { + // 将流ID记录到数据库 + if (channel.getDataType() != ChannelDataType.GB28181) { + channelMapper.updateStream(channel.getGbId(), data.getStream()); + } + } + callback.run(code, msg, data); + }); + } + @Override + public void playback(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback callback) { + log.info("[通用通道] 回放, 类型: {}, 编号:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePlaybackService playbackService = sourcePlaybackServiceMap.get(ChannelDataType.PLAYBACK_SERVICE + dataType); + if (playbackService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持回放", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + playbackService.playback(channel, startTime, stopTime, callback); + } + + @Override + public void download(CommonGBChannel channel, Long startTime, Long stopTime, Integer downloadSpeed, + ErrorCallback callback){ + log.info("[通用通道] 录像下载, 类型: {}, 编号:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourceDownloadService downloadService = sourceDownloadServiceMap.get(ChannelDataType.DOWNLOAD_SERVICE + dataType); + if (downloadService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持录像下载", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + downloadService.download(channel, startTime, stopTime, downloadSpeed, callback); + } + + @Override + public void stopPlay(CommonGBChannel channel) { + Integer dataType = channel.getDataType(); + ISourcePlayService sourceChannelPlayService = sourcePlayServiceMap.get(ChannelDataType.PLAY_SERVICE + dataType); + if (sourceChannelPlayService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持停止实时流", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourceChannelPlayService.stopPlay(channel); + } + + @Override + public void stopPlayback(CommonGBChannel channel, String stream) { + log.info("[通用通道] 停止回放, 类型: {}, 编号:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePlaybackService playbackService = sourcePlaybackServiceMap.get(ChannelDataType.PLAYBACK_SERVICE + dataType); + if (playbackService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持回放", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + playbackService.stopPlayback(channel, stream); + } + + @Override + public void stopDownload(CommonGBChannel channel, String stream) { + log.info("[通用通道] 停止录像下载, 类型: {}, 编号:{} stream: {}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId(), stream); + Integer dataType = channel.getDataType(); + ISourceDownloadService downloadService = sourceDownloadServiceMap.get(ChannelDataType.DOWNLOAD_SERVICE + dataType); + if (downloadService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持录像下载", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + downloadService.stopDownload(channel, stream); + } + + @Override + public void playbackPause(CommonGBChannel channel, String stream) { + log.info("[通用通道] 回放暂停, 类型: {}, 编号:{} stream:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId(), stream); + Integer dataType = channel.getDataType(); + ISourcePlaybackService playbackService = sourcePlaybackServiceMap.get(ChannelDataType.PLAYBACK_SERVICE + dataType); + if (playbackService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持回放暂停", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + playbackService.playbackPause(channel, stream); + } + + @Override + public void playbackResume(CommonGBChannel channel, String stream) { + log.info("[通用通道] 回放暂停恢复, 类型: {}, 编号:{} stream:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId(), stream); + Integer dataType = channel.getDataType(); + ISourcePlaybackService playbackService = sourcePlaybackServiceMap.get(ChannelDataType.PLAYBACK_SERVICE + dataType); + if (playbackService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持回放暂停恢复", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + playbackService.playbackResume(channel, stream); + } + + @Override + public void playbackSeek(CommonGBChannel channel, String stream, long seekTime) { + log.info("[通用通道] 回放拖动播放, 类型: {}, 编号:{} stream:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId(), stream); + Integer dataType = channel.getDataType(); + ISourcePlaybackService playbackService = sourcePlaybackServiceMap.get(ChannelDataType.PLAYBACK_SERVICE + dataType); + if (playbackService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持回放暂停恢复", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + playbackService.playbackSeek(channel, stream, seekTime); + } + + @Override + public void playbackSpeed(CommonGBChannel channel, String stream, Double speed) { + log.info("[通用通道] 回放倍速播放, 类型: {}, 编号:{} stream:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId(), stream); + Integer dataType = channel.getDataType(); + ISourcePlaybackService playbackService = sourcePlaybackServiceMap.get(ChannelDataType.PLAYBACK_SERVICE + dataType); + if (playbackService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持回放暂停恢复", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + playbackService.playbackSpeed(channel, stream, speed); + } + + @Override + public void queryRecord(CommonGBChannel channel, String startTime, String endTime, ErrorCallback> callback) { + log.info("[通用通道] 录像查询, 类型: {}, 编号:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePlaybackService playbackService = sourcePlaybackServiceMap.get(ChannelDataType.PLAYBACK_SERVICE + dataType); + if (playbackService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持回放暂停恢复", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + playbackService.queryRecord(channel, startTime, endTime, callback); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java new file mode 100644 index 0000000..984e293 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java @@ -0,0 +1,1193 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.alibaba.excel.support.cglib.beans.BeanMap; +import com.alibaba.excel.util.BeanMapUtils; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.controller.bean.Extent; +import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.GroupMapper; +import com.genersoft.iot.vmp.gb28181.dao.PlatformChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.RegionMapper; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.channel.ChannelEvent; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.utils.VectorTileCatch; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.utils.Coordtransform; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.TileUtils; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.google.common.base.CaseFormat; +import lombok.extern.slf4j.Slf4j; +import no.ecc.vectortile.VectorTileEncoder; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +@Slf4j +@Service +public class GbChannelServiceImpl implements IGbChannelService, CommandLineRunner { + + @Autowired + private EventPublisher eventPublisher; + + @Autowired + private CommonGBChannelMapper commonGBChannelMapper; + + @Autowired + private PlatformChannelMapper platformChannelMapper; + + @Autowired + private IPlatformChannelService platformChannelService; + + @Autowired + private RegionMapper regionMapper; + + @Autowired + private GroupMapper groupMapper; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private VectorTileCatch vectorTileCatch; + + private final GeometryFactory geometryFactory = new GeometryFactory(); + + + @Override + public void run(String... args) throws Exception { + // 启动时重新发布抽稀图层 + List channelList = commonGBChannelMapper.queryAllWithPosition(); + Map> zoomCameraMap = new ConcurrentHashMap<>(); + + channelList.stream().forEach(commonGBChannel -> { + if (commonGBChannel.getMapLevel() == null) { + return; + } + List channelListForZoom = zoomCameraMap.computeIfAbsent(commonGBChannel.getMapLevel(), k -> new ArrayList<>()); + channelListForZoom.add(commonGBChannel); + }); + + String id = "DEFAULT"; + List beforeData = new ArrayList<>(); + for (Integer zoom : zoomCameraMap.keySet()) { + beforeData.addAll(zoomCameraMap.get(zoom)); + log.info("[抽稀-发布mvt矢量瓦片] ID:{},当前层级: {}, ", id, zoom); + // 按照 z/x/y 数据组织数据, 矢量数据暂时保存在内存中 + // 按照范围生成 x y范围, + saveTile(id, zoom, "WGS84", beforeData); + saveTile(id, zoom, "GCJ02", beforeData); + } + } + + @Override + public CommonGBChannel queryByDeviceId(String gbDeviceId) { + List commonGBChannels = commonGBChannelMapper.queryByDeviceId(gbDeviceId); + if (commonGBChannels.isEmpty()) { + return null; + }else { + return commonGBChannels.get(0); + } + } + + @Override + public int add(CommonGBChannel commonGBChannel) { + if (commonGBChannel.getDataType() == null || commonGBChannel.getDataDeviceId() == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "缺少通道数据类型或通道数据关联设备ID"); + } + CommonGBChannel commonGBChannelInDb = commonGBChannelMapper.queryByDataId(commonGBChannel.getDataType(), commonGBChannel.getDataDeviceId()); + Assert.isNull(commonGBChannelInDb, "此推流已经关联通道"); + + // 检验国标编号是否重复 + List channelList = commonGBChannelMapper.queryByDeviceId(commonGBChannel.getGbDeviceId()); + Assert.isTrue(channelList.isEmpty(), "国标编号已经存在"); + + commonGBChannel.setCreateTime(DateUtil.getNow()); + commonGBChannel.setUpdateTime(DateUtil.getNow()); + int result = commonGBChannelMapper.insert(commonGBChannel); + try { + // 发送通知 + eventPublisher.channelEventPublish(commonGBChannel, ChannelEvent.ChannelEventMessageType.ADD); + } catch (Exception e) { + log.warn("[通道移除通知] 发送失败,{}", commonGBChannel.getGbDeviceId(), e); + } + return result; + } + + @Override + @Transactional + public int delete(int gbId) { + // 移除国标级联关联的信息 + try { + platformChannelService.removeChannel(gbId); + }catch (Exception e) { + log.error("[移除通道国标级联共享失败]", e); + } + + CommonGBChannel channel = commonGBChannelMapper.queryById(gbId); + if (channel != null) { + commonGBChannelMapper.delete(gbId); + try { + // 发送通知 + eventPublisher.channelEventPublish(channel, ChannelEvent.ChannelEventMessageType.DEL); + } catch (Exception e) { + log.warn("[通道移除通知] 发送失败,{}", channel.getGbDeviceId(), e); + } + } + return 1; + } + + @Override + @Transactional + public void delete(Collection ids) { + // 移除国标级联关联的信息 + try { + platformChannelService.removeChannels(new ArrayList<>(ids)); + }catch (Exception e) { + log.error("[移除通道国标级联共享失败]", e); + } + List channelListInDb = commonGBChannelMapper.queryByIds(ids); + if (channelListInDb.isEmpty()) { + return; + } + commonGBChannelMapper.batchDelete(channelListInDb); + try { + // 发送通知 + eventPublisher.channelEventPublish(channelListInDb, ChannelEvent.ChannelEventMessageType.DEL); + } catch (Exception e) { + log.warn("[通道移除通知] 发送失败", e); + } + } + + @Override + public int update(CommonGBChannel commonGBChannel) { + log.info("[更新通道] 通道ID: {}, ", commonGBChannel.getGbId()); + if (commonGBChannel.getGbId() <= 0) { + log.warn("[更新通道] 未找到数据库ID,更新失败, {}({})", commonGBChannel.getGbName(), commonGBChannel.getGbDeviceId()); + return 0; + } + // 确定编号是否重复 + List channels = commonGBChannelMapper.queryByDeviceId(commonGBChannel.getGbDeviceId()); + if (channels.size() > 1) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "国标编号重复,请修改编号后保存"); + } + CommonGBChannel oldChannel = commonGBChannelMapper.queryById(commonGBChannel.getGbId()); + commonGBChannel.setUpdateTime(DateUtil.getNow()); + int result = commonGBChannelMapper.update(commonGBChannel); + + if (result > 0) { + try { + CommonGBChannel newChannel = commonGBChannelMapper.queryById(commonGBChannel.getGbId()); + // 发送通知 + eventPublisher.channelEventPublishForUpdate(newChannel, oldChannel); + + if (newChannel.getGbLongitude() != null && !Objects.equals(oldChannel.getGbLongitude(), newChannel.getGbLongitude()) + && newChannel.getGbLatitude() != null && !Objects.equals(oldChannel.getGbLatitude(), newChannel.getGbLatitude())) { + MobilePosition mobilePosition = new MobilePosition(); + mobilePosition.setDeviceId(newChannel.getGbDeviceId()); + mobilePosition.setChannelId(newChannel.getGbId()); + mobilePosition.setChannelDeviceId(newChannel.getGbDeviceId()); + mobilePosition.setDeviceName(newChannel.getGbName()); + mobilePosition.setCreateTime(DateUtil.getNow()); + mobilePosition.setTime(DateUtil.getNow()); + mobilePosition.setLongitude(newChannel.getGbLongitude()); + mobilePosition.setLatitude(newChannel.getGbLatitude()); + eventPublisher.mobilePositionEventPublish(mobilePosition); + } + + } catch (Exception e) { + log.warn("[更新通道通知] 发送失败,{}", JSONObject.toJSONString(commonGBChannel), e); + } + } + return result; + } + + @Override + public int offline(CommonGBChannel commonGBChannel) { + if (commonGBChannel.getGbId() <= 0) { + log.warn("[通道离线] 未找到数据库ID,更新失败, {}({})", commonGBChannel.getGbName(), commonGBChannel.getGbDeviceId()); + return 0; + } + int result = commonGBChannelMapper.updateStatusById(commonGBChannel.getGbId(), "OFF"); + if (result > 0) { + try { + // 发送通知 + eventPublisher.channelEventPublish(commonGBChannel, ChannelEvent.ChannelEventMessageType.OFF); + } catch (Exception e) { + log.warn("[通道离线通知] 发送失败,{}", commonGBChannel.getGbDeviceId(), e); + } + } + return result; + } + + @Override + @Transactional + public int offline(List commonGBChannelList) { + if (commonGBChannelList.isEmpty()) { + log.warn("[多个通道离线] 通道数量为0,更新失败"); + return 0; + } + log.info("[通道离线] 共 {} 个", commonGBChannelList.size()); + int limitCount = 1000; + int result = 0; + if (commonGBChannelList.size() > limitCount) { + for (int i = 0; i < commonGBChannelList.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > commonGBChannelList.size()) { + toIndex = commonGBChannelList.size(); + } + result += commonGBChannelMapper.updateStatusForListById(commonGBChannelList.subList(i, toIndex), "OFF"); + } + } else { + result += commonGBChannelMapper.updateStatusForListById(commonGBChannelList, "OFF"); + } + if (result > 0) { + try { + // 发送catalog + eventPublisher.channelEventPublish(commonGBChannelList, ChannelEvent.ChannelEventMessageType.OFF); + } catch (Exception e) { + log.warn("[多个通道离线] 发送失败,数量:{}", commonGBChannelList.size(), e); + } + } + return result; + } + + @Override + public int online(CommonGBChannel commonGBChannel) { + if (commonGBChannel.getGbId() <= 0) { + log.warn("[通道上线] 未找到数据库ID,更新失败, {}({})", commonGBChannel.getGbName(), commonGBChannel.getGbDeviceId()); + return 0; + } + int result = commonGBChannelMapper.updateStatusById(commonGBChannel.getGbId(), "ON"); + if (result > 0) { + try { + // 发送通知 + eventPublisher.channelEventPublish(commonGBChannel, ChannelEvent.ChannelEventMessageType.ON); + } catch (Exception e) { + log.warn("[通道上线通知] 发送失败,{}", commonGBChannel.getGbDeviceId(), e); + } + } + return 0; + } + + @Override + @Transactional + public int online(List commonGBChannelList) { + if (commonGBChannelList.isEmpty()) { + log.warn("[多个通道上线] 通道数量为0,更新失败"); + return 0; + } + // 批量更新 + int limitCount = 1000; + int result = 0; + if (commonGBChannelList.size() > limitCount) { + for (int i = 0; i < commonGBChannelList.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > commonGBChannelList.size()) { + toIndex = commonGBChannelList.size(); + } + result += commonGBChannelMapper.updateStatusForListById(commonGBChannelList.subList(i, toIndex), "ON"); + } + } else { + result += commonGBChannelMapper.updateStatusForListById(commonGBChannelList, "ON"); + } + try { + // 发送catalog + eventPublisher.channelEventPublish(commonGBChannelList, ChannelEvent.ChannelEventMessageType.ON); + } catch (Exception e) { + log.warn("[多个通道上线] 发送失败,数量:{}", commonGBChannelList.size(), e); + } + + return result; + } + + @Override + @Transactional + public void batchAdd(List commonGBChannels) { + if (commonGBChannels.isEmpty()) { + log.warn("[新增多个通道] 通道数量为0,更新失败"); + return; + } + // 批量保存 + int limitCount = 1000; + int result = 0; + if (commonGBChannels.size() > limitCount) { + for (int i = 0; i < commonGBChannels.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > commonGBChannels.size()) { + toIndex = commonGBChannels.size(); + } + result += commonGBChannelMapper.batchAdd(commonGBChannels.subList(i, toIndex)); + } + } else { + result += commonGBChannelMapper.batchAdd(commonGBChannels); + } + try { + // 发送catalog + eventPublisher.channelEventPublish(commonGBChannels, ChannelEvent.ChannelEventMessageType.ADD); + } catch (Exception e) { + log.warn("[多个通道新增] 发送失败,数量:{}", commonGBChannels.size(), e); + } + log.warn("[新增多个通道] 通道数量为{},成功保存:{}", commonGBChannels.size(), result); + } + + @Override + public void batchUpdate(List commonGBChannels) { + if (commonGBChannels.isEmpty()) { + log.warn("[更新多个通道] 通道数量为0,更新失败"); + return; + } + List oldCommonGBChannelList = commonGBChannelMapper.queryOldChanelListByChannels(commonGBChannels); + // 批量保存 + int limitCount = 1000; + int result = 0; + if (commonGBChannels.size() > limitCount) { + for (int i = 0; i < commonGBChannels.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > commonGBChannels.size()) { + toIndex = commonGBChannels.size(); + } + result += commonGBChannelMapper.batchUpdate(commonGBChannels.subList(i, toIndex)); + } + } else { + result += commonGBChannelMapper.batchUpdate(commonGBChannels); + } + log.info("[更新多个通道] 通道数量为{},成功保存:{}", commonGBChannels.size(), result); + // 发送通过更新通知 + try { + // 发送通知 + eventPublisher.channelEventPublishForUpdate(commonGBChannels, oldCommonGBChannelList); + } catch (Exception e) { + log.warn("[更新多个通道] 发送失败,{}个", commonGBChannels.size(), e); + } + } + + @Override + @Transactional + public void updateStatus(List commonGBChannels) { + if (commonGBChannels.isEmpty()) { + log.warn("[更新多个通道状态] 通道数量为0,更新失败"); + return; + } + List oldChanelListByChannels = commonGBChannelMapper.queryOldChanelListByChannels(commonGBChannels); + int limitCount = 1000; + int result = 0; + if (commonGBChannels.size() > limitCount) { + for (int i = 0; i < commonGBChannels.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > commonGBChannels.size()) { + toIndex = commonGBChannels.size(); + } + result += commonGBChannelMapper.updateStatus(commonGBChannels.subList(i, toIndex)); + } + } else { + result += commonGBChannelMapper.updateStatus(commonGBChannels); + } + log.warn("[更新多个通道状态] 通道数量为{},成功保存:{}", commonGBChannels.size(), result); + // 发送通过更新通知 + try { + // 发送通知 + eventPublisher.channelEventPublishForUpdate(commonGBChannels, oldChanelListByChannels); + } catch (Exception e) { + log.warn("[更新多个通道] 发送失败,{}个", commonGBChannels.size(), e); + } + } + + + + @Override + public CommonGBChannel getOne(int id) { + return commonGBChannelMapper.queryById(id); + } + + @Override + public List getIndustryCodeList() { + IndustryCodeTypeEnum[] values = IndustryCodeTypeEnum.values(); + List result = new ArrayList<>(values.length); + for (IndustryCodeTypeEnum value : values) { + result.add(IndustryCodeType.getInstance(value)); + } + Collections.sort(result); + return result; + } + + @Override + public List getDeviceTypeList() { + DeviceTypeEnum[] values = DeviceTypeEnum.values(); + List result = new ArrayList<>(values.length); + for (DeviceTypeEnum value : values) { + result.add(DeviceType.getInstance(value)); + } + Collections.sort(result); + return result; + } + + @Override + public List getNetworkIdentificationTypeList() { + NetworkIdentificationTypeEnum[] values = NetworkIdentificationTypeEnum.values(); + List result = new ArrayList<>(values.length); + for (NetworkIdentificationTypeEnum value : values) { + result.add(NetworkIdentificationType.getInstance(value)); + } + Collections.sort(result); + return result; + } + + @Override + public void reset(int id, List chanelFields) { + log.info("[重置国标通道] id: {}", id); + Assert.notEmpty(chanelFields, "待重置字段为空"); + CommonGBChannel channel = getOne(id); + if (channel == null) { + log.warn("[重置国标通道] 未找到对应Id的通道: id: {}", id); + throw new ControllerException(ErrorCode.ERROR400); + } + if (channel.getDataType() != ChannelDataType.GB28181) { + log.warn("[重置国标通道] 非国标下级通道无法重置: id: {}", id); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "非国标下级通道无法重置"); + } + List dbFields = new ArrayList<>(); + + for (String chanelField : chanelFields) { + BeanWrapperImpl wrapper = new BeanWrapperImpl(channel); + if (wrapper.isReadableProperty(chanelField)) { + dbFields.add(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, chanelField)); + } + } + Assert.notEmpty(dbFields, "待重置字段为空"); + + // 这个多加一个参数,为了防止将非国标的通道通过此方法清空内容,导致意外发生 + commonGBChannelMapper.reset(id, dbFields, DateUtil.getNow()); + CommonGBChannel channelNew = getOne(id); + // 发送通过更新通知 + try { + // 发送通知 + eventPublisher.channelEventPublishForUpdate(channelNew, channel); + } catch (Exception e) { + log.warn("[通道移除通知] 发送失败,{}", channelNew.getGbDeviceId(), e); + } + } + + @Override + public PageInfo queryListByCivilCode(int page, int count, String query, Boolean online, Integer channelType, String civilCode) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = commonGBChannelMapper.queryListByCivilCode(query, online, channelType, civilCode); + return new PageInfo<>(all); + } + + @Override + public PageInfo queryListByParentId(int page, int count, String query, Boolean online, Integer channelType, String groupDeviceId) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = commonGBChannelMapper.queryListByParentId(query, online, channelType, groupDeviceId); + return new PageInfo<>(all); + } + + @Override + public void removeCivilCode(List allChildren) { + commonGBChannelMapper.removeCivilCode(allChildren); + // TODO 是否需要通知上级, 或者等添加新的行政区划时发送更新通知 + + } + + @Override + public void addChannelToRegion(String civilCode, List channelIds) { + List channelList = commonGBChannelMapper.queryByIds(channelIds); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + List channelListForOld = new ArrayList<>(channelList); + for (CommonGBChannel channel : channelList) { + channel.setGbCivilCode(civilCode); + } + int result = commonGBChannelMapper.updateRegion(civilCode, channelList); + // 发送通知 + if (result > 0) { + platformChannelService.checkRegionAdd(channelList); + try { + // 发送catalog + eventPublisher.channelEventPublishForUpdate(channelList, channelListForOld); + } catch (Exception e) { + log.warn("[多个通道添加行政区划] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + @Transactional + public void deleteChannelToRegion(String civilCode, List channelIds) { + if (!ObjectUtils.isEmpty(civilCode)) { + deleteChannelToRegionByCivilCode(civilCode); + } + if (!ObjectUtils.isEmpty(channelIds)) { + deleteChannelToRegionByChannelIds(channelIds); + } + } + + @Override + public void deleteChannelToRegionByCivilCode(String civilCode) { + List channelList = commonGBChannelMapper.queryByCivilCode(civilCode); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + int result = commonGBChannelMapper.removeCivilCodeByChannels(channelList); + Region region = regionMapper.queryByDeviceId(civilCode); + if (region == null) { + platformChannelService.checkRegionRemove(channelList, null); + }else { + List regionList = new ArrayList<>(); + regionList.add(region); + platformChannelService.checkRegionRemove(channelList, regionList); + } + // TODO 发送通知 +// if (result > 0) { +// try { +// // 发送catalog +// eventPublisher.catalogEventPublish(null, channelList, CatalogEvent.UPDATE); +// }catch (Exception e) { +// log.warn("[多个通道添加行政区划] 发送失败,数量:{}", channelList.size(), e); +// } +// } + } + + @Override + public void deleteChannelToRegionByChannelIds(List channelIds) { + List channelList = commonGBChannelMapper.queryByIds(channelIds); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + int result = commonGBChannelMapper.removeCivilCodeByChannels(channelList); + + platformChannelService.checkRegionRemove(channelList, null); + // TODO 发送通知 +// if (result > 0) { +// try { +// // 发送catalog +// eventPublisher.catalogEventPublish(null, channelList, CatalogEvent.UPDATE); +// }catch (Exception e) { +// log.warn("[多个通道添加行政区划] 发送失败,数量:{}", channelList.size(), e); +// } +// } + } + + @Override + public void addChannelToRegionByGbDevice(String civilCode, List deviceIds) { + List channelList = commonGBChannelMapper.queryByDataTypeAndDeviceIds(ChannelDataType.GB28181, deviceIds); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + List channelListForOld = new ArrayList<>(channelList); + for (CommonGBChannel channel : channelList) { + channel.setGbCivilCode(civilCode); + } + int result = commonGBChannelMapper.updateRegion(civilCode, channelList); + // 发送通知 + if (result > 0) { + try { + // 发送catalog + eventPublisher.channelEventPublishForUpdate(channelList, channelListForOld); + } catch (Exception e) { + log.warn("[多个通道添加行政区划] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + public void deleteChannelToRegionByGbDevice(List deviceIds) { + List channelList = commonGBChannelMapper.queryByDataTypeAndDeviceIds(ChannelDataType.GB28181, deviceIds); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + int result = commonGBChannelMapper.removeCivilCodeByChannels(channelList); + platformChannelService.checkRegionRemove(channelList, null); + } + + @Override + @Transactional + public void removeParentIdByBusinessGroup(String businessGroup) { + List channelList = commonGBChannelMapper.queryByBusinessGroup(businessGroup); + if (channelList.isEmpty()) { + return; + } + int result = commonGBChannelMapper.removeParentIdByChannels(channelList); + List groupList = groupMapper.queryByBusinessGroup(businessGroup); + platformChannelService.checkGroupRemove(channelList, groupList); + + } + + @Override + public void removeParentIdByGroupList(List groupList) { + List channelList = commonGBChannelMapper.queryByGroupList(groupList); + if (channelList.isEmpty()) { + return; + } + commonGBChannelMapper.removeParentIdByChannels(channelList); + platformChannelService.checkGroupRemove(channelList, groupList); + } + + @Override + public void updateBusinessGroup(String oldBusinessGroup, String newBusinessGroup) { + List channelList = commonGBChannelMapper.queryByBusinessGroup(oldBusinessGroup); + if (channelList.isEmpty()) { + log.info("[更新业务分组] 发现未关联任何通道: {}", oldBusinessGroup); + return; + } + List channelListForOld = new ArrayList<>(channelList); + int result = commonGBChannelMapper.updateBusinessGroupByChannelList(newBusinessGroup, channelList); + if (result > 0) { + for (CommonGBChannel channel : channelList) { + channel.setGbBusinessGroupId(newBusinessGroup); + } + // 发送catalog + try { + eventPublisher.channelEventPublishForUpdate(channelList, channelListForOld); + } catch (Exception e) { + log.warn("[多个通道业务分组] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + public void updateParentIdGroup(String oldParentId, String newParentId) { + List channelList = commonGBChannelMapper.queryByParentId(oldParentId); + if (channelList.isEmpty()) { + return; + } + List channelListForOld = new ArrayList<>(channelList); + int result = commonGBChannelMapper.updateParentIdByChannelList(newParentId, channelList); + if (result > 0) { + for (CommonGBChannel channel : channelList) { + channel.setGbParentId(newParentId); + } + // 发送catalog + try { + eventPublisher.channelEventPublishForUpdate(channelList, channelListForOld); + } catch (Exception e) { + log.warn("[多个通道业务分组] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + @Transactional + public void addChannelToGroup(String parentId, String businessGroup, List channelIds) { + List channelList = commonGBChannelMapper.queryByIds(channelIds); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + List channelListForOld = new ArrayList<>(channelList); + int result = commonGBChannelMapper.updateGroup(parentId, businessGroup, channelList); + for (CommonGBChannel commonGBChannel : channelList) { + commonGBChannel.setGbParentId(parentId); + commonGBChannel.setGbBusinessGroupId(businessGroup); + } + + // 发送通知 + if (result > 0) { + platformChannelService.checkGroupAdd(channelList); + try { + // 发送catalog + eventPublisher.channelEventPublishForUpdate(channelList, channelListForOld); + } catch (Exception e) { + log.warn("[多个通道添加行政区划] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + public void deleteChannelToGroup(String parentId, String businessGroup, List channelIds) { + List channelList = commonGBChannelMapper.queryByIds(channelIds); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + commonGBChannelMapper.removeParentIdByChannels(channelList); + + Group group = groupMapper.queryOneByDeviceId(parentId, businessGroup); + if (group == null) { + platformChannelService.checkGroupRemove(channelList, null); + }else { + List groupList = new ArrayList<>(); + groupList.add(group); + platformChannelService.checkGroupRemove(channelList, groupList); + } + } + + @Override + @Transactional + public void addChannelToGroupByGbDevice(String parentId, String businessGroup, List deviceIds) { + List channelList = commonGBChannelMapper.queryByDataTypeAndDeviceIds(ChannelDataType.GB28181, deviceIds); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + List channelListForOld = new ArrayList<>(channelList); + + for (CommonGBChannel channel : channelList) { + channel.setGbParentId(parentId); + channel.setGbBusinessGroupId(businessGroup); + } + int result = commonGBChannelMapper.updateGroup(parentId, businessGroup, channelList); + + for (CommonGBChannel commonGBChannel : channelList) { + commonGBChannel.setGbParentId(parentId); + commonGBChannel.setGbBusinessGroupId(businessGroup); + } + // 发送通知 + if (result > 0) { + platformChannelService.checkGroupAdd(channelList); + try { + // 发送catalog + eventPublisher.channelEventPublishForUpdate(channelList, channelListForOld); + } catch (Exception e) { + log.warn("[多个通道添加行政区划] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + public void deleteChannelToGroupByGbDevice(List deviceIds) { + List channelList = commonGBChannelMapper.queryByDataTypeAndDeviceIds(ChannelDataType.GB28181, deviceIds); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + commonGBChannelMapper.removeParentIdByChannels(channelList); + platformChannelService.checkGroupRemove(channelList, null); + } + + @Override + public CommonGBChannel queryOneWithPlatform(Integer platformId, String channelDeviceId) { + // 防止共享的通道编号重复 + List channelList = platformChannelMapper.queryOneWithPlatform(platformId, channelDeviceId); + if (!channelList.isEmpty()) { + return channelList.get(channelList.size() - 1); + }else { + return null; + } + } + + @Override + public void updateCivilCode(String oldCivilCode, String newCivilCode) { + List channelList = commonGBChannelMapper.queryByCivilCode(oldCivilCode); + if (channelList.isEmpty()) { + return; + } + List channelListForOld = new ArrayList<>(channelList); + int result = commonGBChannelMapper.updateCivilCodeByChannelList(newCivilCode, channelList); + if (result > 0) { + for (CommonGBChannel channel : channelList) { + channel.setGbCivilCode(newCivilCode); + } + // 发送catalog + try { + eventPublisher.channelEventPublishForUpdate(channelList, channelListForOld); + } catch (Exception e) { + log.warn("[多个通道业务分组] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + public List queryListByStreamPushList(List streamPushList) { + return commonGBChannelMapper.queryListByStreamPushList(ChannelDataType.STREAM_PUSH, streamPushList); + } + + @Override + public PageInfo queryList(int page, int count, String query, Boolean online, Boolean hasRecordPlan, + Integer channelType, String civilCode, String parentDeviceId) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = commonGBChannelMapper.queryList(query, online, hasRecordPlan, channelType, civilCode, parentDeviceId); + return new PageInfo<>(all); + } + + @Override + public PageInfo queryListByCivilCodeForUnusual(int page, int count, String query, Boolean online, Integer channelType) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = commonGBChannelMapper.queryListByCivilCodeForUnusual(query, online, channelType); + return new PageInfo<>(all); + } + + @Override + public void clearChannelCivilCode(Boolean all, List channelIds) { + + List channelIdsForClear; + if (all != null && all) { + channelIdsForClear = commonGBChannelMapper.queryAllForUnusualCivilCode(); + }else { + channelIdsForClear = channelIds; + } + commonGBChannelMapper.removeCivilCodeByChannelIds(channelIdsForClear); + } + + @Override + public PageInfo queryListByParentForUnusual(int page, int count, String query, Boolean online, Integer channelType) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = commonGBChannelMapper.queryListByParentForUnusual(query, online, channelType); + return new PageInfo<>(all); + } + + @Override + public void clearChannelParent(Boolean all, List channelIds) { + List channelIdsForClear; + if (all != null && all) { + channelIdsForClear = commonGBChannelMapper.queryAllForUnusualParent(); + }else { + channelIdsForClear = channelIds; + } + commonGBChannelMapper.removeParentIdByChannelIds(channelIdsForClear); + } + + @Override + public void updateGPSFromGPSMsgInfo(List gpsMsgInfoList) { + if (gpsMsgInfoList == null || gpsMsgInfoList.isEmpty()) { + return; + } + // 此处来源默认为WGS84, 所以直接入库 + commonGBChannelMapper.updateGpsByDeviceId(gpsMsgInfoList); +// +// Map gpsMsgInfoMap = new ConcurrentReferenceHashMap<>(); +// for (GPSMsgInfo gpsMsgInfo : gpsMsgInfoList) { +// gpsMsgInfoMap.put(gpsMsgInfo.getId(), gpsMsgInfo); +// } +// +// List channelList = commonGBChannelMapper.queryByGbDeviceIds(new ArrayList<>(gpsMsgInfoMap.keySet())); +// if (channelList.isEmpty()) { +// return; +// } +// channelList.forEach(commonGBChannel -> { +// MobilePosition mobilePosition = new MobilePosition(); +// mobilePosition.setDeviceId(commonGBChannel.getGbDeviceId()); +// mobilePosition.setChannelId(commonGBChannel.getGbId()); +// mobilePosition.setDeviceName(commonGBChannel.getGbName()); +// mobilePosition.setCreateTime(DateUtil.getNow()); +// mobilePosition.setTime(DateUtil.getNow()); +// mobilePosition.setLongitude(commonGBChannel.getGbLongitude()); +// mobilePosition.setLatitude(commonGBChannel.getGbLatitude()); +// eventPublisher.mobilePositionEventPublish(mobilePosition); +// }); + } + + @Transactional + @Override + public void updateGPS(List commonGBChannels) { + int limitCount = 1000; + if (commonGBChannels.size() > limitCount) { + for (int i = 0; i < commonGBChannels.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > commonGBChannels.size()) { + toIndex = commonGBChannels.size(); + } + commonGBChannelMapper.updateGps(commonGBChannels.subList(i, toIndex)); + } + } else { + commonGBChannelMapper.updateGps(commonGBChannels); + } + } + + @Override + public List queryListForMap(String query, Boolean online, Boolean hasRecordPlan, Integer channelType) { + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + return commonGBChannelMapper.queryList(query, online, hasRecordPlan, channelType, null, null); + } + + @Override + public CommonGBChannel queryCommonChannelByDeviceChannel(DeviceChannel channel) { + return commonGBChannelMapper.queryCommonChannelByDeviceChannel(channel.getDataType(), channel.getDataDeviceId(), channel.getDeviceId()); + } + + @Override + public void resetLevel() { + commonGBChannelMapper.resetLevel(); + } + + @Override + public byte[] getTile(int z, int x, int y, String geoCoordSys) { + double minLon = TileUtils.tile2lon(x, z); + double maxLon = TileUtils.tile2lon(x + 1, z); + double maxLat = TileUtils.tile2lat(y, z); + double minLat = TileUtils.tile2lat(y + 1, z); + + if (geoCoordSys != null) { + if (geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] minPosition = Coordtransform.GCJ02ToWGS84(minLon, minLat); + minLon = minPosition[0]; + minLat = minPosition[1]; + + Double[] maxPosition = Coordtransform.GCJ02ToWGS84(maxLon, maxLat); + maxLon = maxPosition[0]; + maxLat = maxPosition[1]; + } + } + // 从数据库查询对应的数据 + List channelList = commonGBChannelMapper.queryCameraChannelInBox(minLon, maxLon, minLat, maxLat); + VectorTileEncoder encoder = new VectorTileEncoder(); + if (!channelList.isEmpty()) { + channelList.forEach(commonGBChannel -> { + double lon = commonGBChannel.getGbLongitude(); + double lat = commonGBChannel.getGbLatitude(); + // 转换为目标坐标系 + if (geoCoordSys != null) { + if (geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] minPosition = Coordtransform.WGS84ToGCJ02(lon, lat); + lon = minPosition[0]; + lat = minPosition[1]; + } + } + + // 将 lon/lat 转为瓦片内像素坐标(0..256) + double[] px = TileUtils.lonLatToTilePixel(lon, lat, z, x, y); + Point pointGeom = geometryFactory.createPoint(new Coordinate(px[0], px[1])); + + BeanMap beanMap = BeanMapUtils.create(commonGBChannel); + encoder.addFeature("points", beanMap, pointGeom); + }); + } + return encoder.encode(); + } + + + @Override + public String drawThin(Map zoomParam, Extent extent, String geoCoordSys) { + long time = System.currentTimeMillis(); + + String id = UUID.randomUUID().toString(); + List channelListInExtent; + if (extent == null) { + log.info("[抽稀] ID: {}, 未设置范围,从数据库读取摄像头的范围", id); + extent = commonGBChannelMapper.queryExtent(); + channelListInExtent = commonGBChannelMapper.queryAllWithPosition(); + }else { + if (geoCoordSys != null && geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] maxPosition = Coordtransform.GCJ02ToWGS84(extent.getMaxLng(), extent.getMaxLat()); + Double[] minPosition = Coordtransform.GCJ02ToWGS84(extent.getMinLng(), extent.getMinLat()); + + extent.setMaxLng(maxPosition[0]); + extent.setMaxLat(maxPosition[1]); + + extent.setMinLng(minPosition[0]); + extent.setMinLat(minPosition[1]); + } + // 获取数据源 + channelListInExtent = commonGBChannelMapper.queryListInExtent(extent.getMinLng(), extent.getMaxLng(), extent.getMinLat(), extent.getMaxLat()); + } + Assert.isTrue(!channelListInExtent.isEmpty(), "通道数据为空"); + + log.info("[开始抽稀] ID: {}, 范围,[{}, {}, {}, {}]", id, extent.getMinLng(), extent.getMinLat(), extent.getMaxLng(), extent.getMaxLat()); + + Extent finalExtent = extent; + // 记录进度 + saveProcess(id, 0, "开始抽稀"); + dynamicTask.startDelay(id, () -> { + try { + // 存储每层的抽稀结果, key为层级(zoom),value为摄像头数组 + Map> zoomCameraMap = new HashMap<>(); + + // 冗余一份已经处理过的摄像头的数据, 避免多次循环获取 + Map useCameraMap = new HashMap<>(); + AtomicReference process = new AtomicReference<>((double) 0); + for (Integer zoom : zoomParam.keySet()) { + + Map useCameraMapForZoom = new HashMap<>(); + Map cameraMapForZoom = new HashMap<>(); + + if (Objects.equals(zoom, Collections.max(zoomParam.keySet()))) { + // 最大层级不进行抽稀, 将未进行抽稀的数据直接存储到这个层级 + for (CommonGBChannel channel : channelListInExtent) { + if (!useCameraMap.containsKey(channel.getGbId())) { + channel.setMapLevel(zoom); + // 这个的key跟后面的不一致是因为无需抽稀, 直接存储原始数据 + cameraMapForZoom.put(channel.getGbId() + "", channel); + useCameraMap.put(channel.getGbId(), channel); + } + } + }else { + Double diff = zoomParam.get(zoom); + // 对这个层级展开抽稀 + log.info("[抽稀] ID:{},当前层级: {}, 坐标间隔: {}", id, zoom, diff); + + // 更新上级图层的数据到当前层级,确保当前层级展示时考虑到之前层级的数据 + for (CommonGBChannel channel : useCameraMap.values()) { + int lngGrid = (int)(channel.getGbLongitude() / diff); + int latGrid = (int)(channel.getGbLatitude() / diff); + String gridKey = latGrid + ":" + lngGrid; + useCameraMapForZoom.put(gridKey, channel); + } + + // 对数据开始执行抽稀 + for (CommonGBChannel channel : channelListInExtent) { + // 已经分配再其他层级的,本层级不再使用 + if (useCameraMap.containsKey(channel.getGbId())) { + continue; + } + int lngGrid = (int)(channel.getGbLongitude() / diff); + int latGrid = (int)(channel.getGbLatitude() / diff); + // 数据网格Id + String gridKey = latGrid + ":" + lngGrid; + if (useCameraMapForZoom.containsKey(gridKey)) { + continue; + } + if (cameraMapForZoom.containsKey(gridKey)) { + CommonGBChannel oldChannel = cameraMapForZoom.get(gridKey); + // 如果一个网格存在多个数据,则选择最接近中心点的, 目前只选择了经度方向作为参考 + if (channel.getGbLongitude() % diff < oldChannel.getGbLongitude() % diff) { + channel.setMapLevel(zoom); + cameraMapForZoom.put(gridKey, channel); + useCameraMap.put(channel.getGbId(), channel); + useCameraMap.remove(oldChannel.getGbId()); + oldChannel.setMapLevel(null); + } + }else { + channel.setMapLevel(zoom); + cameraMapForZoom.put(gridKey, channel); + useCameraMap.put(channel.getGbId(), channel); + } + } + } + // 存储 + zoomCameraMap.put(zoom, cameraMapForZoom.values()); + process.updateAndGet(v -> (v + 0.5 / zoomParam.size())); + saveProcess(id, process.get(), "抽稀图层: " + zoom); + } + + // 抽稀完成, 对数据发布mvt矢量瓦片 + List beforeData = new ArrayList<>(); + for (Integer zoom : zoomCameraMap.keySet()) { + beforeData.addAll(zoomCameraMap.get(zoom)); + log.info("[抽稀-发布mvt矢量瓦片] ID:{},当前层级: {}", id, zoom); + // 按照 z/x/y 数据组织数据, 矢量数据暂时保存在内存中 + // 按照范围生成 x y范围, + saveTile(id, zoom, "WGS84", beforeData); + saveTile(id, zoom, "GCJ02", beforeData); + process.updateAndGet(v -> (v + 0.5 / zoomParam.size())); + saveProcess(id, process.get(), "发布矢量瓦片: " + zoom); + } + // 记录原始数据,未保存做准备 + vectorTileCatch.addSource(id, new ArrayList<>(useCameraMap.values())); + + log.info("[抽稀完成] ID:{}, 耗时: {}ms", id, (System.currentTimeMillis() - time)); + saveProcess(id, 1, "抽稀完成"); + } catch (Exception e) { + log.info("[抽稀] 失败 ID:{}", id, e); + } + + }, 1); + + return id; + } + + private void saveTile(String id, int z, String geoCoordSys, Collection commonGBChannelList ) { + Map encoderMap = new HashMap<>(); + commonGBChannelList.forEach(commonGBChannel -> { + double lon = commonGBChannel.getGbLongitude(); + double lat = commonGBChannel.getGbLatitude(); + if (geoCoordSys != null && geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] minPosition = Coordtransform.WGS84ToGCJ02(lon, lat); + lon = minPosition[0]; + lat = minPosition[1]; + } + double[] doubles = TileUtils.lonLatToTileXY(lon, lat, z); + int x = (int) doubles[0]; + int y = (int) doubles[1]; + String key = z + "_" + x + "_" + y + "_" + geoCoordSys; + VectorTileEncoder encoder =encoderMap.get(key); + if (encoder == null) { + encoder = new VectorTileEncoder(); + encoderMap.put(key, encoder); + } + // 将 lon/lat 转为瓦片内像素坐标(0..256) + double[] px = TileUtils.lonLatToTilePixel(lon, lat, z, x, y); + Point pointGeom = geometryFactory.createPoint(new Coordinate(px[0], px[1])); + BeanMap beanMap = BeanMapUtils.create(commonGBChannel); + encoder.addFeature("points", beanMap, pointGeom); + }); + encoderMap.forEach((key, encoder) -> { + vectorTileCatch.addVectorTile(id, key, encoder.encode()); + }); + } + + private void saveProcess(String id, double process, String msg) { + String key = VideoManagerConstants.DRAW_THIN_PROCESS_PREFIX + id; + Duration duration = Duration.ofMinutes(30); + redisTemplate.opsForValue().set(key, new DrawThinProcess(process, msg), duration); + } + + @Override + public DrawThinProcess thinProgress(String id) { + String key = VideoManagerConstants.DRAW_THIN_PROCESS_PREFIX + id; + return (DrawThinProcess) redisTemplate.opsForValue().get(key); + } + + @Override + @Transactional + public void saveThin(String id) { + commonGBChannelMapper.resetLevel(); + List channelList = vectorTileCatch.getChannelList(id); + if (channelList != null && !channelList.isEmpty()) { + int limitCount = 1000; + if (channelList.size() > limitCount) { + for (int i = 0; i < channelList.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > channelList.size()) { + toIndex = channelList.size(); + } + commonGBChannelMapper.saveLevel(channelList.subList(i, toIndex)); + } + } else { + commonGBChannelMapper.saveLevel(channelList); + } + } + vectorTileCatch.save(id); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GroupServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GroupServiceImpl.java new file mode 100644 index 0000000..fbbc1da --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GroupServiceImpl.java @@ -0,0 +1,334 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.GroupMapper; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.service.IGroupService; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import java.util.*; + +/** + * 区域管理类 + */ +@Service +@Slf4j +public class GroupServiceImpl implements IGroupService, CommandLineRunner { + + @Autowired + private GroupMapper groupManager; + + @Autowired + private CommonGBChannelMapper commonGBChannelMapper; + + @Autowired + private IGbChannelService gbChannelService; + + @Autowired + private EventPublisher eventPublisher; + + @Autowired + private RedisTemplate redisTemplate; + + // 启动后请求组织结构同步 + @Override + public void run(String... args) throws Exception { + String key = VideoManagerConstants.VM_MSG_GROUP_LIST_REQUEST; + log.info("[redis发送通知] 发送 同步组织结构请求 {}", key); + redisTemplate.convertAndSend(key, ""); + } + + @Override + public void add(Group group) { + Assert.notNull(group, "参数不可为NULL"); + Assert.notNull(group.getDeviceId(), "分组编号不可为NULL"); + Assert.isTrue(group.getDeviceId().trim().length() == 20, "分组编号必须为20位"); + Assert.notNull(group.getName(), "分组名称不可为NULL"); + + GbCode gbCode = GbCode.decode(group.getDeviceId()); + Assert.notNull(gbCode, "分组编号不满足国标定义"); + + // 查询数据库中已经存在的. + List groupListInDb = groupManager.queryInGroupListByDeviceId(Lists.newArrayList(group)); + if (!ObjectUtils.isEmpty(groupListInDb)){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), String.format("该节点编号 %s 已存在", group.getDeviceId())); + } + + if ("215".equals(gbCode.getTypeCode())){ + // 添加业务分组 + addBusinessGroup(group); + }else { + Assert.isTrue("216".equals(gbCode.getTypeCode()), "创建虚拟组织时设备编号11-13位应使用216"); + // 添加虚拟组织 + addGroup(group); + } + } + + private void addGroup(Group group) { + // 建立虚拟组织 + Assert.notNull(group.getBusinessGroup(), "所属的业务分组分组不存在"); + Group businessGroup = groupManager.queryBusinessGroup(group.getBusinessGroup()); + Assert.notNull(businessGroup, "所属的业务分组分组不存在"); + if (!ObjectUtils.isEmpty(group.getParentDeviceId())) { + Group parentGroup = groupManager.queryOneByDeviceId(group.getParentDeviceId(), group.getBusinessGroup()); + Assert.notNull(parentGroup, "所属的上级分组分组不存在"); + }else { + group.setParentDeviceId(null); + } + group.setCreateTime(DateUtil.getNow()); + group.setUpdateTime(DateUtil.getNow()); + groupManager.add(group); + } + + private void addBusinessGroup(Group group) { + group.setBusinessGroup(group.getDeviceId()); + group.setCreateTime(DateUtil.getNow()); + group.setUpdateTime(DateUtil.getNow()); + groupManager.addBusinessGroup(group); + } + + @Override + public List queryAllChildren(Integer id) { + List children = groupManager.getChildren(id); + if (ObjectUtils.isEmpty(children)) { + return children; + } + for (int i = 0; i < children.size(); i++) { + children.addAll(queryAllChildren(children.get(i).getId())); + } + return children; + } + + @Override + @Transactional + public void update(Group group) { + Assert.isTrue(group.getId()> 0, "更新必须携带分组ID"); + Assert.notNull(group.getDeviceId(), "编号不可为NULL"); + Assert.notNull(group.getBusinessGroup(), "业务分组不可为NULL"); + Group groupInDb = groupManager.queryOne(group.getId()); + Assert.notNull(groupInDb, "分组不存在"); + + // 查询数据库中已经存在的. + List groupListInDb = groupManager.queryInGroupListByDeviceId(Lists.newArrayList(group)); + if (!ObjectUtils.isEmpty(groupListInDb) && groupListInDb.get(0).getId() != group.getId()){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), String.format("该该节点编号 %s 已存在", group.getDeviceId())); + } + + group.setName(group.getName()); + group.setUpdateTime(DateUtil.getNow()); + groupManager.update(group); + // 修改他的子节点 + if (!group.getDeviceId().equals(groupInDb.getDeviceId()) + || !group.getBusinessGroup().equals(groupInDb.getBusinessGroup())) { + List groupList = queryAllChildren(groupInDb.getId()); + if (!groupList.isEmpty()) { + int result = groupManager.updateChild(groupInDb.getId(), group); + if (result > 0) { + for (Group chjildGroup : groupList) { + chjildGroup.setParentDeviceId(group.getDeviceId()); + chjildGroup.setBusinessGroup(group.getBusinessGroup()); + // 将变化信息发送通知 + CommonGBChannel channel = CommonGBChannel.build(chjildGroup); + try { + // 发送catalog + eventPublisher.channelEventPublishForUpdate(channel, null); + }catch (Exception e) { + log.warn("[业务分组/虚拟组织变化] 发送失败,{}", group.getDeviceId(), e); + } + } + } + } + } + // 将变化信息发送通知 + CommonGBChannel channel = CommonGBChannel.build(group); + try { + // 发送catalog + eventPublisher.channelEventPublishForUpdate(channel, null); + }catch (Exception e) { + log.warn("[业务分组/虚拟组织变化] 发送失败,{}", group.getDeviceId(), e); + } + + // 由于编号变化,会需要处理太多内容以及可能发送大量消息,所以目前更新只只支持重命名 + GbCode decode = GbCode.decode(group.getDeviceId()); + if (!groupInDb.getDeviceId().equals(group.getDeviceId())) { + if (decode.getTypeCode().equals("215")) { + // 业务分组变化。需要将其下的所有业务分组修改 + gbChannelService.updateBusinessGroup(groupInDb.getDeviceId(), group.getDeviceId()); + }else { + // 虚拟组织修改,需要把其下的子节点修改父节点ID + gbChannelService.updateParentIdGroup(groupInDb.getDeviceId(), group.getDeviceId()); + } + } + } + + @Override + public Group queryGroupByDeviceId(String regionDeviceId) { + return groupManager.queryOneByOnlyDeviceId(regionDeviceId); + } + + @Override + public List queryForTree(String query, Integer parentId, Boolean hasChannel) { + + List groupTrees = groupManager.queryForTree(query, parentId); + if (parentId == null) { + return groupTrees; + } + // 查询含有的通道 + Group parentGroup = groupManager.queryOne(parentId); + if (parentGroup != null && hasChannel != null && hasChannel) { + List groupTreesForChannel = commonGBChannelMapper.queryForGroupTreeByParentId(query, parentGroup.getDeviceId()); + if (!ObjectUtils.isEmpty(groupTreesForChannel)) { + groupTrees.addAll(groupTreesForChannel); + } + } + return groupTrees; + } + + @Override + @Transactional + public boolean delete(int id) { + Group group = groupManager.queryOne(id); + Assert.notNull(group, "分组不存在"); + List groupListForDelete = new ArrayList<>(); + GbCode gbCode = GbCode.decode(group.getDeviceId()); + if (gbCode.getTypeCode().equals("215")) { + List groupList = groupManager.queryByBusinessGroup(group.getDeviceId()); + if (!groupList.isEmpty()) { + groupListForDelete.addAll(groupList); + } + // 业务分组 + gbChannelService.removeParentIdByBusinessGroup(group.getDeviceId()); + }else { + List groupList = queryAllChildren(group.getId()); + if (!groupList.isEmpty()) { + groupListForDelete.addAll(groupList); + } + groupListForDelete.add(group); + gbChannelService.removeParentIdByGroupList(groupListForDelete); + } + groupManager.batchDelete(groupListForDelete); + + for (Group groupForDelete : groupListForDelete) { + // 删除平台关联的分组信息。同时发送通知 + List platformList = groupManager.queryForPlatformByGroupId(groupForDelete.getId()); + if ( !platformList.isEmpty()) { + groupManager.deletePlatformGroup(groupForDelete.getId()); + // 将变化信息发送通知 + CommonGBChannel channel = CommonGBChannel.build(groupForDelete); + for (Platform platform : platformList) { + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, channel, CatalogEvent.DEL); + }catch (Exception e) { + log.warn("[业务分组/虚拟组织删除] 发送失败,{}", groupForDelete.getDeviceId(), e); + } + } + } + } + return true; + } + + @Override + @Transactional + public boolean batchAdd(List groupList) { + if (groupList== null || groupList.isEmpty()) { + return false; + } + Map groupMapForVerification = new HashMap<>(); + for (Group group : groupList) { + groupMapForVerification.put(group.getDeviceId(), group); + } + // 查询数据库中已经存在的. + List groupListInDb = groupManager.queryInGroupListByDeviceId(groupList); + if (!groupListInDb.isEmpty()) { + for (Group group : groupListInDb) { + groupMapForVerification.remove(group.getDeviceId()); + } + } + if (!groupMapForVerification.isEmpty()) { + List groupListForAdd = new ArrayList<>(groupMapForVerification.values()); + groupManager.batchAdd(groupListForAdd); + // 更新分组关系 + groupManager.updateParentId(groupListForAdd); + groupManager.updateParentIdWithBusinessGroup(groupListForAdd); + } + + return true; + } + + @Override + public List getPath(String deviceId, String businessGroup) { + Group businessGroupInDb = groupManager.queryBusinessGroup(businessGroup); + if (businessGroupInDb == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "业务分组不存在"); + } + List groupList = new LinkedList<>(); + groupList.add(businessGroupInDb); + Group group = groupManager.queryOneByDeviceId(deviceId, businessGroup); + if (group == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "虚拟组织不存在"); + } + List allParent = getAllParent(group); + groupList.addAll(allParent); + groupList.add(group); + return groupList; + } + + private List getAllParent(Group group) { + if (group.getParentId() == null || group.getBusinessGroup() == null) { + return new ArrayList<>(); + } + + List groupList = new ArrayList<>(); + Group parent = groupManager.queryOneByDeviceId(group.getParentDeviceId(), group.getBusinessGroup()); + if (parent == null) { + return groupList; + } + List allParent = getAllParent(parent); + allParent.add(parent); + return allParent; + } + + @Override + public PageInfo queryList(Integer page, Integer count, String query) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = groupManager.query(query, null, null); + return new PageInfo<>(all); + } + + @Override + public Group queryGroupByAlias(String groupAlias) { + return groupManager.queryGroupByAlias(groupAlias); + } + + @Override + public void sync() { + try { + this.run(); + }catch (Exception e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "同步失败: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/InviteStreamServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/InviteStreamServiceImpl.java new file mode 100755 index 0000000..c3c7680 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/InviteStreamServiceImpl.java @@ -0,0 +1,354 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.*; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.dao.DeviceChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.DeviceMapper; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +@Slf4j +@Service +public class InviteStreamServiceImpl implements IInviteStreamService { + + private final Map>> inviteErrorCallbackMap = new ConcurrentHashMap<>(); + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private UserSetting userSetting; + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private DeviceChannelMapper deviceChannelMapper; + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + if ("rtsp".equals(event.getSchema()) && "rtp".equals(event.getApp())) { + InviteInfo inviteInfo = getInviteInfoByStream(null, event.getStream()); + if (inviteInfo != null && (inviteInfo.getType() == InviteSessionType.PLAY || inviteInfo.getType() == InviteSessionType.PLAYBACK)) { + removeInviteInfo(inviteInfo); + Device device = deviceMapper.getDeviceByDeviceId(inviteInfo.getDeviceId()); + if (device != null) { + deviceChannelMapper.stopPlayById(inviteInfo.getChannelId()); + } + } + } + } + + @Override + public void updateInviteInfo(InviteInfo inviteInfo) { + if (InviteSessionStatus.ready == inviteInfo.getStatus()) { + updateInviteInfo(inviteInfo, Long.valueOf(userSetting.getPlayTimeout()) * 2); + } else { + updateInviteInfo(inviteInfo, null); + } + } + + @Override + public void updateInviteInfo(InviteInfo inviteInfo, Long time) { + if (inviteInfo == null || (inviteInfo.getDeviceId() == null || inviteInfo.getChannelId() == null)) { + log.warn("[更新Invite信息],参数不全: {}", JSON.toJSON(inviteInfo)); + return; + } + InviteInfo inviteInfoForUpdate; + + if (InviteSessionStatus.ready == inviteInfo.getStatus()) { + if (inviteInfo.getDeviceId() == null || inviteInfo.getChannelId() == null + || inviteInfo.getType() == null || inviteInfo.getStream() == null + ) { + return; + } + inviteInfoForUpdate = inviteInfo; + } else { + InviteInfo inviteInfoInRedis = getInviteInfo(inviteInfo.getType(), inviteInfo.getChannelId(), inviteInfo.getStream()); + if (inviteInfoInRedis == null) { + log.warn("[更新Invite信息],未从缓存中读取到Invite信息: deviceId: {}, channel: {}, stream: {}", + inviteInfo.getDeviceId(), inviteInfo.getChannelId(), inviteInfo.getStream()); + return; + } + if (inviteInfo.getStreamInfo() != null) { + inviteInfoInRedis.setStreamInfo(inviteInfo.getStreamInfo()); + } + if (inviteInfo.getSsrcInfo() != null) { + inviteInfoInRedis.setSsrcInfo(inviteInfo.getSsrcInfo()); + } + if (inviteInfo.getStreamMode() != null) { + inviteInfoInRedis.setStreamMode(inviteInfo.getStreamMode()); + } + if (inviteInfo.getReceiveIp() != null) { + inviteInfoInRedis.setReceiveIp(inviteInfo.getReceiveIp()); + } + if (inviteInfo.getReceivePort() != null) { + inviteInfoInRedis.setReceivePort(inviteInfo.getReceivePort()); + } + if (inviteInfo.getStatus() != null) { + inviteInfoInRedis.setStatus(inviteInfo.getStatus()); + } + + inviteInfoForUpdate = inviteInfoInRedis; + + } + if (inviteInfoForUpdate.getCreateTime() == null) { + inviteInfoForUpdate.setCreateTime(System.currentTimeMillis()); + } + String key = VideoManagerConstants.INVITE_PREFIX; + String objectKey = inviteInfoForUpdate.getType() + + ":" + inviteInfoForUpdate.getChannelId() + + ":" + inviteInfoForUpdate.getStream(); + if (time != null && time > 0) { + inviteInfoForUpdate.setExpirationTime(time); + } + redisTemplate.opsForHash().put(key, objectKey, inviteInfoForUpdate); + } + + @Override + public InviteInfo updateInviteInfoForStream(InviteInfo inviteInfo, String stream) { + + InviteInfo inviteInfoInDb = getInviteInfo(inviteInfo.getType(), inviteInfo.getChannelId(), inviteInfo.getStream()); + if (inviteInfoInDb == null) { + return null; + } + removeInviteInfo(inviteInfoInDb); + String key = VideoManagerConstants.INVITE_PREFIX; + String objectKey = inviteInfo.getType() + + ":" + inviteInfo.getChannelId() + + ":" + stream; + inviteInfoInDb.setStream(stream); + if (inviteInfoInDb.getSsrcInfo() != null) { + inviteInfoInDb.getSsrcInfo().setStream(stream); + } + if (InviteSessionStatus.ready == inviteInfo.getStatus()) { + inviteInfoInDb.setExpirationTime((long) (userSetting.getPlayTimeout() * 2)); + } + if (inviteInfoInDb.getCreateTime() == null) { + inviteInfoInDb.setCreateTime(System.currentTimeMillis()); + } + redisTemplate.opsForHash().put(key, objectKey, inviteInfoInDb); + return inviteInfoInDb; + } + + @Override + public InviteInfo getInviteInfo(InviteSessionType type, Integer channelId, String stream) { + String key = VideoManagerConstants.INVITE_PREFIX; + String keyPattern = (type != null ? type : "*") + + ":" + (channelId != null ? channelId : "*") + + ":" + (stream != null ? stream : "*"); + ScanOptions options = ScanOptions.scanOptions().match(keyPattern).count(20).build(); + try (Cursor> cursor = redisTemplate.opsForHash().scan(key, options)) { + if (cursor.hasNext()) { + InviteInfo inviteInfo = (InviteInfo) cursor.next().getValue(); + cursor.close(); + return inviteInfo; + + } + } catch (Exception e) { + log.error("[Redis-InviteInfo] 查询异常: ", e); + } + return null; + } + + @Override + public List getAllInviteInfo() { + List result = new ArrayList<>(); + String key = VideoManagerConstants.INVITE_PREFIX; + List values = redisTemplate.opsForHash().values(key); + if(values.isEmpty()) { + return result; + } + for (Object value : values) { + result.add((InviteInfo)value); + } + return result; + } + + @Override + public InviteInfo getInviteInfoByDeviceAndChannel(InviteSessionType type, Integer channelId) { + return getInviteInfo(type, channelId, null); + } + + @Override + public InviteInfo getInviteInfoByStream(InviteSessionType type, String stream) { + return getInviteInfo(type, null, stream); + } + + @Override + public void removeInviteInfo(InviteSessionType type, Integer channelId, String stream) { + String key = VideoManagerConstants.INVITE_PREFIX; + if (type == null && channelId == null && stream == null) { + redisTemplate.opsForHash().delete(key); + return; + } + InviteInfo inviteInfo = getInviteInfo(type, channelId, stream); + if (inviteInfo != null) { + String objectKey = inviteInfo.getType() + + ":" + inviteInfo.getChannelId() + + ":" + inviteInfo.getStream(); + redisTemplate.opsForHash().delete(key, objectKey); + } + } + + @Override + public void removeInviteInfoByDeviceAndChannel(InviteSessionType inviteSessionType, Integer channelId) { + removeInviteInfo(inviteSessionType, channelId, null); + } + + @Override + public void removeInviteInfo(InviteInfo inviteInfo) { + removeInviteInfo(inviteInfo.getType(), inviteInfo.getChannelId(), inviteInfo.getStream()); + } + + @Override + public void once(InviteSessionType type, Integer channelId, String stream, ErrorCallback callback) { + String key = buildKey(type, channelId, stream); + List> callbacks = inviteErrorCallbackMap.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()); + callbacks.add(callback); + + } + + private String buildKey(InviteSessionType type, Integer channelId, String stream) { + String key = type + ":" + channelId; + // 如果ssrc未null那么可以实现一个通道只能一次操作,ssrc不为null则可以支持一个通道多次invite + if (stream != null) { + key += (":" + stream); + } + return key; + } + + + @Override + public void clearInviteInfo(String deviceId) { + List inviteInfoList = getAllInviteInfo(); + for (InviteInfo inviteInfo : inviteInfoList) { + if (inviteInfo.getDeviceId().equals(deviceId)) { + removeInviteInfo(inviteInfo); + } + } + } + + @Override + public int getStreamInfoCount(String mediaServerId) { + int count = 0; + String key = VideoManagerConstants.INVITE_PREFIX; + List values = redisTemplate.opsForHash().values(key); + if (values.isEmpty()) { + return count; + } + for (Object value : values) { + InviteInfo inviteInfo = (InviteInfo)value; + if (inviteInfo != null + && inviteInfo.getStreamInfo() != null + && inviteInfo.getStreamInfo().getMediaServer() != null + && inviteInfo.getStreamInfo().getMediaServer().getId().equals(mediaServerId)) { + if (inviteInfo.getType().equals(InviteSessionType.DOWNLOAD) && inviteInfo.getStreamInfo().getProgress() == 1) { + continue; + } + count++; + } + } + return count; + } + + @Override + public void call(InviteSessionType type, Integer channelId, String stream, int code, String msg, StreamInfo data) { + String key = buildSubStreamKey(type, channelId, stream); + List> callbacks = inviteErrorCallbackMap.get(key); + if (callbacks == null || callbacks.isEmpty()) { + return; + } + for (ErrorCallback callback : callbacks) { + if (callback != null) { + callback.run(code, msg, data); + } + } + inviteErrorCallbackMap.remove(key); + } + + + private String buildSubStreamKey(InviteSessionType type, Integer channelId, String stream) { + String key = type + ":" + channelId; + if (stream != null) { + key += (":" + stream); + } + return key; + } + + @Override + public InviteInfo getInviteInfoBySSRC(String ssrc) { + List inviteInfoList = getAllInviteInfo(); + if (inviteInfoList.isEmpty()) { + return null; + } + for (InviteInfo inviteInfo : inviteInfoList) { + if (inviteInfo.getSsrcInfo() != null && ssrc.equals(inviteInfo.getSsrcInfo().getSsrc())) { + return inviteInfo; + } + } + return null; + } + + @Override + public InviteInfo updateInviteInfoForSSRC(InviteInfo inviteInfo, String ssrc) { + InviteInfo inviteInfoInDb = getInviteInfo(inviteInfo.getType(), inviteInfo.getChannelId(), inviteInfo.getStream()); + if (inviteInfoInDb == null) { + return null; + } + removeInviteInfo(inviteInfoInDb); + String key = VideoManagerConstants.INVITE_PREFIX; + String objectKey = inviteInfo.getType() + + ":" + inviteInfo.getChannelId() + + ":" + inviteInfo.getStream(); + if (inviteInfoInDb.getSsrcInfo() != null) { + inviteInfoInDb.getSsrcInfo().setSsrc(ssrc); + } + redisTemplate.opsForHash().put(key, objectKey, inviteInfoInDb); + return inviteInfoInDb; + } + + @Scheduled(fixedRate = 10000) //定时检测,清理错误的redis数据,防止因为错误数据导致的点播不可用 + public void execute(){ + String key = VideoManagerConstants.INVITE_PREFIX; + if(redisTemplate.opsForHash().size(key) == 0) { + return; + } + List values = redisTemplate.opsForHash().values(key); + for (Object value : values) { + InviteInfo inviteInfo = (InviteInfo)value; + if (inviteInfo.getStreamInfo() != null) { + continue; + } + if (inviteInfo.getCreateTime() == null || inviteInfo.getExpirationTime() == 0) { + removeInviteInfo(inviteInfo); + } + long time = inviteInfo.getCreateTime() + inviteInfo.getExpirationTime(); + if (System.currentTimeMillis() > time) { + removeInviteInfo(inviteInfo); + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PTZServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PTZServiceImpl.java new file mode 100644 index 0000000..011331f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PTZServiceImpl.java @@ -0,0 +1,111 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.bean.Preset; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IPTZService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.List; + +@Slf4j +@Service +public class PTZServiceImpl implements IPTZService { + + + @Autowired + private SIPCommander cmder; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IRedisRpcPlayService redisRpcPlayService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IDeviceService deviceService; + + + @Override + public void ptz(Device device, String channelId, int cmdCode, int horizonSpeed, int verticalSpeed, int zoomSpeed) { + try { + cmder.frontEndCmd(device, channelId, cmdCode, horizonSpeed, verticalSpeed, zoomSpeed); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 云台控制: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void frontEndCommand(Device device, String channelId, int cmdCode, int parameter1, int parameter2, int combindCode2) { + // 判断设备是否属于当前平台, 如果不属于则发起自动调用 + if (!userSetting.getServerId().equals(device.getServerId())) { + // 通道ID + DeviceChannel deviceChannel = deviceChannelService.getOneForSource(device.getDeviceId(), channelId); + Assert.notNull(deviceChannel, "通道不存在"); + String msg = redisRpcPlayService.frontEndCommand(device.getServerId(), deviceChannel.getId(), cmdCode, parameter1, parameter2, combindCode2); + if (msg != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), msg); + } + return; + } + try { + cmder.frontEndCmd(device, channelId, cmdCode, parameter1, parameter2, combindCode2); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 前端控制: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void frontEndCommand(CommonGBChannel channel, Integer cmdCode, Integer parameter1, Integer parameter2, Integer combindCode2) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只有国标通道的支持云台控制 + log.warn("[INFO 消息] 只有国标通道的支持云台控制, 通道ID: {}", channel.getGbId()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "不支持"); + } + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备ID"); + } + DeviceChannel deviceChannel = deviceChannelService.getOneById(channel.getGbId()); + frontEndCommand(device, deviceChannel.getDeviceId(), cmdCode, parameter1, parameter2, combindCode2); + } + + @Override + public void queryPresetList(CommonGBChannel channel, ErrorCallback> callback) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只有国标通道的支持云台控制 + log.warn("[INFO 消息] 只有国标通道的支持云台控制, 通道ID: {}", channel.getGbId()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "不支持"); + } + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备"); + } + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + if (deviceChannel == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道"); + } + deviceService.queryPreset(device, deviceChannel.getDeviceId(), callback); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlatformChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlatformChannelServiceImpl.java new file mode 100755 index 0000000..44e310d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlatformChannelServiceImpl.java @@ -0,0 +1,763 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.dao.*; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.channel.ChannelEvent; +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.*; + +/** + * @author lin + */ +@Slf4j +@Service +public class PlatformChannelServiceImpl implements IPlatformChannelService { + + @Autowired + private PlatformChannelMapper platformChannelMapper; + + @Autowired + private EventPublisher eventPublisher; + + @Autowired + private GroupMapper groupMapper; + + + @Autowired + private RegionMapper regionMapper; + + @Autowired + private CommonGBChannelMapper commonGBChannelMapper; + + @Autowired + private PlatformMapper platformMapper; + + @Autowired + private ISIPCommanderForPlatform sipCommanderFroPlatform; + + @Autowired + private SubscribeHolder subscribeHolder; + + @Autowired + private UserSetting userSetting; + + // 监听通道信息变化 + @EventListener + public void onApplicationEvent(ChannelEvent event) { + if (event.getChannels().isEmpty()) { + return; + } + // 获取通道所关联的平台 + List allPlatform = platformMapper.queryByServerId(userSetting.getServerId()); + // 获取所用订阅 + List platforms = subscribeHolder.getAllCatalogSubscribePlatform(allPlatform); + + Map> platformMap = new HashMap<>(); + Map channelMap = new HashMap<>(); + if (platforms.isEmpty()) { + return; + } + for (CommonGBChannel deviceChannel : event.getChannels()) { + List parentPlatformsForGB = queryPlatFormListByChannelDeviceId( + deviceChannel.getGbId(), platforms); + platformMap.put(deviceChannel.getGbDeviceId(), parentPlatformsForGB); + channelMap.put(deviceChannel.getGbDeviceId(), deviceChannel); + } + if (platformMap.isEmpty()) { + return; + } + switch (event.getMessageType()) { + case ON: + case OFF: + case DEL: + for (String serverGbId : platformMap.keySet()) { + List platformList = platformMap.get(serverGbId); + if (platformList != null && !platformList.isEmpty()) { + for (Platform platform : platformList) { + SubscribeInfo subscribeInfo = subscribeHolder.getCatalogSubscribe(platform.getServerGBId()); + if (subscribeInfo == null) { + continue; + } + log.info("[Catalog事件: {}]平台:{},影响通道{}", event.getMessageType(), platform.getServerGBId(), serverGbId); + List deviceChannelList = new ArrayList<>(); + CommonGBChannel deviceChannel = new CommonGBChannel(); + deviceChannel.setGbDeviceId(serverGbId); + deviceChannelList.add(deviceChannel); + try { + sipCommanderFroPlatform.sendNotifyForCatalogOther(event.getMessageType().name(), platform, deviceChannelList, subscribeInfo, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + }else { + log.info("[Catalog事件: {}] 未找到上级平台: {}", event.getMessageType(), serverGbId); + } + } + break; + case VLOST: + break; + case DEFECT: + break; + case ADD: + case UPDATE: + for (String gbId : platformMap.keySet()) { + List parentPlatforms = platformMap.get(gbId); + if (parentPlatforms != null && !parentPlatforms.isEmpty()) { + for (Platform platform : parentPlatforms) { + SubscribeInfo subscribeInfo = subscribeHolder.getCatalogSubscribe(platform.getServerGBId()); + if (subscribeInfo == null) { + continue; + } + log.info("[Catalog事件: {}]平台:{},影响通道{}", event.getMessageType(), platform.getServerGBId(), gbId); + List channelList = new ArrayList<>(); + CommonGBChannel deviceChannel = channelMap.get(gbId); + channelList.add(deviceChannel); + try { + sipCommanderFroPlatform.sendNotifyForCatalogAddOrUpdate(event.getMessageType().name(), platform, channelList, subscribeInfo, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | + SipException | IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + } + } + break; + default: + break; + } + } + + @EventListener + public void onApplicationEvent(CatalogEvent event) { + log.info("[Catalog事件: {}]通道数量: {}", event.getType(), event.getChannels().size()); + Platform platform = event.getPlatform(); + if (platform == null || platform.getServerGBId() == null) { + return; + } + SubscribeInfo subscribe = subscribeHolder.getCatalogSubscribe(platform.getServerGBId()); + if (subscribe == null) { + return; + } + switch (event.getType()) { + case CatalogEvent.ON: + case CatalogEvent.OFF: + case CatalogEvent.DEL: + List channels = new ArrayList<>(); + if (event.getChannels() != null) { + channels.addAll(event.getChannels()); + } + if (!channels.isEmpty()) { + log.info("[Catalog事件: {}]平台:{},影响通道{}个", event.getType(), platform.getServerGBId(), channels.size()); + try { + sipCommanderFroPlatform.sendNotifyForCatalogOther(event.getType(), platform, channels, subscribe, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + break; + case CatalogEvent.VLOST: + break; + case CatalogEvent.DEFECT: + break; + case CatalogEvent.ADD: + case CatalogEvent.UPDATE: + List deviceChannelList = new ArrayList<>(); + if (event.getChannels() != null) { + deviceChannelList.addAll(event.getChannels()); + } + if (!deviceChannelList.isEmpty()) { + log.info("[Catalog事件: {}]平台:{},影响通道{}个", event.getType(), platform.getServerGBId(), deviceChannelList.size()); + try { + sipCommanderFroPlatform.sendNotifyForCatalogAddOrUpdate(event.getType(), platform, deviceChannelList, subscribe, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + break; + default: + break; + } + } + + + @Override + public PageInfo queryChannelList(int page, int count, String query, Integer channelType, Boolean online, Integer platformId, Boolean hasShare) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = platformChannelMapper.queryForPlatformForWebList(platformId, query, channelType, online, hasShare); + return new PageInfo<>(all); + } + + /** + * 获取通道使用的分组中未分享的 + */ + @Transactional + public Set getGroupNotShareByChannelList(List channelList, Integer platformId) { + // 获取分组中未分享的节点 + Set groupList = groupMapper.queryNotShareGroupForPlatformByChannelList(channelList, platformId); + // 获取这些节点的所有父节点 + if (groupList.isEmpty()) { + return new HashSet<>(); + } + Set allGroup = getAllGroup(groupList); + allGroup.addAll(groupList); + // 获取全部节点中未分享的 + return groupMapper.queryNotShareGroupForPlatformByGroupList(allGroup, platformId); + } + + /** + * 获取通道使用的分组中未分享的 + */ + private Set getRegionNotShareByChannelList(List channelList, Integer platformId) { + // 获取分组中未分享的节点 + Set regionSet = regionMapper.queryNotShareRegionForPlatformByChannelList(channelList, platformId); + // 获取这些节点的所有父节点 + if (regionSet.isEmpty()) { + return new HashSet<>(); + } + Set allRegion = getAllRegion(regionSet); + allRegion.addAll(regionSet); + // 获取全部节点中未分享的 + return regionMapper.queryNotShareRegionForPlatformByRegionList(allRegion, platformId); + } + + /** + * 移除空的共享,并返回移除的分组 + */ + @Transactional + public Set deleteEmptyGroup(Set groupSet, Integer platformId) { + Iterator iterator = groupSet.iterator(); + while (iterator.hasNext()) { + Group group = iterator.next(); + // groupSet 为当前通道直接使用的分组,如果已经没有子分组与其他的通道,则可以移除 + // 获取分组子节点 + Set children = platformChannelMapper.queryShareChildrenGroup(group.getId(), platformId); + if (!children.isEmpty()) { + iterator.remove(); + continue; + } + // 获取分组关联的通道 + List channelList = commonGBChannelMapper.queryShareChannelByParentId(group.getDeviceId(), platformId); + if (!channelList.isEmpty()) { + iterator.remove(); + continue; + } + platformChannelMapper.removePlatformGroupById(group.getId(), platformId); + } + // 如果空了,说明没有通道需要处理了 + if (groupSet.isEmpty()) { + return new HashSet<>(); + } + Set parent = platformChannelMapper.queryShareParentGroupByGroupSet(groupSet, platformId); + if (parent.isEmpty()) { + return groupSet; + }else { + Set parentGroupSet = deleteEmptyGroup(parent, platformId); + groupSet.addAll(parentGroupSet); + return groupSet; + } + } + + /** + * 移除空的共享,并返回移除的行政区划 + */ + private Set deleteEmptyRegion(Set regionSet, Integer platformId) { + Iterator iterator = regionSet.iterator(); + while (iterator.hasNext()) { + Region region = iterator.next(); + // groupSet 为当前通道直接使用的分组,如果已经没有子分组与其他的通道,则可以移除 + // 获取分组子节点 + Set children = platformChannelMapper.queryShareChildrenRegion(region.getDeviceId(), platformId); + if (!children.isEmpty()) { + iterator.remove(); + continue; + } + // 获取分组关联的通道 + List channelList = commonGBChannelMapper.queryShareChannelByCivilCode(region.getDeviceId(), platformId); + if (!channelList.isEmpty()) { + iterator.remove(); + continue; + } + platformChannelMapper.removePlatformRegionById(region.getId(), platformId); + } + // 如果空了,说明没有通道需要处理了 + if (regionSet.isEmpty()) { + return new HashSet<>(); + } + Set parent = platformChannelMapper.queryShareParentRegionByRegionSet(regionSet, platformId); + if (parent.isEmpty()) { + return regionSet; + }else { + Set parentGroupSet = deleteEmptyRegion(parent, platformId); + regionSet.addAll(parentGroupSet); + return regionSet; + } + } + + private Set getAllGroup(Set groupList ) { + if (groupList.isEmpty()) { + return new HashSet<>(); + } + Set channelList = groupMapper.queryParentInChannelList(groupList); + if (channelList.isEmpty()) { + return channelList; + } + Set allParentRegion = getAllGroup(channelList); + channelList.addAll(allParentRegion); + return channelList; + } + + private Set getAllRegion(Set regionSet ) { + if (regionSet.isEmpty()) { + return new HashSet<>(); + } + + Set channelList = regionMapper.queryParentInChannelList(regionSet); + if (channelList.isEmpty()) { + return channelList; + } + Set allParentRegion = getAllRegion(channelList); + channelList.addAll(allParentRegion); + return channelList; + } + + @Override + @Transactional + public int addAllChannel(Integer platformId) { + List channelListNotShare = platformChannelMapper.queryNotShare(platformId, null); + Assert.notEmpty(channelListNotShare, "所有通道已共享"); + return addChannelList(platformId, channelListNotShare); + } + + @Override + @Transactional + public int addChannels(Integer platformId, List channelIds) { + List channelListNotShare = platformChannelMapper.queryNotShare(platformId, channelIds); + Assert.notEmpty(channelListNotShare, "通道已共享"); + return addChannelList(platformId, channelListNotShare); + } + + @Transactional + public int addChannelList(Integer platformId, List channelList) { + Platform platform = platformMapper.query(platformId); + if (platform == null) { + return 0; + } + int result = platformChannelMapper.addChannels(platformId, channelList); + if (result > 0) { + // 查询通道相关的行政区划信息是否共享,如果没共享就添加 + Set regionListNotShare = getRegionNotShareByChannelList(channelList, platformId); + if (!regionListNotShare.isEmpty()) { + int addGroupResult = platformChannelMapper.addPlatformRegion(new ArrayList<>(regionListNotShare), platformId); + if (addGroupResult > 0) { + for (Region region : regionListNotShare) { + // 分组信息排序时需要将顶层排在最后 + channelList.add(0, CommonGBChannel.build(region)); + } + } + } + + // 查询通道相关的分组信息是否共享,如果没共享就添加 + Set groupListNotShare = getGroupNotShareByChannelList(channelList, platformId); + if (!groupListNotShare.isEmpty()) { + int addGroupResult = platformChannelMapper.addPlatformGroup(new ArrayList<>(groupListNotShare), platformId); + if (addGroupResult > 0) { + for (Group group : groupListNotShare) { + // 分组信息排序时需要将顶层排在最后 + channelList.add(0, CommonGBChannel.build(group)); + } + } + } + // 发送消息 + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, channelList, CatalogEvent.ADD); + } catch (Exception e) { + log.warn("[关联通道] 发送失败,数量:{}", channelList.size(), e); + } + } + return result; + } + + @Override + public int removeAllChannel(Integer platformId) { + Platform platform = platformMapper.query(platformId); + if (platform == null) { + return 0; + } + + List channelListShare = platformChannelMapper.queryShare(platformId, null); + Assert.notEmpty(channelListShare, "未共享任何通道"); + int result = platformChannelMapper.removeChannelsWithPlatform(platformId, channelListShare); + if (result > 0) { + // 查询通道相关的分组信息 + Set regionSet = regionMapper.queryByChannelList(channelListShare); + Set deleteRegion = deleteEmptyRegion(regionSet, platformId); + if (!deleteRegion.isEmpty()) { + for (Region region : deleteRegion) { + channelListShare.add(0, CommonGBChannel.build(region)); + } + } + + // 查询通道相关的分组信息 + Set groupSet = groupMapper.queryByChannelList(channelListShare); + Set deleteGroup = deleteEmptyGroup(groupSet, platformId); + if (!deleteGroup.isEmpty()) { + for (Group group : deleteGroup) { + channelListShare.add(0, CommonGBChannel.build(group)); + } + } + // 发送消息 + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, channelListShare, CatalogEvent.DEL); + } catch (Exception e) { + log.warn("[移除全部关联通道] 发送失败,数量:{}", channelListShare.size(), e); + } + } + return result; + } + + @Override + @Transactional + public void addChannelByDevice(Integer platformId, List deviceIds) { + List channelList = commonGBChannelMapper.queryByGbDeviceIdsForIds(ChannelDataType.GB28181, deviceIds); + addChannels(platformId, channelList); + } + + @Override + @Transactional + public void removeChannelByDevice(Integer platformId, List deviceIds) { + List channelList = commonGBChannelMapper.queryByGbDeviceIdsForIds(ChannelDataType.GB28181, deviceIds); + removeChannels(platformId, channelList); + } + + @Transactional + public int removeChannelList(Integer platformId, List channelList) { + Platform platform = platformMapper.query(platformId); + if (platform == null) { + return 0; + } + int result = platformChannelMapper.removeChannelsWithPlatform(platformId, channelList); + if (result > 0) { + // 查询通道相关的分组信息 + Set regionSet = regionMapper.queryByChannelList(channelList); + Set deleteRegion = deleteEmptyRegion(regionSet, platformId); + if (!deleteRegion.isEmpty()) { + for (Region region : deleteRegion) { + channelList.add(0, CommonGBChannel.build(region)); + } + } + + // 查询通道相关的分组信息 + Set groupSet = groupMapper.queryByChannelList(channelList); + Set deleteGroup = deleteEmptyGroup(groupSet, platformId); + if (!deleteGroup.isEmpty()) { + for (Group group : deleteGroup) { + channelList.add(0, CommonGBChannel.build(group)); + } + } + // 发送消息 + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, channelList, CatalogEvent.DEL); + } catch (Exception e) { + log.warn("[移除关联通道] 发送失败,数量:{}", channelList.size(), e); + } + } + return result; + } + + @Override + @Transactional + public int removeChannels(Integer platformId, List channelIds) { + List channelList = platformChannelMapper.queryShare(platformId, channelIds); + if (channelList.isEmpty()) { + return 0; + } + return removeChannelList(platformId, channelList); + } + + @Override + @Transactional + public void removeChannels(List ids) { + List platformList = platformChannelMapper.queryPlatFormListByChannelList(ids); + if (platformList.isEmpty()) { + return; + } + + for (Platform platform : platformList) { + removeChannels(platform.getId(), ids); + } + } + + @Override + @Transactional + public void removeChannel(int channelId) { + List platformList = platformChannelMapper.queryPlatFormListByChannelId(channelId); + if (platformList.isEmpty()) { + return; + } + for (Platform platform : platformList) { + ArrayList ids = new ArrayList<>(); + ids.add(channelId); + removeChannels(platform.getId(), ids); + } + } + + @Override + public List queryByPlatform(Platform platform) { + if (platform == null) { + return null; + } + List commonGBChannelList = commonGBChannelMapper.queryWithPlatform(platform.getId()); + if (commonGBChannelList.isEmpty()) { + return new ArrayList<>(); + } + List channelList = new ArrayList<>(); + // 是否包含平台信息 + if (platform.getCatalogWithPlatform() > 0) { + CommonGBChannel channel = CommonGBChannel.build(platform); + channelList.add(channel); + } + // 关联的行政区划信息 + if (platform.getCatalogWithRegion() > 0) { + // 查询关联平台的行政区划信息 + List regionChannelList = regionMapper.queryByPlatform(platform.getId()); + if (!regionChannelList.isEmpty()) { + channelList.addAll(regionChannelList); + } + } + if (platform.getCatalogWithGroup() > 0) { + // 关联的分组信息 + List groupChannelList = groupMapper.queryForPlatform(platform.getId()); + if (!groupChannelList.isEmpty()) { + channelList.addAll(groupChannelList); + } + } + + channelList.addAll(commonGBChannelList); + return channelList; + } + + @Override + public void pushChannel(Integer platformId) { + Platform platform = platformMapper.query(platformId); + Assert.notNull(platform, "平台不存在"); + List channelList = queryByPlatform(platform); + if (channelList.isEmpty()){ + return; + } + SubscribeInfo subscribeInfo = SubscribeInfo.buildSimulated(platform.getServerGBId(), platform.getServerIp()); + + try { + sipCommanderFroPlatform.sendNotifyForCatalogAddOrUpdate(CatalogEvent.ADD, platform, channelList, subscribeInfo, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | + SipException | IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + + @Override + public void updateCustomChannel(PlatformChannel channel) { + platformChannelMapper.updateCustomChannel(channel); + Platform platform = platformMapper.query(channel.getPlatformId()); + CommonGBChannel commonGBChannel = platformChannelMapper.queryShareChannel(channel.getPlatformId(), channel.getGbId()); + // 发送消息 + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, commonGBChannel, CatalogEvent.UPDATE); + } catch (Exception e) { + log.warn("[自定义通道信息] 发送失败, 平台ID: {}, 通道: {}({})", channel.getPlatformId(), + channel.getGbName(), channel.getId(), e); + } + } + + @Override + @Transactional + public void checkGroupRemove(List channelList, List groupList) { + + List channelIds = new ArrayList<>(); + channelList.stream().forEach(commonGBChannel -> { + channelIds.add(commonGBChannel.getGbId()); + }); + // 获取关联这些通道的平台 + List platformList = platformChannelMapper.queryPlatFormListByChannelList(channelIds); + if (platformList.isEmpty()) { + return; + } + for (Platform platform : platformList) { + Set groupSet; + if (groupList == null || groupList.isEmpty()) { + groupSet = platformChannelMapper.queryShareGroup(platform.getId()); + }else { + groupSet = new HashSet<>(groupList); + } + // 清理空的分组并发送消息 + Set deleteGroup = deleteEmptyGroup(groupSet, platform.getId()); + + List channelListForEvent = new ArrayList<>(); + if (!deleteGroup.isEmpty()) { + for (Group group : deleteGroup) { + channelListForEvent.add(0, CommonGBChannel.build(group)); + } + } + // 发送消息 + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, channelListForEvent, CatalogEvent.DEL); + } catch (Exception e) { + log.warn("[移除关联通道] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + @Transactional + public void checkRegionRemove(List channelList, List regionList) { + List channelIds = new ArrayList<>(); + channelList.stream().forEach(commonGBChannel -> { + channelIds.add(commonGBChannel.getGbId()); + }); + // 获取关联这些通道的平台 + List platformList = platformChannelMapper.queryPlatFormListByChannelList(channelIds); + if (platformList.isEmpty()) { + return; + } + for (Platform platform : platformList) { + Set regionSet; + if (regionList == null || regionList.isEmpty()) { + regionSet = platformChannelMapper.queryShareRegion(platform.getId()); + }else { + regionSet = new HashSet<>(regionList); + } + // 清理空的分组并发送消息 + Set deleteRegion = deleteEmptyRegion(regionSet, platform.getId()); + + List channelListForEvent = new ArrayList<>(); + if (!deleteRegion.isEmpty()) { + for (Region region : deleteRegion) { + channelListForEvent.add(0, CommonGBChannel.build(region)); + } + } + // 发送消息 + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, channelListForEvent, CatalogEvent.DEL); + } catch (Exception e) { + log.warn("[移除关联通道] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + @Transactional + public void checkGroupAdd(List channelList) { + List channelIds = new ArrayList<>(); + channelList.stream().forEach(commonGBChannel -> { + channelIds.add(commonGBChannel.getGbId()); + }); + List platformList = platformChannelMapper.queryPlatFormListByChannelList(channelIds); + if (platformList.isEmpty()) { + return; + } + for (Platform platform : platformList) { + + Set addGroup = getGroupNotShareByChannelList(channelList, platform.getId()); + + List channelListForEvent = new ArrayList<>(); + if (!addGroup.isEmpty()) { + for (Group group : addGroup) { + channelListForEvent.add(0, CommonGBChannel.build(group)); + } + platformChannelMapper.addPlatformGroup(addGroup, platform.getId()); + // 发送消息 + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, channelListForEvent, CatalogEvent.ADD); + } catch (Exception e) { + log.warn("[移除关联通道] 发送失败,数量:{}", channelList.size(), e); + } + } + } + } + + @Override + public void checkRegionAdd(List channelList) { + List channelIds = new ArrayList<>(); + channelList.stream().forEach(commonGBChannel -> { + channelIds.add(commonGBChannel.getGbId()); + }); + List platformList = platformChannelMapper.queryPlatFormListByChannelList(channelIds); + if (platformList.isEmpty()) { + return; + } + for (Platform platform : platformList) { + + Set addRegion = getRegionNotShareByChannelList(channelList, platform.getId()); + List channelListForEvent = new ArrayList<>(); + if (!addRegion.isEmpty()) { + for (Region region : addRegion) { + channelListForEvent.add(0, CommonGBChannel.build(region)); + } + platformChannelMapper.addPlatformRegion(new ArrayList<>(addRegion), platform.getId()); + // 发送消息 + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, channelListForEvent, CatalogEvent.ADD); + } catch (Exception e) { + log.warn("[移除关联通道] 发送失败,数量:{}", channelList.size(), e); + } + } + } + } + + @Override + public List queryPlatFormListByChannelDeviceId(Integer channelId, List platforms) { + return platformChannelMapper.queryPlatFormListForGBWithGBId(channelId, platforms); + } + + @Override + public CommonGBChannel queryChannelByPlatformIdAndChannelId(Integer platformId, Integer channelId) { + return platformChannelMapper.queryShareChannel(platformId, channelId); + } + + @Override + public List queryChannelByPlatformIdAndChannelIds(Integer platformId, List channelIds) { + return platformChannelMapper.queryShare(platformId, channelIds); + } + + @Override + public List queryByPlatformBySharChannelId(String channelDeviceId) { + List commonGBChannels = commonGBChannelMapper.queryByDeviceId(channelDeviceId); + ArrayList ids = new ArrayList<>(); + for (CommonGBChannel commonGBChannel : commonGBChannels) { + ids.add(commonGBChannel.getGbId()); + } + return platformChannelMapper.queryPlatFormListByChannelList(ids); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlatformServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlatformServiceImpl.java new file mode 100755 index 0000000..7d827c5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlatformServiceImpl.java @@ -0,0 +1,902 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.*; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.dao.PlatformChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.PlatformMapper; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.task.platformStatus.PlatformKeepaliveTask; +import com.genersoft.iot.vmp.gb28181.task.platformStatus.PlatformRegisterTask; +import com.genersoft.iot.vmp.gb28181.task.platformStatus.PlatformRegisterTaskInfo; +import com.genersoft.iot.vmp.gb28181.task.platformStatus.PlatformStatusTaskRunner; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.HookData; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaSendRtpStoppedEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.bean.*; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import javax.sdp.*; +import javax.sip.InvalidArgumentException; +import javax.sip.ResponseEvent; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.List; +import java.util.UUID; +import java.util.Vector; +import java.util.concurrent.TimeUnit; + +/** + * @author lin + */ +@Slf4j +@Service +@Order(value=15) +public class PlatformServiceImpl implements IPlatformService, CommandLineRunner { + + @Autowired + private PlatformMapper platformMapper; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private SSRCFactory ssrcFactory; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private ISIPCommanderForPlatform commanderForPlatform; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private SubscribeHolder subscribeHolder; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IRedisRpcService redisRpcService; + + @Autowired + private SipConfig sipConfig; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private PlatformChannelMapper platformChannelMapper; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private PlatformStatusTaskRunner statusTaskRunner; + + @Override + public void run(String... args) throws Exception { + + // 查找国标推流 + List sendRtpItems = redisCatchStorage.queryAllSendRTPServer(); + if (!sendRtpItems.isEmpty()) { + for (SendRtpInfo sendRtpItem : sendRtpItems) { + MediaServer mediaServerItem = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + CommonGBChannel channel = channelService.getOne(sendRtpItem.getChannelId()); + if (channel == null){ + continue; + } + sendRtpServerService.delete(sendRtpItem); + if (mediaServerItem != null) { + ssrcFactory.releaseSsrc(sendRtpItem.getMediaServerId(), sendRtpItem.getSsrc()); + boolean stopResult = mediaServerService.initStopSendRtp(mediaServerItem, sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getSsrc()); + if (stopResult) { + Platform platform = queryPlatformByServerGBId(sendRtpItem.getTargetId()); + + if (platform != null) { + try { + commanderForPlatform.streamByeCmd(platform, sendRtpItem, channel); + } catch (InvalidArgumentException | ParseException | SipException e) { + log.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage()); + } + } + } + } + } + } + + // 启动时 如果存在未过期的注册平台,则发送注销 + List registerTaskInfoList = statusTaskRunner.getAllRegisterTaskInfo(); + if (registerTaskInfoList.isEmpty()) { + return; + } + for (PlatformRegisterTaskInfo taskInfo : registerTaskInfoList) { + log.info("[国标级联] 启动服务是发现平台注册仍在有效期,注销: {}", taskInfo.getPlatformServerId()); + Platform platform = queryPlatformByServerGBId(taskInfo.getPlatformServerId()); + if (platform == null) { + statusTaskRunner.removeRegisterTask(taskInfo.getPlatformServerId()); + continue; + } + sendUnRegister(platform, taskInfo.getSipTransactionInfo()); + } + // 启动时所有平台默认离线 + platformMapper.offlineAll(userSetting.getServerId()); + } + @Scheduled(fixedDelay = 20, timeUnit = TimeUnit.SECONDS) //每3秒执行一次 + public void statusLostCheck(){ + // 每隔20秒检测,是否存在启用但是未注册的平台,存在则发起注册 + // 获取所有在线并且启用的平台 + List platformList = platformMapper.queryServerIdsWithEnableAndServer(userSetting.getServerId()); + if (platformList.isEmpty()) { + return; + } + for (Platform platform : platformList) { + if (statusTaskRunner.containsRegister(platform.getServerGBId()) && statusTaskRunner.containsKeepAlive(platform.getServerGBId())) { + continue; + } + if (statusTaskRunner.containsRegister(platform.getServerGBId())) { + SipTransactionInfo transactionInfo = statusTaskRunner.getRegisterTransactionInfo(platform.getServerGBId()); + // 注销后出发平台离线, 如果是启用的平台,那么下次丢失检测会检测到并重新注册上线 + sendUnRegister(platform, transactionInfo); + }else { + statusTaskRunner.removeKeepAliveTask(platform.getServerGBId()); + sendRegister(platform, null); + } + } + } + + private void sendRegister(Platform platform, SipTransactionInfo sipTransactionInfo) { + try { + commanderForPlatform.register(platform, sipTransactionInfo, eventResult -> { + log.info("[国标级联] {}({}),注册失败", platform.getName(), platform.getServerGBId()); + offline(platform); + }, null); + } catch (InvalidArgumentException | ParseException | SipException e) { + log.error("[命令发送失败] 国标级联: {}", e.getMessage()); + } + } + + private void sendUnRegister(Platform platform, SipTransactionInfo sipTransactionInfo) { + statusTaskRunner.removeRegisterTask(platform.getServerGBId()); + statusTaskRunner.removeKeepAliveTask(platform.getServerGBId()); + try { + commanderForPlatform.unregister(platform, sipTransactionInfo, null, eventResult -> { + log.info("[国标级联] 注销成功, 平台:{}", platform.getServerGBId()); + }); + } catch (InvalidArgumentException | ParseException | SipException e) { + log.error("[命令发送失败] 国标级联: {}", e.getMessage()); + } + } + + // 定时监听国标级联所进行的WVP服务是否正常, 如果异常则选择新的wvp执行 + @Scheduled(fixedDelay = 2, timeUnit = TimeUnit.SECONDS) //每3秒执行一次 + public void execute(){ + if (!userSetting.isAutoRegisterPlatform()) { + return; + } + // 查找非平台的国标级联执行服务Id + List serverIds = platformMapper.queryServerIdsWithEnableAndNotInServer(userSetting.getServerId()); + if (serverIds == null || serverIds.isEmpty()) { + return; + } + serverIds.forEach(serverId -> { + // 检查每个是否存活 + ServerInfo serverInfo = redisCatchStorage.queryServerInfo(serverId); + if (serverInfo != null) { + return; + } + log.info("[集群] 检测到 {} 已离线", serverId); + redisCatchStorage.removeOfflineWVPInfo(serverId); + String chooseServerId = redisCatchStorage.chooseOneServer(serverId); + if (!userSetting.getServerId().equals(chooseServerId)){ + return; + } + // 此平台需要选择新平台处理, 确定由当前平台即开始处理 + List platformList = platformMapper.queryByServerId(serverId); + platformList.forEach(platform -> { + log.info("[集群] 由本平台开启上级平台{}({})的注册", platform.getName(), platform.getServerGBId()); + // 设置平台使用当前平台的IP + platform.setAddress(getIpWithSameNetwork(platform.getAddress())); + platform.setServerId(userSetting.getServerId()); + platformMapper.update(platform); + // 检查就平台是否注册到期,没有则注销,由本平台重新注册 + List taskInfoList = statusTaskRunner.getRegisterTransactionInfoByServerId(serverId); + boolean needUnregister = false; + SipTransactionInfo sipTransactionInfo = null; + if (!taskInfoList.isEmpty()) { + for (PlatformRegisterTaskInfo taskInfo : taskInfoList) { + if (taskInfo.getPlatformServerId().equals(platform.getServerGBId()) + && taskInfo.getSipTransactionInfo() != null) { + needUnregister = true; + sipTransactionInfo = taskInfo.getSipTransactionInfo(); + break; + } + } + } + if (needUnregister) { + sendUnRegister(platform, sipTransactionInfo); + }else { + // 开始注册 + // 注册成功时由程序直接调用了online方法 + sendRegister(platform, null); + } + }); + }); + } + + /** + * 获取同网段的IP + */ + private String getIpWithSameNetwork(String ip){ + if (ip == null || sipConfig.getMonitorIps().size() == 1) { + return sipConfig.getMonitorIps().get(0); + } + String[] ipSplit = ip.split("\\."); + String ip1 = null, ip2 = null, ip3 = null; + for (String monitorIp : sipConfig.getMonitorIps()) { + String[] monitorIpSplit = monitorIp.split("\\."); + if (monitorIpSplit[0].equals(ipSplit[0]) && monitorIpSplit[1].equals(ipSplit[1]) && monitorIpSplit[2].equals(ipSplit[2])) { + ip3 = monitorIp; + }else if (monitorIpSplit[0].equals(ipSplit[0]) && monitorIpSplit[1].equals(ipSplit[1])) { + ip2 = monitorIp; + }else if (monitorIpSplit[0].equals(ipSplit[0])) { + ip1 = monitorIp; + } + } + if (ip3 != null) { + return ip3; + }else if (ip2 != null) { + return ip2; + }else if (ip1 != null) { + return ip1; + }else { + return sipConfig.getMonitorIps().get(0); + } + } + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + List sendRtpItems = sendRtpServerService.queryByStream(event.getStream()); + if (!sendRtpItems.isEmpty()) { + for (SendRtpInfo sendRtpItem : sendRtpItems) { + if (sendRtpItem != null && sendRtpItem.getApp().equals(event.getApp()) && sendRtpItem.isSendToPlatform()) { + String platformId = sendRtpItem.getTargetId(); + Platform platform = platformMapper.getParentPlatByServerGBId(platformId); + CommonGBChannel channel = channelService.getOne(sendRtpItem.getChannelId()); + try { + if (platform != null && channel != null) { + commanderForPlatform.streamByeCmd(platform, sendRtpItem, channel); + sendRtpServerService.delete(sendRtpItem); + } + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 发送BYE: {}", e.getMessage()); + } + } + } + } + } + + + /** + * 发流停止 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaSendRtpStoppedEvent event) { + List sendRtpItems = sendRtpServerService.queryByStream(event.getStream()); + if (sendRtpItems != null && !sendRtpItems.isEmpty()) { + for (SendRtpInfo sendRtpItem : sendRtpItems) { + if (sendRtpItem != null && sendRtpItem.getApp().equals(event.getApp()) && sendRtpItem.isSendToPlatform()) { + Platform platform = platformMapper.getParentPlatByServerGBId(sendRtpItem.getTargetId()); + CommonGBChannel channel = channelService.getOne(sendRtpItem.getChannelId()); + ssrcFactory.releaseSsrc(sendRtpItem.getMediaServerId(), sendRtpItem.getSsrc()); + try { + commanderForPlatform.streamByeCmd(platform, sendRtpItem, channel); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage()); + } + sendRtpServerService.delete(sendRtpItem); + } + } + } + } + + @Override + public Platform queryPlatformByServerGBId(String platformGbId) { + return platformMapper.getParentPlatByServerGBId(platformGbId); + } + + @Override + public PageInfo queryPlatformList(int page, int count, String query) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = platformMapper.queryList(query); + return new PageInfo<>(all); + } + + @Override + public boolean add(Platform platform) { + log.info("[国标级联]添加平台 {}", platform.getDeviceGBId()); + if (platform.getCatalogGroup() == 0) { + // 每次发送目录的数量默认为1 + platform.setCatalogGroup(1); + } + platform.setServerId(userSetting.getServerId()); + int result = platformMapper.add(platform); + + if (platform.isEnable()) { + // 保存时启用就发送注册 + // 注册成功时由程序直接调用了online方法 + sendRegister(platform, null); + } + return result > 0; + } + + + + @Override + public boolean update(Platform platform) { + Assert.isTrue(platform.getId() > 0, "ID必须存在"); + log.info("[国标级联] 更新平台 {}({})", platform.getName(), platform.getDeviceGBId()); + platform.setCharacterSet(platform.getCharacterSet().toUpperCase()); + Platform platformInDb = platformMapper.query(platform.getId()); + Assert.notNull(platformInDb, "平台不存在"); + if (!userSetting.getServerId().equals(platformInDb.getServerId())) { + return redisRpcService.updatePlatform(platformInDb.getServerId(), platform); + } + // 更新数据库 + if (platform.getCatalogGroup() == 0) { + platform.setCatalogGroup(1); + } + platformMapper.update(platform); + if (statusTaskRunner.containsRegister(platformInDb.getServerGBId())) { + SipTransactionInfo transactionInfo = statusTaskRunner.getRegisterTransactionInfo(platformInDb.getServerGBId()); + // 注销后出发平台离线, 如果是启用的平台,那么下次丢失检测会检测到并重新注册上线 + sendUnRegister(platformInDb, transactionInfo); + }else if (platform.isEnable()) { + sendRegister(platform, null); + } + + return false; + } + + @Override + public void online(Platform platform, SipTransactionInfo sipTransactionInfo) { + log.info("[国标级联]:{}, 平台上线", platform.getServerGBId()); + PlatformRegisterTask registerTask = new PlatformRegisterTask(platform.getServerGBId(), platform.getExpires() * 1000L - 500L, + sipTransactionInfo, (platformServerGbId) -> { + this.registerExpire(platformServerGbId, sipTransactionInfo); + }); + statusTaskRunner.addRegisterTask(registerTask); + + PlatformKeepaliveTask keepaliveTask = new PlatformKeepaliveTask(platform.getServerGBId(), platform.getKeepTimeout() * 1000L, + this::keepaliveExpire); + statusTaskRunner.addKeepAliveTask(keepaliveTask); + platformMapper.updateStatus(platform.getId(), true, userSetting.getServerId()); + + if (platform.getAutoPushChannel() != null && platform.getAutoPushChannel()) { + if (subscribeHolder.getCatalogSubscribe(platform.getServerGBId()) == null) { + log.info("[国标级联]:{}, 添加自动通道推送模拟订阅信息", platform.getServerGBId()); + addSimulatedSubscribeInfo(platform); + } + }else { + SubscribeInfo catalogSubscribe = subscribeHolder.getCatalogSubscribe(platform.getServerGBId()); + if (catalogSubscribe != null && catalogSubscribe.getExpires() == -1) { + subscribeHolder.removeCatalogSubscribe(platform.getServerGBId()); + } + } + } + + /** + * 注册到期处理 + */ + private void registerExpire(String platformServerId, SipTransactionInfo transactionInfo) { + log.info("[国标级联] 注册到期, 上级平台编号: {}", platformServerId); + Platform platform = queryPlatformByServerGBId(platformServerId); + if (platform == null || !platform.isEnable()) { + log.info("[国标级联] 注册到期, 上级平台编号: {}, 平台不存在或者未启用, 忽略", platformServerId); + return; + } + sendRegister(platform, transactionInfo); + } + + private void keepaliveExpire(String platformServerId, int failCount) { + Platform platform = queryPlatformByServerGBId(platformServerId); + if (platform == null || !platform.isEnable()) { + log.info("[国标级联] 心跳到期, 上级平台编号: {}, 平台不存在或者未启用, 忽略", platformServerId); + return; + } + try { + commanderForPlatform.keepalive(platform, eventResult -> { + // 心跳失败 + if (eventResult.type != SipSubscribe.EventResultType.timeout) { + log.warn("[国标级联] 发送心跳收到错误,code: {}, msg: {}", eventResult.statusCode, eventResult.msg); + } + + // 心跳超时失败 + if (failCount < 2) { + log.info("[国标级联] 心跳发送超时, 平台服务编号: {}", platformServerId); + PlatformKeepaliveTask keepaliveTask = new PlatformKeepaliveTask(platform.getServerGBId(), platform.getKeepTimeout() * 1000L, + this::keepaliveExpire); + keepaliveTask.setFailCount(failCount + 1); + statusTaskRunner.addKeepAliveTask(keepaliveTask); + }else { + // 心跳超时三次, 不再发送心跳, 平台离线 + log.info("[国标级联] 心跳发送超时三次,平台离线, 平台服务编号: {}", platformServerId); + offline(platform); + } + }, eventResult -> { + PlatformKeepaliveTask keepaliveTask = new PlatformKeepaliveTask(platform.getServerGBId(), platform.getKeepTimeout() * 1000L, + this::keepaliveExpire); + statusTaskRunner.addKeepAliveTask(keepaliveTask); + }); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 发送心跳: {}", e.getMessage()); + if (failCount < 2) { + PlatformKeepaliveTask keepaliveTask = new PlatformKeepaliveTask(platform.getServerGBId(), platform.getKeepTimeout() * 1000L, + this::keepaliveExpire); + keepaliveTask.setFailCount(failCount + 1); + statusTaskRunner.addKeepAliveTask(keepaliveTask); + }else { + // 心跳超时三次, 不再发送心跳, 平台离线 + log.info("[国标级联] 心跳发送失败三次,平台离线, 平台服务编号: {}", platformServerId); + offline(platform); + } + } + } + + @Override + public void addSimulatedSubscribeInfo(Platform platform) { + // 自动添加一条模拟的订阅信息 + subscribeHolder.putCatalogSubscribe(platform.getServerGBId(), + SubscribeInfo.buildSimulated(platform.getServerGBId(), platform.getServerIp())); + } + + @Override + public void offline(Platform platform) { + log.info("[平台离线]:{}({})", platform.getName(), platform.getServerGBId()); + statusTaskRunner.removeRegisterTask(platform.getServerGBId()); + statusTaskRunner.removeKeepAliveTask(platform.getServerGBId()); + + subscribeHolder.removeCatalogSubscribe(platform.getServerGBId()); + subscribeHolder.removeMobilePositionSubscribe(platform.getServerGBId()); + + platformMapper.updateStatus(platform.getId(), false, userSetting.getServerId()); + + // 停止所有推流 + log.info("[平台离线] {}({}), 停止所有推流", platform.getName(), platform.getServerGBId()); + stopAllPush(platform.getServerGBId()); + } + + private void stopAllPush(String platformId) { + List sendRtpItems = sendRtpServerService.queryForPlatform(platformId); + if (sendRtpItems != null && !sendRtpItems.isEmpty()) { + for (SendRtpInfo sendRtpItem : sendRtpItems) { + ssrcFactory.releaseSsrc(sendRtpItem.getMediaServerId(), sendRtpItem.getSsrc()); + sendRtpServerService.delete(sendRtpItem); + MediaServer mediaInfo = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + mediaServerService.stopSendRtp(mediaInfo, sendRtpItem.getApp(), sendRtpItem.getStream(), null); + } + } + } + + @Override + public void sendNotifyMobilePosition(String platformId) { + Platform platform = platformMapper.getParentPlatByServerGBId(platformId); + if (platform == null) { + return; + } + SubscribeInfo subscribe = subscribeHolder.getMobilePositionSubscribe(platform.getServerGBId()); + if (subscribe != null) { + + List channelList = platformChannelMapper.queryShare(platform.getId(), null); + if (channelList.isEmpty()) { + return; + } + for (CommonGBChannel channel : channelList) { + GPSMsgInfo gpsMsgInfo = redisCatchStorage.getGpsMsgInfo(channel.getGbDeviceId()); + // 无最新位置则发送当前位置 + if (gpsMsgInfo != null && (gpsMsgInfo.getLng() == 0 && gpsMsgInfo.getLat() == 0)) { + gpsMsgInfo = null; + } + + if (gpsMsgInfo == null && !userSetting.isSendPositionOnDemand()){ + gpsMsgInfo = new GPSMsgInfo(); + gpsMsgInfo.setId(channel.getGbDeviceId()); + gpsMsgInfo.setLng(channel.getGbLongitude()); + gpsMsgInfo.setLat(channel.getGbLatitude()); + gpsMsgInfo.setAltitude(channel.getGpsAltitude()); + gpsMsgInfo.setSpeed(channel.getGpsSpeed()); + gpsMsgInfo.setDirection(channel.getGpsDirection()); + gpsMsgInfo.setTime(channel.getGpsTime()); + } + + // 无最新位置不发送 + if (gpsMsgInfo != null) { + // 发送GPS消息 + try { + commanderForPlatform.sendNotifyMobilePosition(platform, gpsMsgInfo, channel, subscribe); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 移动位置通知: {}", e.getMessage()); + } + } + } + } + } + + @Override + public void broadcastInvite(Platform platform, CommonGBChannel channel, String sourceId, MediaServer mediaServerItem, HookSubscribe.Event hookEvent, + SipSubscribe.Event errorEvent, InviteTimeOutCallback timeoutCallback) throws InvalidArgumentException, ParseException, SipException { + + if (mediaServerItem == null) { + log.info("[国标级联] 语音喊话未找到可用的zlm. platform: {}", platform.getServerGBId()); + return; + } + InviteInfo inviteInfoForOld = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.BROADCAST, channel.getGbId()); + + if (inviteInfoForOld != null && inviteInfoForOld.getStreamInfo() != null) { + // 如果zlm不存在这个流,则删除数据即可 + MediaServer mediaServerItemForStreamInfo = mediaServerService.getOne(inviteInfoForOld.getStreamInfo().getMediaServer().getId()); + if (mediaServerItemForStreamInfo != null) { + Boolean ready = mediaServerService.isStreamReady(mediaServerItemForStreamInfo, inviteInfoForOld.getStreamInfo().getApp(), inviteInfoForOld.getStreamInfo().getStream()); + if (!ready) { + // 错误存在于redis中的数据 + inviteStreamService.removeInviteInfo(inviteInfoForOld); + }else { + // 流确实尚在推流,直接回调结果 + HookData hookData = new HookData(); + hookData.setApp(inviteInfoForOld.getStreamInfo().getApp()); + hookData.setStream(inviteInfoForOld.getStreamInfo().getStream()); + hookData.setMediaServer(mediaServerItemForStreamInfo); + hookEvent.response(hookData); + return; + } + } + } + + String streamId = null; + if (mediaServerItem.isRtpEnable()) { + streamId = String.format("%s_%s", platform.getServerGBId(), channel.getGbDeviceId()); + } + // 默认不进行SSRC校验, TODO 后续可改为配置 + boolean ssrcCheck = false; + int tcpMode; + if (userSetting.getBroadcastForPlatform().equalsIgnoreCase("TCP-PASSIVE")) { + tcpMode = 1; + }else if (userSetting.getBroadcastForPlatform().equalsIgnoreCase("TCP-ACTIVE")) { + tcpMode = 2; + } else { + tcpMode = 0; + } + SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, null, ssrcCheck, false, null, true, false, false, tcpMode); + if (ssrcInfo == null || ssrcInfo.getPort() < 0) { + log.info("[国标级联] 发起语音喊话 开启端口监听失败, platform: {}, channel: {}", platform.getServerGBId(), channel.getGbDeviceId()); + SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult<>(); + eventResult.statusCode = -1; + eventResult.msg = "端口监听失败"; + eventResult.type = SipSubscribe.EventResultType.failedToGetPort; + errorEvent.response(eventResult); + return; + } + log.info("[国标级联] 语音喊话,发起Invite消息 deviceId: {}, channelId: {},收流端口: {}, 收流模式:{}, SSRC: {}, SSRC校验:{}", + platform.getServerGBId(), channel.getGbDeviceId(), ssrcInfo.getPort(), userSetting.getBroadcastForPlatform(), ssrcInfo.getSsrc(), ssrcCheck); + + // 初始化redis中的invite消息状态 + InviteInfo inviteInfo = InviteInfo.getInviteInfo(platform.getServerGBId(), channel.getGbId(), ssrcInfo.getStream(), ssrcInfo, mediaServerItem.getId(), + mediaServerItem.getSdpIp(), ssrcInfo.getPort(), userSetting.getBroadcastForPlatform(), InviteSessionType.BROADCAST, + InviteSessionStatus.ready, userSetting.getRecordSip()); + inviteStreamService.updateInviteInfo(inviteInfo); + String timeOutTaskKey = UUID.randomUUID().toString(); + dynamicTask.startDelay(timeOutTaskKey, () -> { + // 执行超时任务时查询是否已经成功,成功了则不执行超时任务,防止超时任务取消失败的情况 + InviteInfo inviteInfoForBroadcast = inviteStreamService.getInviteInfo(InviteSessionType.BROADCAST, channel.getGbId(), null); + if (inviteInfoForBroadcast == null) { + log.info("[国标级联] 发起语音喊话 收流超时 deviceId: {}, channelId: {},端口:{}, SSRC: {}", platform.getServerGBId(), channel.getGbDeviceId(), ssrcInfo.getPort(), ssrcInfo.getSsrc()); + // 点播超时回复BYE 同时释放ssrc以及此次点播的资源 + try { + commanderForPlatform.streamByeCmd(platform, channel, ssrcInfo.getApp(), ssrcInfo.getStream(), null, null); + } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { + log.error("[点播超时], 发送BYE失败 {}", e.getMessage()); + } finally { + timeoutCallback.run(1, "收流超时"); + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream()); + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + } + } + }, userSetting.getPlayTimeout()); + commanderForPlatform.broadcastInviteCmd(platform, channel,sourceId, mediaServerItem, ssrcInfo, (hookData)->{ + log.info("[国标级联] 发起语音喊话 收到上级推流 deviceId: {}, channelId: {}", platform.getServerGBId(), channel.getGbDeviceId()); + dynamicTask.stop(timeOutTaskKey); + // hook响应 + onPublishHandlerForBroadcast(hookData.getMediaServer(), hookData.getMediaInfo(), platform, channel); + // 收到流 + if (hookEvent != null) { + hookEvent.response(hookData); + } + }, event -> { + + inviteOKHandler(event, ssrcInfo, tcpMode, ssrcCheck, mediaServerItem, platform, channel, timeOutTaskKey, + null, inviteInfo, InviteSessionType.BROADCAST); + }, eventResult -> { + // 收到错误回复 + if (errorEvent != null) { + errorEvent.response(eventResult); + } + }); + } + + public void onPublishHandlerForBroadcast(MediaServer mediaServerItem, MediaInfo mediaInfo, Platform platform, CommonGBChannel channel) { + StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServerItem, mediaInfo.getApp(), mediaInfo.getStream(), mediaInfo, null); + streamInfo.setChannelId(channel.getGbId()); + + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.BROADCAST, channel.getGbId()); + if (inviteInfo != null) { + inviteInfo.setStatus(InviteSessionStatus.ok); + inviteInfo.setStreamInfo(streamInfo); + inviteStreamService.updateInviteInfo(inviteInfo); + } + } + + private void inviteOKHandler(SipSubscribe.EventResult eventResult, SSRCInfo ssrcInfo, int tcpMode, boolean ssrcCheck, MediaServer mediaServerItem, + Platform platform, CommonGBChannel channel, String timeOutTaskKey, ErrorCallback callback, + InviteInfo inviteInfo, InviteSessionType inviteSessionType){ + inviteInfo.setStatus(InviteSessionStatus.ok); + ResponseEvent responseEvent = (ResponseEvent) eventResult.event; + String contentString = new String(responseEvent.getResponse().getRawContent()); + String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString); + // 兼容回复的消息中缺少ssrc(y字段)的情况 + if (ssrcInResponse == null) { + ssrcInResponse = ssrcInfo.getSsrc(); + } + if (ssrcInfo.getSsrc().equals(ssrcInResponse)) { + // ssrc 一致 + if (mediaServerItem.isRtpEnable()) { + // 多端口 + if (tcpMode == 2) { + tcpActiveHandler(platform, channel, contentString, mediaServerItem, tcpMode, ssrcCheck, + timeOutTaskKey, ssrcInfo, callback); + } + }else { + // 单端口 + if (tcpMode == 2) { + log.warn("[Invite 200OK] 单端口收流模式不支持tcp主动模式收流"); + } + } + }else { + log.info("[Invite 200OK] 收到invite 200, 发现下级自定义了ssrc: {}", ssrcInResponse); + // ssrc 不一致 + if (mediaServerItem.isRtpEnable()) { + // 多端口 + if (ssrcCheck) { + // ssrc检验 + // 更新ssrc + log.info("[Invite 200OK] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + Boolean result = mediaServerService.updateRtpServerSSRC(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse); + if (!result) { + try { + log.warn("[Invite 200OK] 更新ssrc失败,停止喊话 {}/{}", platform.getServerGBId(), channel.getGbDeviceId()); + commanderForPlatform.streamByeCmd(platform, channel, ssrcInfo.getApp(), ssrcInfo.getStream(), null, null); + } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { + log.error("[命令发送失败] 停止播放, 发送BYE: {}", e.getMessage()); + } + + dynamicTask.stop(timeOutTaskKey); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + + callback.run(InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(), + "下级自定义了ssrc,重新设置收流信息失败", null); + inviteStreamService.call(inviteSessionType, channel.getGbId(), null, + InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(), + "下级自定义了ssrc,重新设置收流信息失败", null); + + }else { + ssrcInfo.setSsrc(ssrcInResponse); + inviteInfo.setSsrcInfo(ssrcInfo); + inviteInfo.setStream(ssrcInfo.getStream()); + if (tcpMode == 2) { + if (mediaServerItem.isRtpEnable()) { + tcpActiveHandler(platform, channel, contentString, mediaServerItem, tcpMode, ssrcCheck, + timeOutTaskKey, ssrcInfo, callback); + }else { + log.warn("[Invite 200OK] 单端口收流模式不支持tcp主动模式收流"); + } + } + inviteStreamService.updateInviteInfo(inviteInfo); + } + }else { + ssrcInfo.setSsrc(ssrcInResponse); + inviteInfo.setSsrcInfo(ssrcInfo); + inviteInfo.setStream(ssrcInfo.getStream()); + if (tcpMode == 2) { + if (mediaServerItem.isRtpEnable()) { + tcpActiveHandler(platform, channel, contentString, mediaServerItem, tcpMode, ssrcCheck, + timeOutTaskKey, ssrcInfo, callback); + }else { + log.warn("[Invite 200OK] 单端口收流模式不支持tcp主动模式收流"); + } + } + inviteStreamService.updateInviteInfo(inviteInfo); + } + }else { + if (ssrcInResponse != null) { + // 单端口 + // 重新订阅流上线 + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByStream(ssrcInfo.getApp(), inviteInfo.getStream()); + sessionManager.removeByStream(ssrcInfo.getApp(), inviteInfo.getStream()); + inviteStreamService.updateInviteInfoForSSRC(inviteInfo, ssrcInResponse); + + ssrcTransaction.setPlatformId(platform.getServerGBId()); + ssrcTransaction.setChannelId(channel.getGbId()); + ssrcTransaction.setApp(ssrcInfo.getApp()); + ssrcTransaction.setStream(inviteInfo.getStream()); + ssrcTransaction.setSsrc(ssrcInResponse); + ssrcTransaction.setMediaServerId(mediaServerItem.getId()); + ssrcTransaction.setSipTransactionInfo(new SipTransactionInfo((SIPResponse) responseEvent.getResponse())); + ssrcTransaction.setType(inviteSessionType); + + sessionManager.put(ssrcTransaction); + } + } + } + } + + + private void tcpActiveHandler(Platform platform, CommonGBChannel channel, String contentString, + MediaServer mediaServerItem, int tcpMode, boolean ssrcCheck, + String timeOutTaskKey, SSRCInfo ssrcInfo, ErrorCallback callback){ + if (tcpMode != 2) { + return; + } + + String substring; + if (contentString.indexOf("y=") > 0) { + substring = contentString.substring(0, contentString.indexOf("y=")); + }else { + substring = contentString; + } + try { + SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring); + int port = -1; + Vector mediaDescriptions = sdp.getMediaDescriptions(true); + for (Object description : mediaDescriptions) { + MediaDescription mediaDescription = (MediaDescription) description; + Media media = mediaDescription.getMedia(); + + Vector mediaFormats = media.getMediaFormats(false); + if (mediaFormats.contains("8") || mediaFormats.contains("0")) { + port = media.getMediaPort(); + break; + } + } + log.info("[TCP主动连接对方] serverGbId: {}, channelId: {}, 连接对方的地址:{}:{}, SSRC: {}, SSRC校验:{}", + platform.getServerGBId(), channel.getGbDeviceId(), sdp.getConnection().getAddress(), port, ssrcInfo.getSsrc(), ssrcCheck); + Boolean result = mediaServerService.connectRtpServer(mediaServerItem, sdp.getConnection().getAddress(), port, ssrcInfo.getStream()); + log.info("[TCP主动连接对方] 结果: {}", result); + } catch (SdpException e) { + log.error("[TCP主动连接对方] serverGbId: {}, channelId: {}, 解析200OK的SDP信息失败", platform.getServerGBId(), channel.getGbDeviceId(), e); + dynamicTask.stop(timeOutTaskKey); + mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream()); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + + callback.run(InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null); + inviteStreamService.call(InviteSessionType.PLAY, channel.getGbId(), null, + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null); + } + } + + @Override + public void stopBroadcast(Platform platform, CommonGBChannel channel, String app, String stream, boolean sendBye, MediaServer mediaServerItem) { + + try { + if (sendBye) { + commanderForPlatform.streamByeCmd(platform, channel, app, stream, null, null); + } + } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { + log.warn("[消息发送失败] 停止语音对讲, 平台:{},通道:{}", platform.getId(), channel.getGbDeviceId() ); + } finally { + mediaServerService.closeRTPServer(mediaServerItem, stream); + InviteInfo inviteInfo = inviteStreamService.getInviteInfo(null, channel.getGbId(), stream); + if (inviteInfo != null) { + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), inviteInfo.getSsrcInfo().getSsrc()); + inviteStreamService.removeInviteInfo(inviteInfo); + } + sessionManager.removeByStream(app, stream); + } + } + + @Override + public Platform queryOne(Integer platformId) { + return platformMapper.query(platformId); + } + + @Override + public List queryEnablePlatformList(String serverId) { + return platformMapper.queryEnableParentPlatformListByServerId(serverId,true); + } + + @Override + @Transactional + public void delete(Integer platformId, CommonCallback callback) { + Platform platform = platformMapper.query(platformId); + Assert.notNull(platform, "平台不存在"); + if (statusTaskRunner.containsRegister(platform.getServerGBId())) { + try { + SipTransactionInfo transactionInfo = statusTaskRunner.getRegisterTransactionInfo(platform.getServerGBId()); + sendUnRegister(platform, transactionInfo); + }catch (Exception ignored) {} + } + platformMapper.delete(platform.getId()); + + statusTaskRunner.removeRegisterTask(platform.getServerGBId()); + statusTaskRunner.removeKeepAliveTask(platform.getServerGBId()); + + subscribeHolder.removeCatalogSubscribe(platform.getServerGBId()); + subscribeHolder.removeMobilePositionSubscribe(platform.getServerGBId()); + if (callback != null) { + callback.run(true); + } + } + + @Override + public List queryAll(String serverId) { + return platformMapper.queryByServerId(serverId); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java new file mode 100755 index 0000000..8085ba4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java @@ -0,0 +1,1809 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.*; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.exception.ServiceException; +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.controller.bean.AudioBroadcastEvent; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.service.*; +import com.genersoft.iot.vmp.gb28181.session.AudioBroadcastManager; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.media.event.media.MediaNotFoundEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.service.ICloudRecordService; +import com.genersoft.iot.vmp.service.IReceiveRtpServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.bean.*; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import javax.sdp.*; +import javax.sip.InvalidArgumentException; +import javax.sip.ResponseEvent; +import javax.sip.SipException; +import javax.sip.header.CallIdHeader; +import javax.sip.message.Response; +import java.io.File; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.Vector; + +@SuppressWarnings(value = {"rawtypes", "unchecked"}) +@Slf4j +@Service("playService") +public class PlayServiceImpl implements IPlayService { + + @Autowired + private ISIPCommander cmder; + + @Autowired + private AudioBroadcastManager audioBroadcastManager; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private ISIPCommanderForPlatform sipCommanderFroPlatform; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private HookSubscribe subscribe; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private ISIPCommanderForPlatform commanderForPlatform; + + @Autowired + private SSRCFactory ssrcFactory; + + @Autowired + private IPlatformService platformService; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private IReceiveRtpServerService receiveRtpServerService; + + @Autowired + private ICloudRecordService cloudRecordService; + + @Autowired + private IRedisRpcPlayService redisRpcPlayService; + + /** + * 流到来的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaArrivalEvent event) { + if ("broadcast".equals(event.getApp()) || "talk".equals(event.getApp())) { + if (event.getStream().indexOf("_") > 0) { + String[] streamArray = event.getStream().split("_"); + if (streamArray.length == 2) { + String deviceId = streamArray[0]; + String channelId = streamArray[1]; + Device device = deviceService.getDeviceByDeviceId(deviceId); + DeviceChannel channel = deviceChannelService.getOneForSource(deviceId, channelId); + if (device == null) { + log.info("[语音对讲/喊话] 未找到设备:{}", deviceId); + return; + } + if (channel == null) { + log.info("[语音对讲/喊话] 未找到通道:{}", channelId); + return; + } + if ("broadcast".equals(event.getApp())) { + if (audioBroadcastManager.exit(channel.getId())) { + stopAudioBroadcast(device, channel); + } + // 开启语音对讲通道 + try { + audioBroadcastCmd(device, channel, event.getMediaServer(), + event.getApp(), event.getStream(), 60, false, (msg) -> { + log.info("[语音对讲] 通道建立成功, device: {}, channel: {}", deviceId, channelId); + }); + } catch (InvalidArgumentException | ParseException | SipException e) { + log.error("[命令发送失败] 语音对讲: {}", e.getMessage()); + } + }else if ("talk".equals(event.getApp())) { + // 开启语音对讲通道 + talkCmd(device, channel, event.getMediaServer(), event.getStream(), (msg) -> { + log.info("[语音对讲] 通道建立成功, device: {}, channel: {}", deviceId, channelId); + }); + } + } + } + } + + + } + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + List sendRtpInfos = sendRtpServerService.queryByStream(event.getStream()); + if (!sendRtpInfos.isEmpty()) { + for (SendRtpInfo sendRtpInfo : sendRtpInfos) { + if (sendRtpInfo != null && sendRtpInfo.isSendToPlatform() && sendRtpInfo.getApp().equals(event.getApp())) { + String platformId = sendRtpInfo.getTargetId(); + Device device = deviceService.getDeviceByDeviceId(platformId); + DeviceChannel channel = deviceChannelService.getOneById(sendRtpInfo.getChannelId()); + try { + if (device != null && channel != null) { + cmder.streamByeCmd(device, channel.getDeviceId(), event.getApp(), event.getStream(), sendRtpInfo.getCallId(), null); + if (sendRtpInfo.getPlayType().equals(InviteStreamType.BROADCAST) + || sendRtpInfo.getPlayType().equals(InviteStreamType.TALK)) { + AudioBroadcastCatch audioBroadcastCatch = audioBroadcastManager.get(channel.getId()); + if (audioBroadcastCatch != null) { + // 来自上级平台的停止对讲 + log.info("[停止对讲] 来自上级,平台:{}, 通道:{}", sendRtpInfo.getTargetId(), sendRtpInfo.getChannelId()); + audioBroadcastManager.del(sendRtpInfo.getChannelId()); + } + } + } + } catch (SipException | InvalidArgumentException | ParseException | + SsrcTransactionNotFoundException e) { + log.error("[命令发送失败] 发送BYE: {}", e.getMessage()); + } + } + } + } + + if ("broadcast".equals(event.getApp()) || "talk".equals(event.getApp())) { + if (event.getStream().indexOf("_") > 0) { + String[] streamArray = event.getStream().split("_"); + if (streamArray.length == 2) { + String deviceId = streamArray[0]; + String channelId = streamArray[1]; + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device == null) { + log.info("[语音对讲/喊话] 未找到设备:{}", deviceId); + return; + } + DeviceChannel channel = deviceChannelService.getOneForSource(deviceId, channelId); + if (channel == null) { + log.info("[语音对讲/喊话] 未找到通道:{}", channelId); + return; + } + if ("broadcast".equals(event.getApp())) { + stopAudioBroadcast(device, channel); + }else if ("talk".equals(event.getApp())) { + stopTalk(device, channel, false); + } + } + } + }else if ("rtp".equals(event.getApp())) { + // 释放ssrc + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, event.getStream()); + if (inviteInfo != null && inviteInfo.getStatus() == InviteSessionStatus.ok + && inviteInfo.getStreamInfo() != null && inviteInfo.getSsrcInfo() != null) { + // 发送bye + stop(inviteInfo); + } + + } + } + + /** + * 流未找到的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaNotFoundEvent event) { + if (!"rtp".equals(event.getApp())) { + return; + } + String[] s = event.getStream().split("_"); + if ((s.length != 2 && s.length != 4)) { + return; + } + String deviceId = s[0]; + String channelId = s[1]; + Device device = redisCatchStorage.getDevice(deviceId); + if (device == null || !device.isOnLine()) { + return; + } + DeviceChannel deviceChannel = deviceChannelService.getOne(deviceId, channelId); + if (deviceChannel == null) { + return; + } + if (s.length == 2) { + log.info("[ZLM HOOK] 预览流未找到, 发起自动点播:{}->{}->{}/{}", event.getMediaServer().getId(), event.getSchema(), event.getApp(), event.getStream()); + play(event.getMediaServer(), deviceId, channelId, null, (code, msg, data) -> {}); + } else if (s.length == 4) { + // 此时为录像回放, 录像回放格式为> 设备ID_通道ID_开始时间_结束时间 + String startTimeStr = s[2]; + String endTimeStr = s[3]; + if (startTimeStr == null || endTimeStr == null || startTimeStr.length() != 14 || endTimeStr.length() != 14) { + return; + } + String startTime = DateUtil.urlToyyyy_MM_dd_HH_mm_ss(startTimeStr); + String endTime = DateUtil.urlToyyyy_MM_dd_HH_mm_ss(endTimeStr); + log.info("[ZLM HOOK] 回放流未找到, 发起自动点播:{}->{}->{}/{}-{}-{}", + event.getMediaServer().getId(), event.getSchema(), + event.getApp(), event.getStream(), + startTime, endTime + ); + + playBack(event.getMediaServer(), device, deviceChannel, startTime, endTime, (code, msg, data) -> {}); + } + } + + @Override + public void play(Device device, DeviceChannel channel, ErrorCallback callback) { + + // 判断设备是否属于当前平台, 如果不属于则发起自动调用 + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcPlayService.play(device.getServerId(), channel.getId(), callback); + return; + } + MediaServer mediaServerItem = getNewMediaServerItem(device); + if (mediaServerItem == null) { + log.warn("[点播] 未找到可用的zlm deviceId: {},channelId:{}", device.getDeviceId(), channel.getDeviceId()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm"); + } + play(mediaServerItem, device, channel, null, userSetting.getRecordSip(), callback); + } + + @Override + public SSRCInfo play(MediaServer mediaServerItem, String deviceId, String channelId, String ssrc, ErrorCallback callback) { + if (mediaServerItem == null) { + log.warn("[点播] 未找到可用的zlm deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm"); + } + Device device = redisCatchStorage.getDevice(deviceId); + if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE") && !mediaServerItem.isRtpEnable()) { + log.warn("[点播] 单端口收流时不支持TCP主动方式收流 deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "单端口收流时不支持TCP主动方式收流"); + } + DeviceChannel channel = deviceChannelService.getOneForSource(deviceId, channelId); + if (channel == null) { + log.warn("[点播] 未找到通道 deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道"); + } + + return play(mediaServerItem, device, channel, ssrc, userSetting.getRecordSip(), callback); + } + + private SSRCInfo play(MediaServer mediaServerItem, Device device, DeviceChannel channel, String ssrc, Boolean record, + ErrorCallback callback) { + if (mediaServerItem == null ) { + if (callback != null) { + callback.run(InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getCode(), + InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getMsg(), + null); + } + return null; + } + + InviteInfo inviteInfoInCatch = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + if (inviteInfoInCatch != null ) { + if (inviteInfoInCatch.getStreamInfo() == null) { + // 释放生成的ssrc,使用上一次申请的 + + ssrcFactory.releaseSsrc(mediaServerItem.getId(), ssrc); + // 点播发起了但是尚未成功, 仅注册回调等待结果即可 + inviteStreamService.once(InviteSessionType.PLAY, channel.getId(), null, callback); + log.info("[点播开始] 已经请求中,等待结果, deviceId: {}, channelId({}): {}", device.getDeviceId(), channel.getDeviceId(), channel.getId()); + return inviteInfoInCatch.getSsrcInfo(); + }else { + StreamInfo streamInfo = inviteInfoInCatch.getStreamInfo(); + String streamId = streamInfo.getStream(); + if (streamId == null) { + callback.run(InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(), "点播失败, redis缓存streamId等于null", null); + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(), + "点播失败, redis缓存streamId等于null", + null); + return inviteInfoInCatch.getSsrcInfo(); + } + MediaServer mediaInfo = streamInfo.getMediaServer(); + Boolean ready = mediaServerService.isStreamReady(mediaInfo, "rtp", streamId); + if (ready != null && ready) { + if(callback != null) { + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + } + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + InviteErrorCode.SUCCESS.getCode(), + InviteErrorCode.SUCCESS.getMsg(), + streamInfo); + log.info("[点播已存在] 直接返回, 设备编号: {}, 通道编号: {}", device.getDeviceId(), channel.getDeviceId()); + return inviteInfoInCatch.getSsrcInfo(); + }else { + // 点播发起了但是尚未成功, 仅注册回调等待结果即可 + inviteStreamService.once(InviteSessionType.PLAY, channel.getId(), null, callback); + deviceChannelService.stopPlay(channel.getId()); + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + } + } + } + + String streamId = String.format("%s_%s", device.getDeviceId(), channel.getDeviceId()); + int tcpMode = device.getStreamMode().equals("TCP-ACTIVE")? 2: (device.getStreamMode().equals("TCP-PASSIVE")? 1:0); + RTPServerParam rtpServerParam = new RTPServerParam(); + rtpServerParam.setMediaServerItem(mediaServerItem); + rtpServerParam.setStreamId(streamId); + rtpServerParam.setPresetSsrc(ssrc); + rtpServerParam.setSsrcCheck(device.isSsrcCheck()); + rtpServerParam.setPlayback(false); + rtpServerParam.setPort(0); + rtpServerParam.setTcpMode(tcpMode); + rtpServerParam.setOnlyAuto(false); + rtpServerParam.setDisableAudio(!channel.isHasAudio()); + + SSRCInfo ssrcInfo = receiveRtpServerService.openRTPServer(rtpServerParam, (code, msg, result) -> { + + if (code == InviteErrorCode.SUCCESS.getCode() && result != null && result.getHookData() != null) { + // hook响应 + StreamInfo streamInfo = onPublishHandlerForPlay(result.getHookData().getMediaServer(), result.getHookData().getMediaInfo(), device, channel); + if (streamInfo == null){ + if (callback != null) { + callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null); + } + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null); + return; + } + if (callback != null) { + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + } + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + InviteErrorCode.SUCCESS.getCode(), + InviteErrorCode.SUCCESS.getMsg(), + streamInfo); + + log.info("[点播成功] 设备编号: {}, 通道编号:{}, 码流类型:{}", device.getDeviceId(), channel.getDeviceId(), + channel.getStreamIdentification()); + snapOnPlay(result.getHookData().getMediaServer(), device.getDeviceId(), channel.getDeviceId(), streamId); + }else { + if (callback != null) { + callback.run(code, msg, null); + } + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, code, msg, null); + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByStream("rtp", streamId); + if (ssrcTransaction != null) { + try { + cmder.streamByeCmd(device, channel.getDeviceId(),"rtp", streamId, null, null); + } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { + log.error("[点播超时], 发送BYE失败 {}", e.getMessage()); + } finally { + sessionManager.removeByStream("rtp", streamId); + } + } + } + }); + if (ssrcInfo == null || ssrcInfo.getPort() <= 0) { + log.info("[点播端口/SSRC]获取失败,设备编号:{}, 通道编号:{},ssrcInfo;{}", device.getDeviceId(), channel.getDeviceId(), ssrcInfo); + callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "获取端口或者ssrc失败", null); + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(), + null); + return null; + } + log.info("[点播开始] 设备编号: {}, 通道编号: {}, 收流端口: {}, 流ID:{}, 收流模式:{}, SSRC: {}, SSRC校验:{}", + device.getDeviceId(), channel.getDeviceId(), channel.getStreamIdentification(), ssrcInfo.getPort(), ssrcInfo.getStream(), + ssrcInfo.getSsrc(), device.isSsrcCheck()); + + // 初始化redis中的invite消息状态 + InviteInfo inviteInfo = InviteInfo.getInviteInfo(device.getDeviceId(), channel.getId(), ssrcInfo.getStream(), ssrcInfo, mediaServerItem.getId(), + mediaServerItem.getSdpIp(), ssrcInfo.getPort(), device.getStreamMode(), InviteSessionType.PLAY, + InviteSessionStatus.ready, userSetting.getRecordSip()); + if (record != null) { + inviteInfo.setRecord(record); + }else { + inviteInfo.setRecord(userSetting.getRecordSip()); + } + + inviteStreamService.updateInviteInfo(inviteInfo); + + try { + cmder.playStreamCmd(mediaServerItem, ssrcInfo, device, channel, (eventResult) -> { + // 处理收到200ok后的TCP主动连接以及SSRC不一致的问题 + InviteOKHandler(eventResult, ssrcInfo, mediaServerItem, device, channel, callback, inviteInfo, InviteSessionType.PLAY); + }, (event) -> { + log.info("[点播失败]{}:{} deviceId: {}, channelId:{}",event.statusCode, event.msg, device.getDeviceId(), channel.getDeviceId()); + receiveRtpServerService.closeRTPServer(mediaServerItem, ssrcInfo); + + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + if (callback != null) { + callback.run(event.statusCode, event.msg, null); + } + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + event.statusCode, event.msg, null); + + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + }, userSetting.getPlayTimeout().longValue()); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 点播消息: {}", e.getMessage()); + receiveRtpServerService.closeRTPServer(mediaServerItem, ssrcInfo); + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + if (callback != null) { + callback.run(InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(), + InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null); + } + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(), + InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null); + + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + } + return ssrcInfo; + } + + + private void talk(MediaServer mediaServerItem, Device device, DeviceChannel channel, String stream, + HookSubscribe.Event hookEvent, SipSubscribe.Event errorEvent, + Runnable timeoutCallback, AudioBroadcastEvent audioEvent) { + + String playSsrc = ssrcFactory.getPlaySsrc(mediaServerItem.getId()); + + if (playSsrc == null) { + audioEvent.call("ssrc已经用尽"); + return; + } + SendRtpInfo sendRtpInfo; + try { + sendRtpInfo = sendRtpServerService.createSendRtpInfo(mediaServerItem, null, null, playSsrc, device.getDeviceId(), "talk", stream, + channel.getId(), true, false); + sendRtpInfo.setPlayType(InviteStreamType.TALK); + }catch (PlayException e) { + log.info("[语音对讲]开始 获取发流端口失败 deviceId: {}, channelId: {},", device.getDeviceId(), channel.getDeviceId()); + return; + } + + sendRtpInfo.setOnlyAudio(true); + sendRtpInfo.setPt(8); + sendRtpInfo.setStatus(1); + sendRtpInfo.setTcpActive(false); + sendRtpInfo.setUsePs(false); + sendRtpInfo.setReceiveStream(stream + "_talk"); + + String callId = SipUtils.getNewCallId(); + log.info("[语音对讲]开始 deviceId: {}, channelId: {},收流端口: {}, 收流模式:{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channel.getDeviceId(), sendRtpInfo.getLocalPort(), device.getStreamMode(), sendRtpInfo.getSsrc(), false); + // 超时处理 + String timeOutTaskKey = UUID.randomUUID().toString(); + dynamicTask.startDelay(timeOutTaskKey, () -> { + + log.info("[语音对讲] 收流超时 deviceId: {}, channelId: {},端口:{}, SSRC: {}", device.getDeviceId(), channel.getDeviceId(), sendRtpInfo.getPort(), sendRtpInfo.getSsrc()); + timeoutCallback.run(); + // 点播超时回复BYE 同时释放ssrc以及此次点播的资源 + try { + cmder.streamByeCmd(device, channel.getDeviceId(), null, null, callId, null); + } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { + log.error("[语音对讲]超时, 发送BYE失败 {}", e.getMessage()); + } finally { + timeoutCallback.run(); + mediaServerService.releaseSsrc(mediaServerItem.getId(), sendRtpInfo.getSsrc()); + sessionManager.removeByStream(sendRtpInfo.getApp(), sendRtpInfo.getStream()); + } + }, userSetting.getPlayTimeout()); + + try { + Integer localPort = mediaServerService.startSendRtpPassive(mediaServerItem, sendRtpInfo, userSetting.getPlayTimeout() * 1000); + if (localPort == null || localPort <= 0) { + timeoutCallback.run(); + mediaServerService.releaseSsrc(mediaServerItem.getId(), sendRtpInfo.getSsrc()); + sessionManager.removeByStream(sendRtpInfo.getApp(), sendRtpInfo.getStream()); + return; + } + sendRtpInfo.setPort(localPort); + }catch (ControllerException e) { + mediaServerService.releaseSsrc(mediaServerItem.getId(), sendRtpInfo.getSsrc()); + log.info("[语音对讲]失败 deviceId: {}, channelId: {}", device.getDeviceId(), channel.getDeviceId()); + audioEvent.call("失败, " + e.getMessage()); + // 查看是否已经建立了通道,存在则发送bye + stopTalk(device, channel); + } + + + // 查看设备是否已经在推流 + try { + cmder.talkStreamCmd(mediaServerItem, sendRtpInfo, device, channel, callId, (hookData) -> { + log.info("[语音对讲] 流已生成, 开始推流: " + hookData); + dynamicTask.stop(timeOutTaskKey); + // TODO 暂不做处理 + }, (hookData) -> { + log.info("[语音对讲] 设备开始推流: " + hookData); + dynamicTask.stop(timeOutTaskKey); + + }, (event) -> { + dynamicTask.stop(timeOutTaskKey); + + if (event.event instanceof ResponseEvent) { + ResponseEvent responseEvent = (ResponseEvent) event.event; + if (responseEvent.getResponse() instanceof SIPResponse) { + SIPResponse response = (SIPResponse) responseEvent.getResponse(); + sendRtpInfo.setFromTag(response.getFromTag()); + sendRtpInfo.setToTag(response.getToTag()); + sendRtpInfo.setCallId(response.getCallIdHeader().getCallId()); + sendRtpServerService.update(sendRtpInfo); + + SsrcTransaction ssrcTransaction = SsrcTransaction.buildForDevice(device.getDeviceId(), sendRtpInfo.getChannelId(), response.getCallIdHeader().getCallId(), sendRtpInfo.getApp(), + sendRtpInfo.getStream(), sendRtpInfo.getSsrc(), sendRtpInfo.getMediaServerId(), + response, InviteSessionType.TALK); + + sessionManager.put(ssrcTransaction); + } else { + log.error("[语音对讲]收到的消息错误,response不是SIPResponse"); + } + } else { + log.error("[语音对讲]收到的消息错误,event不是ResponseEvent"); + } + + }, (event) -> { + dynamicTask.stop(timeOutTaskKey); + mediaServerService.closeRTPServer(mediaServerItem, sendRtpInfo.getStream()); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), sendRtpInfo.getSsrc()); + sessionManager.removeByStream(sendRtpInfo.getApp(), sendRtpInfo.getStream()); + errorEvent.response(event); + }, userSetting.getPlayTimeout().longValue()); + } catch (InvalidArgumentException | SipException | ParseException e) { + + log.error("[命令发送失败] 对讲消息: {}", e.getMessage()); + dynamicTask.stop(timeOutTaskKey); + mediaServerService.closeRTPServer(mediaServerItem, sendRtpInfo.getStream()); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), sendRtpInfo.getSsrc()); + + sessionManager.removeByStream(sendRtpInfo.getApp(), sendRtpInfo.getStream()); + SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(); + eventResult.type = SipSubscribe.EventResultType.cmdSendFailEvent; + eventResult.statusCode = -1; + eventResult.msg = "命令发送失败"; + errorEvent.response(eventResult); + } +// } + + } + + private void tcpActiveHandler(Device device, DeviceChannel channel, String contentString, + MediaServer mediaServerItem, SSRCInfo ssrcInfo, ErrorCallback callback){ + if (!device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) { + return; + } + + String substring; + if (contentString.indexOf("y=") > 0) { + substring = contentString.substring(0, contentString.indexOf("y=")); + }else { + substring = contentString; + } + try { + SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring); + int port = -1; + Vector mediaDescriptions = sdp.getMediaDescriptions(true); + for (Object description : mediaDescriptions) { + MediaDescription mediaDescription = (MediaDescription) description; + Media media = mediaDescription.getMedia(); + + Vector mediaFormats = media.getMediaFormats(false); + if (mediaFormats.contains("96")) { + port = media.getMediaPort(); + break; + } + } + log.info("[TCP主动连接对方] deviceId: {}, channelId: {}, 连接对方的地址:{}:{}, 收流模式:{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channel.getDeviceId(), sdp.getConnection().getAddress(), port, device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck()); + Boolean result = mediaServerService.connectRtpServer(mediaServerItem, sdp.getConnection().getAddress(), port, ssrcInfo.getStream()); + log.info("[TCP主动连接对方] 结果: {}" , result); + if (!result) { + // 主动连接失败,结束流程, 清理数据 + receiveRtpServerService.closeRTPServer(mediaServerItem, ssrcInfo); + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + callback.run(InviteErrorCode.ERROR_FOR_TCP_ACTIVE_CONNECTION_REFUSED_ERROR.getCode(), + InviteErrorCode.ERROR_FOR_TCP_ACTIVE_CONNECTION_REFUSED_ERROR.getMsg(), null); + inviteStreamService.call(InviteSessionType.BROADCAST, channel.getId(), null, + InviteErrorCode.ERROR_FOR_TCP_ACTIVE_CONNECTION_REFUSED_ERROR.getCode(), + InviteErrorCode.ERROR_FOR_TCP_ACTIVE_CONNECTION_REFUSED_ERROR.getMsg(), null); + } + } catch (SdpException e) { + log.error("[TCP主动连接对方] deviceId: {}, channelId: {}, 解析200OK的SDP信息失败", device.getDeviceId(), channel.getDeviceId(), e); + receiveRtpServerService.closeRTPServer(mediaServerItem, ssrcInfo); + + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + + callback.run(InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null); + inviteStreamService.call(InviteSessionType.BROADCAST, channel.getId(), null, + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null); + } + } + + /** + * 点播成功时调用截图. + * + * @param mediaServerItemInuse media + * @param deviceId 设备 ID + * @param channelId 通道 ID + * @param stream ssrc + */ + private void snapOnPlay(MediaServer mediaServerItemInuse, String deviceId, String channelId, String stream) { + String path = "snap"; + String fileName = deviceId + "_" + channelId + ".jpg"; + // 请求截图 + log.info("[请求截图]: " + fileName); + mediaServerService.getSnap(mediaServerItemInuse, "rtp", stream, 15, 1, path, fileName); + } + + public StreamInfo onPublishHandlerForPlay(MediaServer mediaServerItem, MediaInfo mediaInfo, Device device, DeviceChannel channel) { + StreamInfo streamInfo = null; + streamInfo = onPublishHandler(mediaServerItem, mediaInfo, device, channel); + if (streamInfo != null) { + deviceChannelService.startPlay(channel.getId(), streamInfo.getStream()); + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + if (inviteInfo != null) { + inviteInfo.setStatus(InviteSessionStatus.ok); + inviteInfo.setStreamInfo(streamInfo); + inviteStreamService.updateInviteInfo(inviteInfo); + } + } + return streamInfo; + + } + + private StreamInfo onPublishHandlerForPlayback(MediaServer mediaServerItem, MediaInfo mediaInfo, Device device, + DeviceChannel channel, String startTime, String endTime) { + StreamInfo streamInfo = onPublishHandler(mediaServerItem, mediaInfo, device, channel); + if (streamInfo != null) { + streamInfo.setStartTime(startTime); + streamInfo.setEndTime(endTime); + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(InviteSessionType.PLAYBACK, mediaInfo.getStream()); + if (inviteInfo != null) { + inviteInfo.setStatus(InviteSessionStatus.ok); + inviteInfo.setStreamInfo(streamInfo); + inviteStreamService.updateInviteInfo(inviteInfo); + } + } + return streamInfo; + } + + @Override + public MediaServer getNewMediaServerItem(Device device) { + if (device == null) { + return null; + } + MediaServer mediaServerItem; + if (ObjectUtils.isEmpty(device.getMediaServerId()) || "auto".equals(device.getMediaServerId())) { + mediaServerItem = mediaServerService.getMediaServerForMinimumLoad(null); + } else { + mediaServerItem = mediaServerService.getOne(device.getMediaServerId()); + } + if (mediaServerItem == null) { + log.warn("点播时未找到可使用的ZLM..."); + } + return mediaServerItem; + } + + @Override + public void playBack(Device device, DeviceChannel channel, String startTime, + String endTime, ErrorCallback callback) { + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备不存在"); + } + if (channel == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道不存在"); + } + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcPlayService.playback(device.getServerId(), channel.getId(), startTime, endTime, callback); + return; + } + + MediaServer newMediaServerItem = getNewMediaServerItem(device); + if (newMediaServerItem == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的节点"); + } + if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE") && ! newMediaServerItem.isRtpEnable()) { + log.warn("[录像回放] 单端口收流时不支持TCP主动方式收流 deviceId: {},channelId:{}", device.getDeviceId(), channel.getDeviceId()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "单端口收流时不支持TCP主动方式收流"); + } + + playBack(newMediaServerItem, device, channel, startTime, endTime, callback); + } + + private void playBack(MediaServer mediaServerItem, + Device device, DeviceChannel channel, String startTime, + String endTime, ErrorCallback callback) { + + String startTimeStr = startTime.replace("-", "") + .replace(":", "") + .replace(" ", ""); + String endTimeTimeStr = endTime.replace("-", "") + .replace(":", "") + .replace(" ", ""); + + String stream = device.getDeviceId() + "_" + channel.getDeviceId() + "_" + startTimeStr + "_" + endTimeTimeStr; + int tcpMode = device.getStreamMode().equals("TCP-ACTIVE")? 2: (device.getStreamMode().equals("TCP-PASSIVE")? 1:0); + + RTPServerParam rtpServerParam = new RTPServerParam(); + rtpServerParam.setMediaServerItem(mediaServerItem); + rtpServerParam.setStreamId(stream); + rtpServerParam.setSsrcCheck(device.isSsrcCheck()); + rtpServerParam.setPlayback(true); + rtpServerParam.setPort(0); + rtpServerParam.setTcpMode(tcpMode); + rtpServerParam.setOnlyAuto(false); + rtpServerParam.setDisableAudio(!channel.isHasAudio()); + SSRCInfo ssrcInfo = receiveRtpServerService.openRTPServer(rtpServerParam, (code, msg, result) -> { + if (code == InviteErrorCode.SUCCESS.getCode() && result != null && result.getHookData() != null) { + // hook响应 + StreamInfo streamInfo = onPublishHandlerForPlayback(result.getHookData().getMediaServer(), result.getHookData().getMediaInfo(), device, channel, startTime, endTime); + if (streamInfo == null) { + log.warn("设备回放API调用失败!"); + callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null); + return; + } + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + log.info("[录像回放] 成功 deviceId: {}, channelId: {}, 开始时间: {}, 结束时间: {}", device.getDeviceId(), channel.getGbDeviceId(), startTime, endTime); + }else { + if (callback != null) { + callback.run(code, msg, null); + } + inviteStreamService.call(InviteSessionType.PLAYBACK, channel.getId(), null, code, msg, null); + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAYBACK, channel.getId()); + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByStream("rtp", stream); + if (ssrcTransaction != null) { + try { + cmder.streamByeCmd(device, channel.getDeviceId(),"rtp", stream, null, null); + } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { + log.error("[录像回放] 发送BYE失败 {}", e.getMessage()); + } finally { + sessionManager.removeByStream("rtp", stream); + } + } + } + }); + if (ssrcInfo == null || ssrcInfo.getPort() <= 0) { + log.info("[回放端口/SSRC]获取失败,deviceId={},channelId={},ssrcInfo={}", device.getDeviceId(), channel.getDeviceId(), ssrcInfo); + if (callback != null) { + callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "获取端口或者ssrc失败", null); + } + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(), + null); + return; + } + + log.info("[录像回放] deviceId: {}, channelId: {}, 开始时间: {}, 结束时间: {}, 收流端口:{}, 收流模式:{}, SSRC: {}, SSRC校验:{}", + device.getDeviceId(), channel.getGbDeviceId(), startTime, endTime, ssrcInfo.getPort(), device.getStreamMode(), + ssrcInfo.getSsrc(), device.isSsrcCheck()); + // 初始化redis中的invite消息状态 + InviteInfo inviteInfo = InviteInfo.getInviteInfo(device.getDeviceId(), channel.getId(), ssrcInfo.getStream(), ssrcInfo, mediaServerItem.getId(), + mediaServerItem.getSdpIp(), ssrcInfo.getPort(), device.getStreamMode(), InviteSessionType.PLAYBACK, + InviteSessionStatus.ready, userSetting.getRecordSip()); + inviteStreamService.updateInviteInfo(inviteInfo); + + try { + cmder.playbackStreamCmd(mediaServerItem, ssrcInfo, device, channel, startTime, endTime, + eventResult -> { + // 处理收到200ok后的TCP主动连接以及SSRC不一致的问题 + InviteOKHandler(eventResult, ssrcInfo, mediaServerItem, device, channel, + callback, inviteInfo, InviteSessionType.PLAYBACK); + }, eventResult -> { + log.info("[录像回放] 失败,{} {}", eventResult.statusCode, eventResult.msg); + if (callback != null) { + callback.run(eventResult.statusCode, eventResult.msg, null); + } + + receiveRtpServerService.closeRTPServer(mediaServerItem, ssrcInfo); + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + inviteStreamService.removeInviteInfo(inviteInfo); + }, userSetting.getPlayTimeout().longValue()); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 录像回放: {}", e.getMessage()); + if (callback != null) { + callback.run(InviteErrorCode.FAIL.getCode(), e.getMessage(), null); + } + receiveRtpServerService.closeRTPServer(mediaServerItem, ssrcInfo); + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + inviteStreamService.removeInviteInfo(inviteInfo); + } + } + + + private void InviteOKHandler(SipSubscribe.EventResult eventResult, SSRCInfo ssrcInfo, MediaServer mediaServerItem, + Device device, DeviceChannel channel, ErrorCallback callback, + InviteInfo inviteInfo, InviteSessionType inviteSessionType){ + inviteInfo.setStatus(InviteSessionStatus.ok); + ResponseEvent responseEvent = (ResponseEvent) eventResult.event; + String contentString = new String(responseEvent.getResponse().getRawContent()); + String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString); + // 兼容回复的消息中缺少ssrc(y字段)的情况 + if (ssrcInResponse == null) { + ssrcInResponse = ssrcInfo.getSsrc(); + } + if (ssrcInfo.getSsrc().equals(ssrcInResponse)) { + // ssrc 一致 + if (mediaServerItem.isRtpEnable()) { + // 多端口 + if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) { + tcpActiveHandler(device, channel, contentString, mediaServerItem, ssrcInfo, callback); + } + }else { + // 单端口 + if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) { + log.warn("[Invite 200OK] 单端口收流模式不支持tcp主动模式收流"); + } + + } + }else { + log.info("[Invite 200OK] 收到invite 200, 发现下级自定义了ssrc: {}", ssrcInResponse); + // ssrc 不一致 + if (mediaServerItem.isRtpEnable()) { + // 多端口 + if (device.isSsrcCheck()) { + // ssrc检验 + // 更新ssrc + log.info("[Invite 200OK] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + Boolean result = mediaServerService.updateRtpServerSSRC(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse); + if (!result) { + try { + log.warn("[Invite 200OK] 更新ssrc失败,停止点播 {}/{}", device.getDeviceId(), channel.getDeviceId()); + cmder.streamByeCmd(device, channel.getDeviceId(), ssrcInfo.getApp(), ssrcInfo.getStream(), null, null); + } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { + log.error("[命令发送失败] 停止播放, 发送BYE: {}", e.getMessage()); + } + + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + + callback.run(InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(), + "下级自定义了ssrc,重新设置收流信息失败", null); + inviteStreamService.call(inviteSessionType, channel.getId(), null, + InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(), + "下级自定义了ssrc,重新设置收流信息失败", null); + + }else { + ssrcInfo.setSsrc(ssrcInResponse); + inviteInfo.setSsrcInfo(ssrcInfo); + inviteInfo.setStream(ssrcInfo.getStream()); + if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) { + if (mediaServerItem.isRtpEnable()) { + tcpActiveHandler(device, channel, contentString, mediaServerItem, ssrcInfo, callback); + }else { + log.warn("[Invite 200OK] 单端口收流模式不支持tcp主动模式收流"); + } + } + inviteStreamService.updateInviteInfo(inviteInfo); + } + } + }else { + if (ssrcInResponse != null) { + // 单端口 + // 重新订阅流上线 + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByStream("rtp", inviteInfo.getStream()); + sessionManager.removeByStream("rtp", inviteInfo.getStream()); + inviteStreamService.updateInviteInfoForSSRC(inviteInfo, ssrcInResponse); + ssrcTransaction.setDeviceId(device.getDeviceId()); + ssrcTransaction.setChannelId(ssrcTransaction.getChannelId()); + ssrcTransaction.setCallId(ssrcTransaction.getCallId()); + ssrcTransaction.setSsrc(ssrcInResponse); + ssrcTransaction.setApp("rtp"); + ssrcTransaction.setStream(inviteInfo.getStream()); + ssrcTransaction.setMediaServerId(mediaServerItem.getId()); + ssrcTransaction.setSipTransactionInfo(new SipTransactionInfo((SIPResponse) responseEvent.getResponse())); + ssrcTransaction.setType(inviteSessionType); + + sessionManager.put(ssrcTransaction); + } + } + } + } + + @Override + public void download(Device device, DeviceChannel channel, String startTime, String endTime, int downloadSpeed, ErrorCallback callback) { + + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcPlayService.download(device.getServerId(), channel.getId(), startTime, endTime, downloadSpeed, callback); + return; + } + + MediaServer newMediaServerItem = this.getNewMediaServerItem(device); + if (newMediaServerItem == null) { + callback.run(InviteErrorCode.ERROR_FOR_ASSIST_NOT_READY.getCode(), + InviteErrorCode.ERROR_FOR_ASSIST_NOT_READY.getMsg(), + null); + return; + } + + download(newMediaServerItem, device, channel, startTime, endTime, downloadSpeed, callback); + } + + + private void download(MediaServer mediaServer, Device device, DeviceChannel channel, String startTime, String endTime, int downloadSpeed, ErrorCallback callback) { + if (mediaServer == null ) { + callback.run(InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getCode(), + InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getMsg(), + null); + return; + } + + int tcpMode = device.getStreamMode().equals("TCP-ACTIVE")? 2: (device.getStreamMode().equals("TCP-PASSIVE")? 1:0); + // 录像下载不使用固定流地址,固定流地址会导致如果开始时间与结束时间一致时文件错误的叠加在一起 + RTPServerParam rtpServerParam = new RTPServerParam(); + rtpServerParam.setMediaServerItem(mediaServer); + rtpServerParam.setSsrcCheck(device.isSsrcCheck()); + rtpServerParam.setPlayback(true); + rtpServerParam.setPort(0); + rtpServerParam.setTcpMode(tcpMode); + rtpServerParam.setOnlyAuto(false); + rtpServerParam.setDisableAudio(!channel.isHasAudio()); + SSRCInfo ssrcInfo = receiveRtpServerService.openRTPServer(rtpServerParam, (code, msg, result) -> { + if (code == InviteErrorCode.SUCCESS.getCode() && result != null && result.getHookData() != null) { + // hook响应 + StreamInfo streamInfo = onPublishHandlerForDownload(mediaServer, result.getHookData().getMediaInfo(), device, channel, startTime, endTime); + if (streamInfo == null) { + log.warn("[录像下载] 获取流地址信息失败"); + callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null); + return; + } + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + log.info("[录像下载] 调用成功 deviceId: {}, channelId: {}, 开始时间: {}, 结束时间: {}", device.getDeviceId(), channel.getDeviceId(), startTime, endTime); + }else { + if (callback != null) { + callback.run(code, msg, null); + } + inviteStreamService.call(InviteSessionType.DOWNLOAD, channel.getId(), null, code, msg, null); + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.DOWNLOAD, channel.getId()); + if (result != null && result.getSsrcInfo() != null) { + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByStream(result.getSsrcInfo().getApp(), result.getSsrcInfo().getStream()); + if (ssrcTransaction != null) { + try { + cmder.streamByeCmd(device, channel.getDeviceId(), ssrcTransaction.getApp(), ssrcTransaction.getStream(), null, null); + } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { + log.error("[录像下载] 发送BYE失败 {}", e.getMessage()); + } finally { + sessionManager.removeByStream(ssrcTransaction.getApp(), ssrcTransaction.getStream()); + } + } + } + } + }); + if (ssrcInfo == null || ssrcInfo.getPort() <= 0) { + log.info("[录像下载端口/SSRC]获取失败,deviceId={},channelId={},ssrcInfo={}", device.getDeviceId(), channel.getDeviceId(), ssrcInfo); + if (callback != null) { + callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "获取端口或者ssrc失败", null); + } + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(), + null); + return; + } + log.info("[录像下载] deviceId: {}, channelId: {}, 开始时间: {}, 结束时间: {}, 下载速度:{}, 收流端口:{}, 收流模式:{}, SSRC: {}({}), SSRC校验:{}", + device.getDeviceId(), channel.getDeviceId(), startTime, endTime, downloadSpeed, ssrcInfo.getPort(), device.getStreamMode(), + ssrcInfo.getSsrc(), String.format("%08x", Long.parseLong(ssrcInfo.getSsrc())).toUpperCase(), + device.isSsrcCheck()); + + // 初始化redis中的invite消息状态 + InviteInfo inviteInfo = InviteInfo.getInviteInfo(device.getDeviceId(), channel.getId(), ssrcInfo.getStream(), ssrcInfo, mediaServer.getId(), + mediaServer.getSdpIp(), ssrcInfo.getPort(), device.getStreamMode(), InviteSessionType.DOWNLOAD, + InviteSessionStatus.ready, true); + inviteInfo.setStartTime(startTime); + inviteInfo.setEndTime(endTime); + + inviteStreamService.updateInviteInfo(inviteInfo); + try { + cmder.downloadStreamCmd(mediaServer, ssrcInfo, device, channel, startTime, endTime, downloadSpeed, + eventResult -> { + // 对方返回错误 + callback.run(InviteErrorCode.FAIL.getCode(), String.format("录像下载失败, 错误码: %s, %s", eventResult.statusCode, eventResult.msg), null); + receiveRtpServerService.closeRTPServer(mediaServer, ssrcInfo); + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + inviteStreamService.removeInviteInfo(inviteInfo); + }, eventResult ->{ + // 处理收到200ok后的TCP主动连接以及SSRC不一致的问题 + InviteOKHandler(eventResult, ssrcInfo, mediaServer, device, channel, + callback, inviteInfo, InviteSessionType.DOWNLOAD); + + // 注册录像回调事件,录像下载结束后写入下载地址 + HookSubscribe.Event hookEventForRecord = (hookData) -> { + log.info("[录像下载] 收到录像写入磁盘消息: , {}/{}-{}", + inviteInfo.getDeviceId(), inviteInfo.getChannelId(), ssrcInfo.getStream()); + log.info("[录像下载] 收到录像写入磁盘消息内容: " + hookData); + RecordInfo recordInfo = hookData.getRecordInfo(); + DownloadFileInfo downloadFileInfo = mediaServerService.getDownloadFilePath(mediaServer, recordInfo); + InviteInfo inviteInfoForNew = inviteStreamService.getInviteInfo(inviteInfo.getType() + , inviteInfo.getChannelId(), inviteInfo.getStream()); + if (inviteInfoForNew != null && inviteInfoForNew.getStreamInfo() != null) { + inviteInfoForNew.getStreamInfo().setDownLoadFilePath(downloadFileInfo); + // 不可以马上移除会导致后续接口拿不到下载地址 + inviteStreamService.updateInviteInfo(inviteInfoForNew, 60*15L); + } + }; + Hook hook = Hook.getInstance(HookType.on_record_mp4, "rtp", ssrcInfo.getStream(), mediaServer.getId()); + // 设置过期时间,下载失败时自动处理订阅数据 + hook.setExpireTime(System.currentTimeMillis() + 24 * 60 * 60 * 1000); + subscribe.addSubscribe(hook, hookEventForRecord); + }, userSetting.getPlayTimeout().longValue()); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 录像下载: {}", e.getMessage()); + callback.run(InviteErrorCode.FAIL.getCode(),e.getMessage(), null); + receiveRtpServerService.closeRTPServer(mediaServer, ssrcInfo); + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + inviteStreamService.removeInviteInfo(inviteInfo); + } + } + + @Override + public StreamInfo getDownLoadInfo(Device device, DeviceChannel channel, String stream) { + + + InviteInfo inviteInfo = inviteStreamService.getInviteInfo(InviteSessionType.DOWNLOAD, channel.getId(), stream); + if (inviteInfo == null) { + String app = "rtp"; + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(app, stream); + if (streamAuthorityInfo != null) { + List allList = cloudRecordService.getAllList(null, app, stream, null, null, null, streamAuthorityInfo.getCallId(), null); + if (allList.isEmpty()) { + log.warn("[获取下载进度] 未查询到录像下载的信息 {}/{}-{}", device.getDeviceId(), channel.getDeviceId(), stream); + return null; + } + + String mediaServerId = allList.get(0).getMediaServerId(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + log.warn("[获取下载进度] 未查询到录像下载的节点信息 {}/{}-{}", device.getDeviceId(), channel.getDeviceId(), stream); + return null; + } + log.warn("[获取下载进度] 发现下载已经结束,直接从数据库获取到文件 {}/{}-{}", device.getDeviceId(), channel.getDeviceId(), stream); + DownloadFileInfo downloadFileInfo = mediaServerService.getDownloadFilePath(mediaServer, RecordInfo.getInstance(allList.get(0))); + StreamInfo streamInfo = new StreamInfo(); + streamInfo.setDownLoadFilePath(downloadFileInfo); + streamInfo.setApp(app); + streamInfo.setStream(stream); + streamInfo.setServerId(mediaServerId); + streamInfo.setProgress(1.0); + return streamInfo; + } + } + + if (inviteInfo == null || inviteInfo.getStreamInfo() == null) { + log.warn("[获取下载进度] 未查询到录像下载的信息 {}/{}-{}", device.getDeviceId(), channel.getDeviceId(), stream); + return null; + } + + if (inviteInfo.getStreamInfo().getProgress() == 1) { + return inviteInfo.getStreamInfo(); + } + + // 获取当前已下载时长 + MediaServer mediaServerItem = inviteInfo.getStreamInfo().getMediaServer(); + if (mediaServerItem == null) { + log.warn("[获取下载进度] 查询录像信息时发现节点不存在"); + return null; + } + String app = "rtp"; + Long duration = mediaServerService.updateDownloadProcess(mediaServerItem, app, stream); + if (duration == null || duration == 0) { + inviteInfo.getStreamInfo().setProgress(0); + } else { + String startTime = inviteInfo.getStreamInfo().getStartTime(); + String endTime = inviteInfo.getStreamInfo().getEndTime(); + // 此时start和end单位是秒 + long start = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime); + long end = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime); + + BigDecimal currentCount = new BigDecimal(duration); + BigDecimal totalCount = new BigDecimal((end - start) * 1000); + BigDecimal divide = currentCount.divide(totalCount, 2, RoundingMode.HALF_UP); + double process = divide.doubleValue(); + if (process > 0.999) { + process = 1.0; + } + inviteInfo.getStreamInfo().setProgress(process); + } + inviteStreamService.updateInviteInfo(inviteInfo); + return inviteInfo.getStreamInfo(); + } + + private StreamInfo onPublishHandlerForDownload(MediaServer mediaServerItemInuse, MediaInfo mediaInfo, Device device, DeviceChannel channel, String startTime, String endTime) { + StreamInfo streamInfo = onPublishHandler(mediaServerItemInuse, mediaInfo, device, channel); + if (streamInfo != null) { + streamInfo.setProgress(0); + streamInfo.setStartTime(startTime); + streamInfo.setEndTime(endTime); + InviteInfo inviteInfo = inviteStreamService.getInviteInfo(InviteSessionType.DOWNLOAD, channel.getId(), streamInfo.getStream()); + if (inviteInfo != null) { + log.info("[录像下载] 更新invite消息中的stream信息"); + inviteInfo.setStatus(InviteSessionStatus.ok); + inviteInfo.setStreamInfo(streamInfo); + inviteStreamService.updateInviteInfo(inviteInfo); + } + } + return streamInfo; + } + + + public StreamInfo onPublishHandler(MediaServer mediaServerItem, MediaInfo mediaInfo, Device device, DeviceChannel channel) { + StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServerItem, "rtp", mediaInfo.getStream(), mediaInfo, null); + streamInfo.setDeviceId(device.getDeviceId()); + streamInfo.setChannelId(channel.getId()); + return streamInfo; + } + + + @Override + public void zlmServerOffline(MediaServer mediaServer) { + // 处理正在向上推流的上级平台 + List sendRtpInfos = sendRtpServerService.queryAll(); + if (!sendRtpInfos.isEmpty()) { + for (SendRtpInfo sendRtpInfo : sendRtpInfos) { + if (sendRtpInfo.getMediaServerId().equals(mediaServer.getId()) && sendRtpInfo.isSendToPlatform()) { + Platform platform = platformService.queryPlatformByServerGBId(sendRtpInfo.getTargetId()); + CommonGBChannel channel = channelService.getOne(sendRtpInfo.getChannelId()); + try { + sipCommanderFroPlatform.streamByeCmd(platform, sendRtpInfo, channel); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage()); + } + } + } + } + // 处理正在观看的国标设备 + List allSsrc = sessionManager.getAll(); + if (allSsrc.size() > 0) { + for (SsrcTransaction ssrcTransaction : allSsrc) { + if (ssrcTransaction.getMediaServerId().equals(mediaServer.getId())) { + Device device = deviceService.getDeviceByDeviceId(ssrcTransaction.getDeviceId()); + if (device == null) { + continue; + } + DeviceChannel deviceChannel = deviceChannelService.getOneById(ssrcTransaction.getChannelId()); + if (deviceChannel == null) { + continue; + } + try { + cmder.streamByeCmd(device, deviceChannel.getDeviceId(), ssrcTransaction.getApp(), + ssrcTransaction.getStream(), null, null); + } catch (InvalidArgumentException | ParseException | SipException | + SsrcTransactionNotFoundException e) { + log.error("[zlm离线]为正在使用此zlm的设备, 发送BYE失败 {}", e.getMessage()); + } + } + } + } + } + + @Override + public AudioBroadcastResult audioBroadcast(String deviceId, String channelDeviceId, Boolean broadcastMode) { + + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "未找到设备: " + deviceId); + } + DeviceChannel deviceChannel = deviceChannelService.getOne(deviceId, channelDeviceId); + if (deviceChannel == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "未找到通道: " + channelDeviceId); + } + + if (!userSetting.getServerId().equals(device.getServerId())) { + return redisRpcPlayService.audioBroadcast(device.getServerId(), deviceId, channelDeviceId, broadcastMode); + } + log.info("[语音喊话] device: {}, channel: {}", device.getDeviceId(), deviceChannel.getDeviceId()); + MediaServer mediaServerItem = mediaServerService.getMediaServerForMinimumLoad(null); + if (broadcastMode == null) { + broadcastMode = true; + } + String app = broadcastMode?"broadcast":"talk"; + String stream = device.getDeviceId() + "_" + deviceChannel.getDeviceId(); + AudioBroadcastResult audioBroadcastResult = new AudioBroadcastResult(); + audioBroadcastResult.setApp(app); + audioBroadcastResult.setStream(stream); + audioBroadcastResult.setStreamInfo(new StreamContent(mediaServerService.getStreamInfoByAppAndStream(mediaServerItem, app, stream, null, null, null, false))); + audioBroadcastResult.setCodec("G.711"); + return audioBroadcastResult; + } + + @Override + public boolean audioBroadcastCmd(Device device, DeviceChannel deviceChannel, MediaServer mediaServerItem, String app, String stream, int timeout, boolean isFromPlatform, AudioBroadcastEvent event) throws InvalidArgumentException, ParseException, SipException { + Assert.notNull(device, "设备不存在"); + Assert.notNull(deviceChannel, "通道不存在"); + log.info("[语音喊话] device: {}, channel: {}", device.getDeviceId(), deviceChannel.getDeviceId()); + // 查询通道使用状态 + if (audioBroadcastManager.exit(deviceChannel.getId())) { + SendRtpInfo sendRtpInfo = sendRtpServerService.queryByChannelId(deviceChannel.getId(), device.getDeviceId()); + if (sendRtpInfo != null && sendRtpInfo.isOnlyAudio()) { + // 查询流是否存在,不存在则认为是异常状态 + Boolean streamReady = mediaServerService.isStreamReady(mediaServerItem, sendRtpInfo.getApp(), sendRtpInfo.getStream()); + if (streamReady) { + log.warn("语音广播已经开启: {}", deviceChannel.getDeviceId()); + event.call("语音广播已经开启"); + return false; + } else { + stopAudioBroadcast(device, deviceChannel); + } + } + } + + // 发送通知 + cmder.audioBroadcastCmd(device, deviceChannel.getDeviceId(), eventResultForOk -> { + // 发送成功 + AudioBroadcastCatch audioBroadcastCatch = new AudioBroadcastCatch(device.getDeviceId(), deviceChannel.getId(), mediaServerItem, app, stream, event, AudioBroadcastCatchStatus.Ready, isFromPlatform); + audioBroadcastManager.update(audioBroadcastCatch); + // 等待invite消息, 超时则结束 + String key = VideoManagerConstants.BROADCAST_WAITE_INVITE + device.getDeviceId(); + if (!SipUtils.isFrontEnd(device.getDeviceId())) { + key += audioBroadcastCatch.getChannelId(); + } + dynamicTask.startDelay(key, ()->{ + log.info("[语音广播]等待invite消息超时:{}/{}", device.getDeviceId(), deviceChannel.getDeviceId()); + stopAudioBroadcast(device, deviceChannel); + }, 10*1000); + }, eventResultForError -> { + // 发送失败 + log.error("语音广播发送失败: {}:{}", deviceChannel.getDeviceId(), eventResultForError.msg); + event.call("语音广播发送失败"); + stopAudioBroadcast(device, deviceChannel); + }); + return true; + } + + @Override + public boolean audioBroadcastInUse(Device device, DeviceChannel channel) { + if (audioBroadcastManager.exit(channel.getId())) { + SendRtpInfo sendRtpInfo = sendRtpServerService.queryByChannelId(channel.getId(), device.getDeviceId()); + if (sendRtpInfo != null && sendRtpInfo.isOnlyAudio()) { + // 查询流是否存在,不存在则认为是异常状态 + MediaServer mediaServerServiceOne = mediaServerService.getOne(sendRtpInfo.getMediaServerId()); + Boolean streamReady = mediaServerService.isStreamReady(mediaServerServiceOne, sendRtpInfo.getApp(), sendRtpInfo.getStream()); + if (streamReady) { + log.warn("语音广播通道使用中: {}", channel.getDeviceId()); + return true; + } + } + } + return false; + } + + + @Override + public void stopAudioBroadcast(Device device, DeviceChannel channel) { + log.info("[停止对讲] 设备:{}, 通道:{}", device.getDeviceId(), channel.getDeviceId()); + List audioBroadcastCatchList = new ArrayList<>(); + if (channel == null) { + audioBroadcastCatchList.addAll(audioBroadcastManager.getByDeviceId(device.getDeviceId())); + } else { + audioBroadcastCatchList.addAll(audioBroadcastManager.getByDeviceId(device.getDeviceId())); + } + if (!audioBroadcastCatchList.isEmpty()) { + for (AudioBroadcastCatch audioBroadcastCatch : audioBroadcastCatchList) { + if (audioBroadcastCatch == null) { + continue; + } + SendRtpInfo sendRtpInfo = sendRtpServerService.queryByChannelId(channel.getId(), device.getDeviceId()); + if (sendRtpInfo != null) { + sendRtpServerService.delete(sendRtpInfo); + MediaServer mediaServer = mediaServerService.getOne(sendRtpInfo.getMediaServerId()); + mediaServerService.stopSendRtp(mediaServer, sendRtpInfo.getApp(), sendRtpInfo.getStream(), null); + try { + cmder.streamByeCmdForDeviceInvite(device, channel.getDeviceId(), audioBroadcastCatch.getSipTransactionInfo(), null); + } catch (InvalidArgumentException | ParseException | SipException | + SsrcTransactionNotFoundException e) { + log.error("[消息发送失败] 发送语音喊话BYE失败"); + } + } + + audioBroadcastManager.del(channel.getId()); + } + } + } + + @Override + public void zlmServerOnline(MediaServer mediaServer) { + // 获取 + List inviteInfoList = inviteStreamService.getAllInviteInfo(); + if (inviteInfoList.isEmpty()) { + return; + } + + List rtpServerList = mediaServerService.listRtpServer(mediaServer); + if (rtpServerList.isEmpty()) { + return; + } + for (InviteInfo inviteInfo : inviteInfoList) { + if (!rtpServerList.contains(inviteInfo.getStream())){ + inviteStreamService.removeInviteInfo(inviteInfo); + } + } + } + + @Override + public void playbackPause(String streamId) throws ServiceException, InvalidArgumentException, ParseException, SipException { + + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(InviteSessionType.PLAYBACK, streamId); + if (null == inviteInfo || inviteInfo.getStreamInfo() == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "streamId不存在"); + } + Device device = deviceService.getDeviceByDeviceId(inviteInfo.getDeviceId()); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备不存在"); + } + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcPlayService.playbackPause(device.getServerId(), streamId); + return; + } + + inviteInfo.getStreamInfo().setPause(true); + inviteStreamService.updateInviteInfo(inviteInfo); + MediaServer mediaServerItem = inviteInfo.getStreamInfo().getMediaServer(); + if (null == mediaServerItem) { + log.warn("mediaServer 不存在!"); + throw new ServiceException("mediaServer不存在"); + } + // zlm 暂停RTP超时检查 + // 使用zlm中的流ID + String streamKey = inviteInfo.getStream(); + if (!mediaServerItem.isRtpEnable()) { + streamKey = Long.toHexString(Long.parseLong(inviteInfo.getSsrcInfo().getSsrc())).toUpperCase(); + } + Boolean result = mediaServerService.pauseRtpCheck(mediaServerItem, streamKey); + if (!result) { + throw new ServiceException("暂停RTP接收失败"); + } + + DeviceChannel channel = deviceChannelService.getOneById(inviteInfo.getChannelId()); + cmder.playPauseCmd(device, channel, inviteInfo.getStreamInfo()); + } + + @Override + public void playbackResume(String streamId) throws ServiceException, InvalidArgumentException, ParseException, SipException { + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(InviteSessionType.PLAYBACK, streamId); + if (null == inviteInfo || inviteInfo.getStreamInfo() == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "streamId不存在"); + } + Device device = deviceService.getDeviceByDeviceId(inviteInfo.getDeviceId()); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备不存在"); + } + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcPlayService.playbackResume(device.getServerId(), streamId); + return; + } + + inviteInfo.getStreamInfo().setPause(false); + inviteStreamService.updateInviteInfo(inviteInfo); + MediaServer mediaServerItem = inviteInfo.getStreamInfo().getMediaServer(); + if (null == mediaServerItem) { + log.warn("mediaServer 不存在!"); + throw new ServiceException("mediaServer不存在"); + } + // 使用zlm中的流ID + String streamKey = inviteInfo.getStream(); + if (!mediaServerItem.isRtpEnable()) { + streamKey = Long.toHexString(Long.parseLong(inviteInfo.getSsrcInfo().getSsrc())).toUpperCase(); + } + boolean result = mediaServerService.resumeRtpCheck(mediaServerItem, streamKey); + if (!result) { + throw new ServiceException("继续RTP接收失败"); + } + DeviceChannel channel = deviceChannelService.getOneById(inviteInfo.getChannelId()); + cmder.playResumeCmd(device, channel, inviteInfo.getStreamInfo()); + } + + @Override + public void playbackSeek(String streamId, long seekTime) throws InvalidArgumentException, ParseException, SipException { + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(InviteSessionType.PLAYBACK, streamId); + + if (null == inviteInfo || inviteInfo.getStreamInfo() == null) { + log.warn("streamId不存在!"); + throw new ControllerException(ErrorCode.ERROR400.getCode(), "streamId不存在"); + } + Device device = deviceService.getDeviceByDeviceId(inviteInfo.getDeviceId()); + DeviceChannel channel = deviceChannelService.getOneById(inviteInfo.getChannelId()); + cmder.playSeekCmd(device, channel, inviteInfo.getStreamInfo(), seekTime); + } + + @Override + public void playbackSpeed(String streamId, double speed) throws InvalidArgumentException, ParseException, SipException { + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(InviteSessionType.PLAYBACK, streamId); + + if (null == inviteInfo || inviteInfo.getStreamInfo() == null) { + log.warn("streamId不存在!"); + throw new ControllerException(ErrorCode.ERROR400.getCode(), "streamId不存在"); + } + Device device = deviceService.getDeviceByDeviceId(inviteInfo.getDeviceId()); + DeviceChannel channel = deviceChannelService.getOneById(inviteInfo.getChannelId()); + cmder.playSpeedCmd(device, channel, inviteInfo.getStreamInfo(), speed); + } + + @Override + public void startPushStream(SendRtpInfo sendRtpInfo, DeviceChannel channel, SIPResponse sipResponse, Platform platform, CallIdHeader callIdHeader) { + // 开始发流 + MediaServer mediaInfo = mediaServerService.getOne(sendRtpInfo.getMediaServerId()); + + if (mediaInfo != null) { + try { + if (sendRtpInfo.isTcpActive()) { + mediaServerService.startSendRtpPassive(mediaInfo, sendRtpInfo, null); + } else { + mediaServerService.startSendRtp(mediaInfo, sendRtpInfo); + } + redisCatchStorage.sendPlatformStartPlayMsg(sendRtpInfo, channel, platform); + }catch (ControllerException e) { + log.error("RTP推流失败: {}", e.getMessage()); + startSendRtpStreamFailHand(sendRtpInfo, platform, callIdHeader); + return; + } + + log.info("RTP推流成功[ {}/{} ],{}, ", sendRtpInfo.getApp(), sendRtpInfo.getStream(), + sendRtpInfo.isTcpActive()?"被动发流": sendRtpInfo.getIp() + ":" + sendRtpInfo.getPort()); + + } + } + + @Override + public void startSendRtpStreamFailHand(SendRtpInfo sendRtpInfo, Platform platform, CallIdHeader callIdHeader) { + if (sendRtpInfo.isOnlyAudio()) { + Device device = deviceService.getDeviceByDeviceId(sendRtpInfo.getTargetId()); + DeviceChannel deviceChannel = deviceChannelService.getOneById(sendRtpInfo.getChannelId()); + AudioBroadcastCatch audioBroadcastCatch = audioBroadcastManager.get(sendRtpInfo.getChannelId()); + if (audioBroadcastCatch != null) { + try { + cmder.streamByeCmd(device, deviceChannel.getDeviceId(), audioBroadcastCatch.getSipTransactionInfo(), null); + } catch (SipException | ParseException | InvalidArgumentException | + SsrcTransactionNotFoundException exception) { + log.error("[命令发送失败] 停止语音对讲: {}", exception.getMessage()); + } + } + } else { + if (platform != null) { + // 向上级平台 + CommonGBChannel channel = channelService.getOne(sendRtpInfo.getChannelId()); + try { + commanderForPlatform.streamByeCmd(platform, sendRtpInfo, channel); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage()); + } + } + + } + } + + @Override + public void talkCmd(Device device, DeviceChannel channel, MediaServer mediaServerItem, String stream, AudioBroadcastEvent event) { + if (device == null || channel == null) { + return; + } + // TODO 必须多端口模式才支持语音喊话鹤语音对讲 + log.info("[语音对讲] device: {}, channel: {}", device.getDeviceId(), channel.getDeviceId()); + // 查询通道使用状态 + if (audioBroadcastManager.exit(channel.getId())) { + SendRtpInfo sendRtpInfo = sendRtpServerService.queryByChannelId(channel.getId(), device.getDeviceId()); + if (sendRtpInfo != null && sendRtpInfo.isOnlyAudio()) { + // 查询流是否存在,不存在则认为是异常状态 + MediaServer mediaServer = mediaServerService.getOne(sendRtpInfo.getMediaServerId()); + Boolean streamReady = mediaServerService.isStreamReady(mediaServer, sendRtpInfo.getApp(), sendRtpInfo.getStream()); + if (streamReady) { + log.warn("[语音对讲] 正在语音广播,无法开启语音通话: {}", channel.getDeviceId()); + event.call("正在语音广播"); + return; + } else { + stopAudioBroadcast(device, channel); + } + } + } + + SendRtpInfo sendRtpInfo = sendRtpServerService.queryByChannelId(channel.getId(), device.getDeviceId()); + if (sendRtpInfo != null) { + MediaServer mediaServer = mediaServerService.getOne(sendRtpInfo.getMediaServerId()); + Boolean streamReady = mediaServerService.isStreamReady(mediaServer, "rtp", sendRtpInfo.getReceiveStream()); + if (streamReady) { + log.warn("[语音对讲] 进行中: {}", channel.getDeviceId()); + event.call("语音对讲进行中"); + return; + } else { + stopTalk(device, channel); + } + } + + talk(mediaServerItem, device, channel, stream, (hookData) -> { + log.info("[语音对讲] 收到设备发来的流"); + }, eventResult -> { + log.warn("[语音对讲] 失败,{}/{}, 错误码 {} {}", device.getDeviceId(), channel.getDeviceId(), eventResult.statusCode, eventResult.msg); + event.call("失败,错误码 " + eventResult.statusCode + ", " + eventResult.msg); + }, () -> { + log.warn("[语音对讲] 失败,{}/{} 超时", device.getDeviceId(), channel.getDeviceId()); + event.call("失败,超时 "); + stopTalk(device, channel); + }, errorMsg -> { + log.warn("[语音对讲] 失败,{}/{} {}", device.getDeviceId(), channel.getDeviceId(), errorMsg); + event.call(errorMsg); + stopTalk(device, channel); + }); + } + + private void stopTalk(Device device, DeviceChannel channel) { + stopTalk(device, channel, null); + } + + @Override + public void stopTalk(Device device, DeviceChannel channel, Boolean streamIsReady) { + log.info("[语音对讲] 停止, {}/{}", device.getDeviceId(), channel.getDeviceId()); + SendRtpInfo sendRtpInfo = sendRtpServerService.queryByChannelId(channel.getId(), device.getDeviceId()); + if (sendRtpInfo == null) { + log.info("[语音对讲] 停止失败, 未找到发送信息,可能已经停止"); + return; + } + // 停止向设备推流 + String mediaServerId = sendRtpInfo.getMediaServerId(); + if (mediaServerId == null) { + return; + } + + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + + if (streamIsReady == null || streamIsReady) { + mediaServerService.stopSendRtp(mediaServer, sendRtpInfo.getApp(), sendRtpInfo.getStream(), sendRtpInfo.getSsrc()); + } + + ssrcFactory.releaseSsrc(mediaServerId, sendRtpInfo.getSsrc()); + + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByStream(sendRtpInfo.getApp(), sendRtpInfo.getStream()); + if (ssrcTransaction != null) { + try { + cmder.streamByeCmd(device, channel.getDeviceId(), sendRtpInfo.getApp(), sendRtpInfo.getStream(), null, null); + } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { + log.info("[语音对讲] 停止消息发送失败,可能已经停止"); + } + } + sendRtpServerService.deleteByChannel(channel.getId(), device.getDeviceId()); + } + + @Override + public void getSnap(String deviceId, String channelId, String fileName, ErrorCallback errorCallback) { + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeviceChannel channel = deviceChannelService.getOne(deviceId, channelId); + Assert.notNull(channel, "通道不存在"); + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + if (inviteInfo != null) { + if (inviteInfo.getStreamInfo() != null) { + // 已存在线直接截图 + MediaServer mediaServer = inviteInfo.getStreamInfo().getMediaServer(); + String path = "snap"; + // 请求截图 + log.info("[请求截图]: " + fileName); + mediaServerService.getSnap(mediaServer, "rtp", inviteInfo.getStreamInfo().getStream(), 15, 1, path, fileName); + File snapFile = new File(path + File.separator + fileName); + if (snapFile.exists()) { + errorCallback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), snapFile.getAbsoluteFile()); + }else { + errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null); + } + return; + } + } + + MediaServer newMediaServerItem = getNewMediaServerItem(device); + play(newMediaServerItem, deviceId, channelId, null, (code, msg, data)->{ + if (code == InviteErrorCode.SUCCESS.getCode()) { + InviteInfo inviteInfoForPlay = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + if (inviteInfoForPlay != null && inviteInfoForPlay.getStreamInfo() != null) { + getSnap(deviceId, channelId, fileName, errorCallback); + }else { + errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null); + } + }else { + errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null); + } + }); + } + + @Override + public void stop(InviteSessionType type, Device device, DeviceChannel channel, String stream) { + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcPlayService.stop(device.getServerId(), type, channel.getId(), stream); + }else { + InviteInfo inviteInfo = inviteStreamService.getInviteInfo(type, channel.getId(), stream); + if (inviteInfo == null) { + if (type == InviteSessionType.PLAY) { + deviceChannelService.stopPlay(channel.getId()); + } + return; + } + inviteStreamService.removeInviteInfo(inviteInfo); + if (InviteSessionStatus.ok == inviteInfo.getStatus()) { + try { + log.info("[停止点播/回放/下载] {}/{}", device.getDeviceId(), channel.getDeviceId()); + cmder.streamByeCmd(device, channel.getDeviceId(), "rtp", inviteInfo.getStream(), null, null); + } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { + log.error("[命令发送失败] 停止点播/回放/下载, 发送BYE: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + if (inviteInfo.getType() == InviteSessionType.PLAY) { + deviceChannelService.stopPlay(channel.getId()); + } + if (inviteInfo.getStreamInfo() != null) { + receiveRtpServerService.closeRTPServer(inviteInfo.getStreamInfo().getMediaServer(), inviteInfo.getSsrcInfo()); + } + } + } + + @Override + public void stop(InviteInfo inviteInfo) { + Assert.notNull(inviteInfo, "参数异常"); + DeviceChannel channel = deviceChannelService.getOneForSourceById(inviteInfo.getChannelId()); + if (channel == null) { + log.warn("[停止点播] 发现通道不存在"); + return; + } + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + log.warn("[停止点播] 发现设备不存在"); + return; + } + inviteStreamService.removeInviteInfo(inviteInfo); + if (InviteSessionStatus.ok == inviteInfo.getStatus()) { + try { + log.info("[停止点播/回放/下载] {}/{}", device.getDeviceId(), channel.getDeviceId()); + cmder.streamByeCmd(device, channel.getDeviceId(), "rtp", inviteInfo.getStream(), null, null); + } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { + log.warn("[命令发送失败] 停止点播/回放/下载, 发送BYE: {}", e.getMessage()); + } + } + + if (inviteInfo.getType() == InviteSessionType.PLAY) { + deviceChannelService.stopPlay(channel.getId()); + } + if (inviteInfo.getStreamInfo() != null) { + receiveRtpServerService.closeRTPServer(inviteInfo.getStreamInfo().getMediaServer(), inviteInfo.getSsrcInfo()); + } + } + + @Override + public void play(CommonGBChannel channel, Boolean record, ErrorCallback callback) { + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + log.warn("[点播] 未找到通道{}的设备信息", channel); + throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + + MediaServer mediaServerItem = getNewMediaServerItem(device); + if (mediaServerItem == null) { + log.warn("[点播] 未找到可用的zlm deviceId: {},channelId:{}", device.getDeviceId(), deviceChannel.getDeviceId()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm"); + } + play(mediaServerItem, device, deviceChannel, null, record, callback); + + } + + @Override + public void stopPlay(InviteSessionType inviteSessionType, CommonGBChannel channel) { + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + log.warn("[停止播放] 未找到通道{}的设备信息", channel); + throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + String stream = String.format("%s_%s", device.getDeviceId(), deviceChannel.getDeviceId()); + stop(inviteSessionType, device, deviceChannel, stream); + } + + @Override + public void stop(InviteSessionType inviteSessionType, CommonGBChannel channel, String stream) { + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + log.warn("[停止播放] 未找到通道{}的设备信息", channel); + throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + stop(inviteSessionType, device, deviceChannel, stream); + } + + @Override + public void playBack(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback callback) { + if (startTime == null || stopTime == null) { + throw new PlayException(Response.BAD_REQUEST, "bad request"); + } + // 国标通道 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + log.warn("[点播] 未找到通道{}的设备信息", channel); + throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + DeviceChannel deviceChannel = deviceChannelService.getOneById(channel.getGbId()); + if (deviceChannel == null) { + log.warn("[点播] 未找到通道{}", channel.getGbDeviceId()); + throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + String startTimeStr = DateUtil.timestampTo_yyyy_MM_dd_HH_mm_ss(startTime); + String stopTimeStr = DateUtil.timestampTo_yyyy_MM_dd_HH_mm_ss(stopTime); + playBack(device, deviceChannel, startTimeStr, stopTimeStr, callback); + } + + @Override + public void download(CommonGBChannel channel, Long startTime, Long stopTime, Integer downloadSpeed, ErrorCallback callback) { + if (startTime == null || stopTime == null || downloadSpeed == null) { + throw new PlayException(Response.BAD_REQUEST, "bad request"); + } + // 国标通道 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + log.warn("[点播] 未找到通道{}的设备信息", channel); + throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + DeviceChannel deviceChannel = deviceChannelService.getOneById(channel.getGbId()); + if (deviceChannel == null) { + log.warn("[点播] 未找到通道{}", channel.getGbDeviceId()); + throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + String startTimeStr = DateUtil.timestampTo_yyyy_MM_dd_HH_mm_ss(startTime); + String stopTimeStr = DateUtil.timestampTo_yyyy_MM_dd_HH_mm_ss(stopTime); + download(device, deviceChannel, startTimeStr, stopTimeStr, downloadSpeed, callback); + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/RegionServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/RegionServiceImpl.java new file mode 100644 index 0000000..f2091e9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/RegionServiceImpl.java @@ -0,0 +1,333 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.CivilCodePo; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Region; +import com.genersoft.iot.vmp.gb28181.bean.RegionTree; +import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.RegionMapper; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.service.IRegionService; +import com.genersoft.iot.vmp.utils.CivilCodeUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import java.util.*; + +/** + * 区域管理类 + */ +@Service +public class RegionServiceImpl implements IRegionService { + + + private static final Logger log = LoggerFactory.getLogger(RegionServiceImpl.class); + @Autowired + private RegionMapper regionMapper; + + @Autowired + private CommonGBChannelMapper commonGBChannelMapper; + + @Autowired + private IGbChannelService gbChannelService; + + @Autowired + private EventPublisher eventPublisher; + + @Override + public void add(Region region) { + Assert.hasLength(region.getName(), "名称必须存在"); + Assert.hasLength(region.getDeviceId(), "国标编号必须存在"); + if (ObjectUtils.isEmpty(region.getParentDeviceId()) || ObjectUtils.isEmpty(region.getParentDeviceId().trim())) { + region.setParentDeviceId(null); + } + region.setCreateTime(DateUtil.getNow()); + region.setUpdateTime(DateUtil.getNow()); + try { + regionMapper.add(region); + }catch (DuplicateKeyException e){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), "此行政区划已存在"); + } + + } + + @Override + @Transactional + public boolean deleteByDeviceId(Integer regionDeviceId) { + Region region = regionMapper.queryOne(regionDeviceId); + // 获取所有子节点 + List allChildren = getAllChildren(regionDeviceId); + allChildren.add(region); + // 设置使用这些节点的通道的civilCode为null, + gbChannelService.removeCivilCode(allChildren); + regionMapper.batchDelete(allChildren); + return true; + } + + private List getAllChildren(Integer deviceId) { + if (deviceId == null) { + return new ArrayList<>(); + } + List children = regionMapper.getChildren(deviceId); + if (ObjectUtils.isEmpty(children)) { + return children; + } + List regions = new ArrayList<>(children); + for (Region region : children) { + if (region.getDeviceId().length() < 8) { + regions.addAll(getAllChildren(region.getId())); + } + } + return regions; + } + + @Override + public PageInfo query(String query, int page, int count) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List regionList = regionMapper.query(query, null); + return new PageInfo<>(regionList); + } + + @Override + @Transactional + public void update(Region region) { + Assert.notNull(region.getDeviceId(), "编号不可为NULL"); + Assert.notNull(region.getName(), "名称不可为NULL"); + Region regionInDb = regionMapper.queryOne(region.getId()); + Assert.notNull(regionInDb, "待更新行政区划在数据库中不存在"); + if (!regionInDb.getDeviceId().equals(region.getDeviceId())) { + Region regionNewInDb = regionMapper.queryByDeviceId(region.getDeviceId()); + Assert.isNull(regionNewInDb, "此行政区划已存在"); + // 编号发生变化,把分配了这个行政区划的通道全部更新,并发送数据 + gbChannelService.updateCivilCode(regionInDb.getDeviceId(), region.getDeviceId()); + // 子节点信息更新 + regionMapper.updateChild(region.getId(), region.getDeviceId()); + } + regionMapper.update(region); + // 发送变化通知 + try { + // 发送catalog + eventPublisher.channelEventPublishForUpdate(CommonGBChannel.build(region), null); + }catch (Exception e) { + log.warn("[行政区划变化] 发送失败,{}", region.getDeviceId(), e); + } + } + + @Override + public List getAllChild(String parent) { + List allChild = CivilCodeUtil.INSTANCE.getAllChild(parent); + Collections.sort(allChild); + return allChild; + } + + @Override + public Region queryRegionByDeviceId(String regionDeviceId) { + return null; + } + + @Override + public List queryForTree(Integer parent, Boolean hasChannel) { + List regionList = regionMapper.queryForTree(parent); + if (parent != null && hasChannel != null && hasChannel) { + Region parentRegion = regionMapper.queryOne(parent); + if (parentRegion != null) { + List channelList = commonGBChannelMapper.queryForRegionTreeByCivilCode(parentRegion.getDeviceId()); + regionList.addAll(channelList); + } + } + return regionList; + } + + @Override + public void syncFromChannel() { + // 获取未初始化的行政区划节点 + List civilCodeList = regionMapper.getUninitializedCivilCode(); + if (civilCodeList.isEmpty()) { + return; + } + List regionList = new ArrayList<>(); + // 收集节点的父节点,用于验证哪些节点的父节点不存在,方便一并存入 + Map regionMapForVerification = new HashMap<>(); + civilCodeList.forEach(civilCode->{ + CivilCodePo civilCodePo = CivilCodeUtil.INSTANCE.getCivilCodePo(civilCode); + if (civilCodePo != null) { + Region region = Region.getInstance(civilCodePo); + regionList.add(region); + // 获取全部的父节点 + List civilCodePoList = CivilCodeUtil.INSTANCE.getAllParentCode(civilCode); + if (!civilCodePoList.isEmpty()) { + for (CivilCodePo codePo : civilCodePoList) { + regionMapForVerification.put(codePo.getCode(), Region.getInstance(codePo)); + } + } + } + }); + if (regionList.isEmpty()){ + return; + } + if (!regionMapForVerification.isEmpty()) { + // 查询数据库中已经存在的. + List civilCodesInDb = regionMapper.queryInList(regionMapForVerification.keySet()); + if (!civilCodesInDb.isEmpty()) { + for (String code : civilCodesInDb) { + regionMapForVerification.remove(code); + } + } + } + for (Region region : regionList) { + regionMapForVerification.put(region.getDeviceId(), region); + } + + regionMapper.batchAdd(new ArrayList<>(regionMapForVerification.values())); + } + + @Override + public boolean delete(int id) { + return regionMapper.delete(id) > 0; + } + + @Override + @Transactional + public boolean batchAdd(List regionList) { + if (regionList== null || regionList.isEmpty()) { + return false; + } + Map regionMapForVerification = new HashMap<>(); + for (Region region : regionList) { + regionMapForVerification.put(region.getDeviceId(), region); + } + // 查询数据库中已经存在的. + List regionListInDb = regionMapper.queryInRegionListByDeviceId(regionList); + if (!regionListInDb.isEmpty()) { + for (Region region : regionListInDb) { + regionMapForVerification.remove(region.getDeviceId()); + } + } + if (!regionMapForVerification.isEmpty()) { + List regions = new ArrayList<>(regionMapForVerification.values()); + regionMapper.batchAdd(regions); + regionMapper.updateParentId(regions); + } + + return true; + } + + @Override + public List getPath(String deviceId) { + Region region = regionMapper.queryByDeviceId(deviceId); + if (region == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "行政区划不存在"); + } + List allParent = getAllParent(region); + allParent.add(region); + return allParent; + } + + + private List getAllParent(Region region) { + if (region.getParentId() == null) { + return new ArrayList<>(); + } + + List regionList = new LinkedList<>(); + Region parent = regionMapper.queryByDeviceId(region.getParentDeviceId()); + if (parent == null) { + return regionList; + } + regionList.add(parent); + List allParent = getAllParent(parent); + regionList.addAll(allParent); + return regionList; + } + + @Override + public String getDescription(String civilCode) { + + CivilCodePo civilCodePo = CivilCodeUtil.INSTANCE.getCivilCodePo(civilCode); + Assert.notNull(civilCodePo, String.format("节点%s未查询到", civilCode)); + StringBuilder sb = new StringBuilder(); + sb.append(civilCodePo.getName()); + List civilCodePoList = CivilCodeUtil.INSTANCE.getAllParentCode(civilCode); + if (civilCodePoList.isEmpty()) { + return sb.toString(); + } + for (int i = 0; i < civilCodePoList.size(); i++) { + CivilCodePo item = civilCodePoList.get(i); + sb.insert(0, item.getName()); + if (i != civilCodePoList.size() - 1) { + sb.insert(0, "/"); + } + } + return sb.toString(); + } + + @Override + @Transactional + public void addByCivilCode(String civilCode) { + CivilCodePo civilCodePo = CivilCodeUtil.INSTANCE.getCivilCodePo(civilCode); + // 查询是否已经存在此节点 + Assert.notNull(civilCodePo, String.format("节点%s未查询到", civilCode)); + List civilCodePoList = CivilCodeUtil.INSTANCE.getAllParentCode(civilCode); + civilCodePoList.add(civilCodePo); + + Set civilCodeSet = regionMapper.queryInCivilCodePoList(civilCodePoList); + if (!civilCodeSet.isEmpty()) { + civilCodePoList.removeIf(item -> civilCodeSet.contains(item.getCode())); + } + if (civilCodePoList.isEmpty()) { + return; + } + int parentId = -1; + for (int i = civilCodePoList.size() - 1; i > -1; i--) { + CivilCodePo codePo = civilCodePoList.get(i); + + Region region = new Region(); + region.setDeviceId(codePo.getCode()); + region.setParentDeviceId(codePo.getParentCode()); + region.setName(civilCodePo.getName()); + region.setCreateTime(DateUtil.getNow()); + region.setUpdateTime(DateUtil.getNow()); + if (parentId == -1 && codePo.getParentCode() != null) { + Region parentRegion = regionMapper.queryByDeviceId(codePo.getParentCode()); + if (parentRegion == null){ + log.error(String.format("行政区划%sy已存在,但查询错误", codePo.getParentCode())); + throw new ControllerException(ErrorCode.ERROR100.getCode(), String.format("行政区划%sy已存在,但查询错误", codePo.getParentCode())); + } + region.setParentId(parentRegion.getId()); + }else { + region.setParentId(parentId); + } + regionMapper.add(region); + parentId = region.getId(); + } + } + + @Override + public PageInfo queryList(int page, int count, String query) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = regionMapper.query(query, null); + return new PageInfo<>(all); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourceDownloadServiceForGbImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourceDownloadServiceForGbImpl.java new file mode 100644 index 0000000..283083e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourceDownloadServiceForGbImpl.java @@ -0,0 +1,35 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.service.ISourceDownloadService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Slf4j +@Service(ChannelDataType.DOWNLOAD_SERVICE + ChannelDataType.GB28181) +public class SourceDownloadServiceForGbImpl implements ISourceDownloadService { + + @Autowired + private IPlayService deviceChannelPlayService; + + @Override + public void download(CommonGBChannel channel, Long startTime, Long stopTime, Integer downloadSpeed, ErrorCallback callback) { + + } + + @Override + public void stopDownload(CommonGBChannel channel, String stream) { + // 国标通道 + try { + deviceChannelPlayService.stop(InviteSessionType.DOWNLOAD, channel, stream); + } catch (Exception e) { + log.error("[停止下载失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePTZServiceForGbImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePTZServiceForGbImpl.java new file mode 100644 index 0000000..5bc7b8c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePTZServiceForGbImpl.java @@ -0,0 +1,351 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IPTZService; +import com.genersoft.iot.vmp.gb28181.service.ISourcePTZService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service(ChannelDataType.PTZ_SERVICE + ChannelDataType.GB28181) +public class SourcePTZServiceForGbImpl implements ISourcePTZService { + + @Autowired + private IPTZService ptzService; + + @Override + public void ptz(CommonGBChannel channel, FrontEndControlCodeForPTZ frontEndControlCode, ErrorCallback callback) { + try { + int cmdCode = 0; + int panSpeed = 0; + int titleSpeed = 0; + int zoomSpeed = 0; + if (frontEndControlCode != null) { + if (frontEndControlCode.getPan() != null) { + if (frontEndControlCode.getPan() == 0) { + cmdCode = cmdCode | 1 << 1; + } else if (frontEndControlCode.getPan() == 1) { + cmdCode = cmdCode | 1; + } + } + if (frontEndControlCode.getTilt() != null) { + if (frontEndControlCode.getTilt() == 0) { + cmdCode = cmdCode | 1 << 3; + } else if (frontEndControlCode.getTilt() == 1) { + cmdCode = cmdCode | 1 << 2; + } + } + + if (frontEndControlCode.getZoom() != null) { + if (frontEndControlCode.getZoom() == 0) { + cmdCode = cmdCode | 1 << 5; + } else if (frontEndControlCode.getZoom() == 1) { + cmdCode = cmdCode | 1 << 4; + } + } + if (frontEndControlCode.getPanSpeed() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + if (frontEndControlCode.getTiltSpeed() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + if (frontEndControlCode.getZoomSpeed() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + panSpeed = (int)(frontEndControlCode.getPanSpeed()/100D* 255); + titleSpeed = (int)(frontEndControlCode.getTiltSpeed()/100D* 255);; + zoomSpeed = (int)(frontEndControlCode.getZoomSpeed()/100D* 16); + } + ptzService.frontEndCommand(channel, cmdCode, panSpeed, titleSpeed, zoomSpeed); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + }catch (Exception e) { + log.error("[云台控制失败] ", e); + callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null); + } + } + + @Override + public void preset(CommonGBChannel channel, FrontEndControlCodeForPreset frontEndControlCode, ErrorCallback callback) { + try { + int cmdCode = 0; + int parameter1 = 0; + int parameter2 = 0; + int parameter3 = 0; + if (frontEndControlCode != null) { + if (frontEndControlCode.getCode() != null) { + if (frontEndControlCode.getCode() == 1) { + cmdCode = 0x81; + } else if (frontEndControlCode.getCode() == 2) { + cmdCode = 0x82; + }else if (frontEndControlCode.getCode() == 3) { + cmdCode = 0x83; + } + } + if (frontEndControlCode.getPresetId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter2 = frontEndControlCode.getPresetId(); + } + ptzService.frontEndCommand(channel, cmdCode, parameter1, parameter2, parameter3); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + }catch (Exception e) { + log.error("[预置位控制失败] ", e); + callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null); + } + } + + @Override + public void fi(CommonGBChannel channel, FrontEndControlCodeForFI frontEndControlCode, ErrorCallback callback) { + try { + int cmdCode = 1 << 6; + int focusSpeed = 0; + int irisSpeed = 0; + int parameter3 = 0; + if (frontEndControlCode != null) { + if (frontEndControlCode.getFocus() != null) { + if (frontEndControlCode.getFocus() == 0) { + cmdCode = cmdCode | 1 << 1; + } else if (frontEndControlCode.getFocus() == 1) { + cmdCode = cmdCode | 1; + }else { + log.error("[FI失败] 未知的聚焦指令 {}", frontEndControlCode.getFocus()); + callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null); + } + } + if (frontEndControlCode.getIris() != null) { + if (frontEndControlCode.getIris() == 0) { + cmdCode = cmdCode | 1 << 3; + } else if (frontEndControlCode.getIris() == 1) { + cmdCode = cmdCode | 1 << 2; + }else { + log.error("[FI失败] 未知的光圈指令 {}", frontEndControlCode.getIris()); + callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null); + } + } + if (frontEndControlCode.getFocusSpeed() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + if (frontEndControlCode.getIrisSpeed() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + focusSpeed = frontEndControlCode.getFocusSpeed(); + irisSpeed = frontEndControlCode.getIrisSpeed(); + } + ptzService.frontEndCommand(channel, cmdCode, focusSpeed, irisSpeed, parameter3); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + }catch (Exception e) { + log.error("[云台控制失败] ", e); + callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null); + } + } + + @Override + public void tour(CommonGBChannel channel, FrontEndControlCodeForTour frontEndControlCode, ErrorCallback callback) { + try { + int cmdCode = 0; + int parameter1 = 0; + int parameter2 = 0; + int parameter3 = 0; + if (frontEndControlCode != null) { + if (frontEndControlCode.getCode() != null) { + if (frontEndControlCode.getCode() == 1) { + cmdCode = 0x84; + if (frontEndControlCode.getPresetId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter2 = frontEndControlCode.getPresetId(); + } else if (frontEndControlCode.getCode() == 2) { + cmdCode = 0x85; + if (frontEndControlCode.getPresetId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter2 = frontEndControlCode.getPresetId(); + }else if (frontEndControlCode.getCode() == 3) { + cmdCode = 0x86; + if (frontEndControlCode.getPresetId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter2 = frontEndControlCode.getPresetId(); + if (frontEndControlCode.getTourSpeed() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter3 = frontEndControlCode.getTourSpeed(); + }else if (frontEndControlCode.getCode() == 4) { + cmdCode = 0x87; + if (frontEndControlCode.getPresetId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter2 = frontEndControlCode.getPresetId(); + if (frontEndControlCode.getTourTime() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter3 = frontEndControlCode.getTourTime(); + }else if (frontEndControlCode.getCode() == 5) { + cmdCode = 0x88; + }else if (frontEndControlCode.getCode() == 6) { + }else { + log.error("[巡航控制失败] 未知的指令 {}", frontEndControlCode.getCode()); + callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null); + } + if (frontEndControlCode.getTourId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter1 = frontEndControlCode.getTourId(); + } + + } + ptzService.frontEndCommand(channel, cmdCode, parameter1, parameter2, parameter3); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + }catch (Exception e) { + log.error("[巡航控制失败] ", e); + callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null); + } + } + + @Override + public void scan(CommonGBChannel channel, FrontEndControlCodeForScan frontEndControlCode, ErrorCallback callback) { + try { + int cmdCode = 0; + int parameter1 = 0; + int parameter2 = 0; + int parameter3 = 0; + if (frontEndControlCode != null) { + if (frontEndControlCode.getCode() != null) { + if (frontEndControlCode.getCode() == 1) { + cmdCode = 0x89; + if (frontEndControlCode.getScanId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter1 = frontEndControlCode.getScanId(); + } else if (frontEndControlCode.getCode() == 2) { + cmdCode = 0x89; + if (frontEndControlCode.getScanId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter1 = frontEndControlCode.getScanId(); + parameter2 = 1; + }else if (frontEndControlCode.getCode() == 3) { + cmdCode = 0x89; + if (frontEndControlCode.getScanId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter1 = frontEndControlCode.getScanId(); + parameter2 = 2; + }else if (frontEndControlCode.getCode() == 4) { + cmdCode = 0x8A; + if (frontEndControlCode.getScanId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + if (frontEndControlCode.getScanSpeed() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter1 = frontEndControlCode.getScanId(); + parameter2 = frontEndControlCode.getScanSpeed(); + }else if (frontEndControlCode.getCode() == 5) { + }else { + log.error("[巡航控制失败] 未知的指令 {}", frontEndControlCode.getCode()); + callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null); + } + } + } + ptzService.frontEndCommand(channel, cmdCode, parameter1, parameter2, parameter3); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + }catch (Exception e) { + log.error("[巡航控制失败] ", e); + callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null); + } + } + + @Override + public void auxiliary(CommonGBChannel channel, FrontEndControlCodeForAuxiliary frontEndControlCode, ErrorCallback callback) { + try { + int cmdCode = 0; + int parameter1 = 0; + int parameter2 = 0; + int parameter3 = 0; + if (frontEndControlCode != null) { + if (frontEndControlCode.getCode() != null) { + if (frontEndControlCode.getCode() == 1) { + cmdCode = 0x8C; + if (frontEndControlCode.getAuxiliaryId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter1 = frontEndControlCode.getAuxiliaryId(); + } else if (frontEndControlCode.getCode() == 2) { + cmdCode = 0x8D; + if (frontEndControlCode.getAuxiliaryId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter1 = frontEndControlCode.getAuxiliaryId(); + }else { + log.error("[辅助开关失败] 未知的指令 {}", frontEndControlCode.getCode()); + callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null); + } + } + } + ptzService.frontEndCommand(channel, cmdCode, parameter1, parameter2, parameter3); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + }catch (Exception e) { + log.error("[辅助开关失败] ", e); + callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null); + } + } + + @Override + public void wiper(CommonGBChannel channel, FrontEndControlCodeForWiper frontEndControlCode, ErrorCallback callback) { + try { + int cmdCode = 0; + int parameter1 = 1; + int parameter2 = 0; + int parameter3 = 0; + if (frontEndControlCode != null) { + if (frontEndControlCode.getCode() != null) { + if (frontEndControlCode.getCode() == 1) { + cmdCode = 0x8C; + } else if (frontEndControlCode.getCode() == 2) { + cmdCode = 0x8D; + }else { + log.error("[雨刷开关失败] 未知的指令 {}", frontEndControlCode.getCode()); + callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null); + } + } + } + ptzService.frontEndCommand(channel, cmdCode, parameter1, parameter2, parameter3); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + }catch (Exception e) { + log.error("[雨刷开关失败] ", e); + callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null); + } + } + + @Override + public void queryPreset(CommonGBChannel channel, ErrorCallback> callback) { + ptzService.queryPresetList(channel, callback); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePlayServiceForGbImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePlayServiceForGbImpl.java new file mode 100644 index 0000000..8dde900 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePlayServiceForGbImpl.java @@ -0,0 +1,51 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.PlayException; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.service.ISourcePlayService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sip.message.Response; + +@Slf4j +@Service(ChannelDataType.PLAY_SERVICE + ChannelDataType.GB28181) +public class SourcePlayServiceForGbImpl implements ISourcePlayService { + + @Autowired + private IPlayService deviceChannelPlayService; + + @Override + public void play(CommonGBChannel channel, Platform platform, Boolean record, ErrorCallback callback) { + // 国标通道 + try { + deviceChannelPlayService.play(channel, record, callback); + } catch (PlayException e) { + callback.run(e.getCode(), e.getMsg(), null); + } catch (ControllerException e) { + log.error("[点播失败] {}({}), {}", channel.getGbName(), channel.getGbDeviceId(), e.getMsg()); + callback.run(Response.BUSY_HERE, "busy here", null); + } catch (Exception e) { + log.error("[点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + callback.run(Response.BUSY_HERE, "busy here", null); + } + } + + @Override + public void stopPlay(CommonGBChannel channel) { + // 国标通道 + try { + deviceChannelPlayService.stopPlay(InviteSessionType.PLAY, channel); + } catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePlaybackServiceForGbImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePlaybackServiceForGbImpl.java new file mode 100644 index 0000000..859cda2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePlaybackServiceForGbImpl.java @@ -0,0 +1,113 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.CommonRecordInfo; +import com.genersoft.iot.vmp.gb28181.bean.PlayException; +import com.genersoft.iot.vmp.gb28181.bean.RecordItem; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.service.ISourcePlaybackService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sip.message.Response; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service(ChannelDataType.PLAYBACK_SERVICE + ChannelDataType.GB28181) +public class SourcePlaybackServiceForGbImpl implements ISourcePlaybackService { + + @Autowired + private IPlayService playService; + + @Autowired + private IDeviceChannelService channelService; + + @Override + public void playback(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback callback) { + try { + playService.playBack(channel, startTime, stopTime, callback); + } catch (PlayException e) { + callback.run(e.getCode(), e.getMsg(), null); + } catch (Exception e) { + callback.run(Response.BUSY_HERE, "busy here", null); + } + } + + @Override + public void stopPlayback(CommonGBChannel channel, String stream) { + // 国标通道 + try { + playService.stop(InviteSessionType.PLAYBACK, channel, stream); + } catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } + + @Override + public void playbackPause(CommonGBChannel channel, String stream) { + // 国标通道 + try { + playService.playbackPause(stream); + } catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } + + @Override + public void playbackResume(CommonGBChannel channel, String stream) { + // 国标通道 + try { + playService.playbackPause(stream); + } catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } + + @Override + public void playbackSeek(CommonGBChannel channel, String stream, long seekTime) { + // 国标通道 + try { + playService.playbackPause(stream); + } catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } + + @Override + public void playbackSpeed(CommonGBChannel channel, String stream, Double speed) { + // 国标通道 + try { + playService.playbackSpeed(stream, speed); + } catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } + + @Override + public void queryRecord(CommonGBChannel channel, String startTime, String endTime, ErrorCallback> callback) { + channelService.queryRecordInfo(channel, startTime, endTime, (code, msg, data) -> { + if (code == ErrorCode.SUCCESS.getCode()) { + List recordList = data.getRecordList(); + List recordInfoList = new ArrayList<>(); + for (RecordItem recordItem : recordList) { + CommonRecordInfo recordInfo = new CommonRecordInfo(); + recordInfo.setStartTime(recordItem.getStartTime()); + recordInfo.setEndTime(recordItem.getEndTime()); + recordInfo.setFileSize(recordItem.getFileSize()); + recordInfoList.add(recordInfo); + } + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), recordInfoList); + }else { + callback.run(code, msg, null); + } + }); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/AudioBroadcastManager.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/AudioBroadcastManager.java new file mode 100644 index 0000000..57067f1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/AudioBroadcastManager.java @@ -0,0 +1,62 @@ +package com.genersoft.iot.vmp.gb28181.session; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.gb28181.bean.AudioBroadcastCatch; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 语音广播消息管理类 + * @author lin + */ +@Slf4j +@Component +public class AudioBroadcastManager { + + @Autowired + private SipConfig config; + + public static Map data = new ConcurrentHashMap<>(); + + + public void update(AudioBroadcastCatch audioBroadcastCatch) { + data.put(audioBroadcastCatch.getChannelId(), audioBroadcastCatch); + } + + public void del(Integer channelId) { + data.remove(channelId); + + } + + public List getAll(){ + Collection values = data.values(); + return new ArrayList<>(values); + } + + + public boolean exit(Integer channelId) { + return data.containsKey(channelId); + } + + public AudioBroadcastCatch get(Integer channelId) { + return data.get(channelId); + } + + public List getByDeviceId(String deviceId) { + List audioBroadcastCatchList= new ArrayList<>(); + for (AudioBroadcastCatch broadcastCatch : data.values()) { + if (broadcastCatch.getDeviceId().equals(deviceId)) { + audioBroadcastCatchList.add(broadcastCatch); + } + } + + return audioBroadcastCatchList; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/CatalogDataManager.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/CatalogDataManager.java new file mode 100755 index 0000000..c524d72 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/CatalogDataManager.java @@ -0,0 +1,310 @@ +package com.genersoft.iot.vmp.gb28181.session; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IGroupService; +import com.genersoft.iot.vmp.gb28181.service.IRegionService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class CatalogDataManager implements CommandLineRunner { + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IRegionService regionService; + + @Autowired + private IGroupService groupService; + + @Autowired + private RedisTemplate redisTemplate; + + private final Map dataMap = new ConcurrentHashMap<>(); + + private final String key = "VMP_CATALOG_DATA"; + + public String buildMapKey(String deviceId, int sn ) { + return deviceId + "_" + sn; + } + + public void addReady(Device device, int sn ) { + CatalogData catalogData = dataMap.get(buildMapKey(device.getDeviceId(),sn)); + if (catalogData != null) { + Set redisKeysForChannel = catalogData.getRedisKeysForChannel(); + if (redisKeysForChannel != null && !redisKeysForChannel.isEmpty()) { + for (String deleteKey : redisKeysForChannel) { + redisTemplate.opsForHash().delete(key, deleteKey); + } + } + Set redisKeysForRegion = catalogData.getRedisKeysForRegion(); + if (redisKeysForRegion != null && !redisKeysForRegion.isEmpty()) { + for (String deleteKey : redisKeysForRegion) { + redisTemplate.opsForHash().delete(key, deleteKey); + } + } + Set redisKeysForGroup = catalogData.getRedisKeysForGroup(); + if (redisKeysForGroup != null && !redisKeysForGroup.isEmpty()) { + for (String deleteKey : redisKeysForGroup) { + redisTemplate.opsForHash().delete(key, deleteKey); + } + } + dataMap.remove(buildMapKey(device.getDeviceId(),sn)); + } + catalogData = new CatalogData(); + catalogData.setDevice(device); + catalogData.setSn(sn); + catalogData.setStatus(CatalogData.CatalogDataStatus.ready); + catalogData.setTime(Instant.now()); + dataMap.put(buildMapKey(device.getDeviceId(),sn), catalogData); + } + + public void put(String deviceId, int sn, int total, Device device, List deviceChannelList, + List regionList, List groupList) { + CatalogData catalogData = dataMap.get(buildMapKey(device.getDeviceId(),sn)); + if (catalogData == null ) { + log.warn("[缓存-Catalog] 未找到缓存对象,可能已经结束"); + return; + } + catalogData.setStatus(CatalogData.CatalogDataStatus.runIng); + catalogData.setTotal(total); + catalogData.setTime(Instant.now()); + + if (deviceChannelList != null && !deviceChannelList.isEmpty()) { + for (DeviceChannel deviceChannel : deviceChannelList) { + String keyForChannel = "CHANNEL:" + deviceId + ":" + deviceChannel.getDeviceId() + ":" + sn; + redisTemplate.opsForHash().put(key, keyForChannel, deviceChannel); + catalogData.getRedisKeysForChannel().add(keyForChannel); + } + } + + if (regionList != null && !regionList.isEmpty()) { + for (Region region : regionList) { + String keyForRegion = "REGION:" + deviceId + ":" + region.getDeviceId() + ":" + sn; + redisTemplate.opsForHash().put(key, keyForRegion, region); + catalogData.getRedisKeysForRegion().add(keyForRegion); + } + } + + if (groupList != null && !groupList.isEmpty()) { + for (Group group : groupList) { + String keyForGroup = "GROUP:" + deviceId + ":" + group.getDeviceId() + ":" + sn; + redisTemplate.opsForHash().put(key, keyForGroup, group); + catalogData.getRedisKeysForGroup().add(keyForGroup); + } + } + } + + public List getDeviceChannelList(String deviceId, int sn) { + List result = new ArrayList<>(); + CatalogData catalogData = dataMap.get(buildMapKey(deviceId,sn)); + if (catalogData == null ) { + log.warn("[Redis-Catalog] 未找到缓存对象,可能已经结束"); + return result; + } + for (String objectKey : catalogData.getRedisKeysForChannel()) { + DeviceChannel deviceChannel = (DeviceChannel) redisTemplate.opsForHash().get(key, objectKey); + if (deviceChannel != null) { + result.add(deviceChannel); + } + } + return result; + } + + public List getRegionList(String deviceId, int sn) { + List result = new ArrayList<>(); + CatalogData catalogData = dataMap.get(buildMapKey(deviceId,sn)); + if (catalogData == null ) { + log.warn("[Redis-Catalog] 未找到缓存对象,可能已经结束"); + return result; + } + for (String objectKey : catalogData.getRedisKeysForRegion()) { + Region region = (Region) redisTemplate.opsForHash().get(key, objectKey); + if (region != null) { + result.add(region); + } + } + return result; + } + + public List getGroupList(String deviceId, int sn) { + List result = new ArrayList<>(); + CatalogData catalogData = dataMap.get(buildMapKey(deviceId,sn)); + if (catalogData == null ) { + log.warn("[Redis-Catalog] 未找到缓存对象,可能已经结束"); + return result; + } + for (String objectKey : catalogData.getRedisKeysForGroup()) { + Group group = (Group) redisTemplate.opsForHash().get(key, objectKey); + if (group != null) { + result.add(group); + } + } + return result; + } + + public SyncStatus getSyncStatus(String deviceId) { + if (dataMap.isEmpty()) { + return null; + } + Set keySet = dataMap.keySet(); + for (String key : keySet) { + CatalogData catalogData = dataMap.get(key); + if (catalogData != null && deviceId.equals(catalogData.getDevice().getDeviceId())) { + SyncStatus syncStatus = new SyncStatus(); + syncStatus.setCurrent(catalogData.getRedisKeysForChannel().size()); + syncStatus.setTotal(catalogData.getTotal()); + syncStatus.setErrorMsg(catalogData.getErrorMsg()); + syncStatus.setTime(catalogData.getTime()); + if (catalogData.getStatus().equals(CatalogData.CatalogDataStatus.ready) || catalogData.getStatus().equals(CatalogData.CatalogDataStatus.end)) { + syncStatus.setSyncIng(false); + }else { + syncStatus.setSyncIng(true); + } + if (catalogData.getErrorMsg() != null) { + // 失败的同步信息,返回一次后直接移除 + dataMap.remove(key); + } + return syncStatus; + } + } + return null; + } + + public boolean isSyncRunning(String deviceId) { + if (dataMap.isEmpty()) { + return false; + } + Set keySet = dataMap.keySet(); + for (String key : keySet) { + CatalogData catalogData = dataMap.get(key); + if (catalogData != null && deviceId.equals(catalogData.getDevice().getDeviceId())) { + // 此时检查是否过期 + Instant instantBefore30S = Instant.now().minusMillis(TimeUnit.SECONDS.toMillis(30)); + if ((catalogData.getStatus().equals(CatalogData.CatalogDataStatus.end) + || catalogData.getStatus().equals(CatalogData.CatalogDataStatus.ready)) + && catalogData.getTime().isBefore(instantBefore30S)) { + dataMap.remove(key); + return false; + } + + return !catalogData.getStatus().equals(CatalogData.CatalogDataStatus.end); + } + } + return false; + } + + @Override + public void run(String... args) throws Exception { + // 启动时清理旧的数据 + redisTemplate.delete(key); + } + + @Scheduled(fixedDelay = 5 * 1000) //每5秒执行一次, 发现数据5秒未更新则移除数据并认为数据接收超时 + private void timerTask(){ + if (dataMap.isEmpty()) { + return; + } + Set keys = dataMap.keySet(); + + Instant instantBefore5S = Instant.now().minusMillis(TimeUnit.SECONDS.toMillis(5)); + Instant instantBefore30S = Instant.now().minusMillis(TimeUnit.SECONDS.toMillis(30)); + for (String dataKey : keys) { + CatalogData catalogData = dataMap.get(dataKey); + if ( catalogData.getTime().isBefore(instantBefore5S)) { + if (catalogData.getStatus().equals(CatalogData.CatalogDataStatus.runIng)) { + String deviceId = catalogData.getDevice().getDeviceId(); + int sn = catalogData.getSn(); + List deviceChannelList = getDeviceChannelList(deviceId, sn); + try { + if (catalogData.getTotal() == deviceChannelList.size()) { + deviceChannelService.resetChannels(catalogData.getDevice().getId(), deviceChannelList); + }else { + deviceChannelService.updateChannels(catalogData.getDevice(), deviceChannelList); + } + List regionList = getRegionList(deviceId, sn); + if ( regionList!= null && !regionList.isEmpty()) { + regionService.batchAdd(regionList); + } + List groupList = getGroupList(deviceId, sn); + if (groupList != null && !groupList.isEmpty()) { + groupService.batchAdd(groupList); + } + }catch (Exception e) { + log.error("[国标通道同步] 入库失败: ", e); + } + String errorMsg = "更新成功,共" + catalogData.getTotal() + "条,已更新" + deviceChannelList.size() + "条"; + catalogData.setErrorMsg(errorMsg); + catalogData.setStatus(CatalogData.CatalogDataStatus.end); + }else if (catalogData.getStatus().equals(CatalogData.CatalogDataStatus.ready)) { + String errorMsg = "同步失败,等待回复超时"; + catalogData.setErrorMsg(errorMsg); + } + } + if ((catalogData.getStatus().equals(CatalogData.CatalogDataStatus.end) || catalogData.getStatus().equals(CatalogData.CatalogDataStatus.ready)) + && catalogData.getTime().isBefore(instantBefore30S)) { // 超过三十秒,如果标记为end则删除 + dataMap.remove(dataKey); + Set redisKeysForChannel = catalogData.getRedisKeysForChannel(); + if (redisKeysForChannel != null && !redisKeysForChannel.isEmpty()) { + for (String deleteKey : redisKeysForChannel) { + redisTemplate.opsForHash().delete(key, deleteKey); + } + } + Set redisKeysForRegion = catalogData.getRedisKeysForRegion(); + if (redisKeysForRegion != null && !redisKeysForRegion.isEmpty()) { + for (String deleteKey : redisKeysForRegion) { + redisTemplate.opsForHash().delete(key, deleteKey); + } + } + Set redisKeysForGroup = catalogData.getRedisKeysForGroup(); + if (redisKeysForGroup != null && !redisKeysForGroup.isEmpty()) { + for (String deleteKey : redisKeysForGroup) { + redisTemplate.opsForHash().delete(key, deleteKey); + } + } + } + } + } + + + public void setChannelSyncEnd(String deviceId, int sn, String errorMsg) { + CatalogData catalogData = dataMap.get(buildMapKey(deviceId,sn)); + if (catalogData == null) { + return; + } + catalogData.setStatus(CatalogData.CatalogDataStatus.end); + catalogData.setErrorMsg(errorMsg); + catalogData.setTime(Instant.now()); + } + + public int size(String deviceId, int sn) { + CatalogData catalogData = dataMap.get(buildMapKey(deviceId,sn)); + if (catalogData == null) { + return 0; + } + return catalogData.getRedisKeysForChannel().size() + catalogData.getErrorChannel().size(); + } + + public int sumNum(String deviceId, int sn) { + CatalogData catalogData = dataMap.get(buildMapKey(deviceId,sn)); + if (catalogData == null) { + return 0; + } + return catalogData.getTotal(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/CommonSessionManager.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/CommonSessionManager.java new file mode 100755 index 0000000..2d8c7e1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/CommonSessionManager.java @@ -0,0 +1,86 @@ +package com.genersoft.iot.vmp.gb28181.session; + +import com.genersoft.iot.vmp.common.CommonCallback; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.Calendar; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 通用回调管理 + */ +@Component +public class CommonSessionManager { + + public static Map callbackMap = new ConcurrentHashMap<>(); + + /** + * 存储回调相关的信息 + */ + class CommonSession{ + public String session; + public long createTime; + public int timeout; + + public CommonCallback callback; + public CommonCallback timeoutCallback; + } + + /** + * 添加回调 + * @param sessionId 唯一标识 + * @param callback 回调 + * @param timeout 超时时间, 单位分钟 + */ + public void add(String sessionId, CommonCallback callback, CommonCallback timeoutCallback, + Integer timeout) { + CommonSession commonSession = new CommonSession(); + commonSession.session = sessionId; + commonSession.callback = callback; + commonSession.createTime = System.currentTimeMillis(); + if (timeoutCallback != null) { + commonSession.timeoutCallback = timeoutCallback; + } + if (timeout != null) { + commonSession.timeout = timeout; + } + callbackMap.put(sessionId, commonSession); + } + + public void add(String sessionId, CommonCallback callback) { + add(sessionId, callback, null, 1); + } + + public CommonCallback get(String sessionId, boolean destroy) { + CommonSession commonSession = callbackMap.get(sessionId); + if (destroy) { + callbackMap.remove(sessionId); + } + return commonSession.callback; + } + + public CommonCallback get(String sessionId) { + return get(sessionId, false); + } + + public void delete(String sessionID) { + callbackMap.remove(sessionID); + } + + @Scheduled(fixedRate= 60) //每分钟执行一次 + public void execute(){ + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MINUTE, -1); + for (String session : callbackMap.keySet()) { + if (callbackMap.get(session).createTime < cal.getTimeInMillis()) { + // 超时 + if (callbackMap.get(session).timeoutCallback != null) { + callbackMap.get(session).timeoutCallback.run("timeout"); + } + callbackMap.remove(session); + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/SSRCFactory.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/SSRCFactory.java new file mode 100755 index 0000000..67f9c29 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/SSRCFactory.java @@ -0,0 +1,125 @@ +package com.genersoft.iot.vmp.gb28181.session; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * ssrc使用 + */ +@Slf4j +@Component +public class SSRCFactory { + + /** + * 播流最大并发个数 + */ + private static final Integer MAX_STREAM_COUNT = 10000; + + /** + * 播流最大并发个数 + */ + private static final String SSRC_INFO_KEY = "VMP_SSRC_INFO_"; + + @Autowired + private StringRedisTemplate redisTemplate; + + @Autowired + private SipConfig sipConfig; + + @Autowired + private UserSetting userSetting; + + + public void initMediaServerSSRC(String mediaServerId, Set usedSet) { + String sipDomain = sipConfig.getDomain(); + String ssrcPrefix = sipDomain.length() >= 8 ? sipDomain.substring(3, 8) : sipDomain; + String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId; + List ssrcList = new ArrayList<>(); + for (int i = 1; i < MAX_STREAM_COUNT; i++) { + String ssrc = String.format("%s%04d", ssrcPrefix, i); + + if (null == usedSet || !usedSet.contains(ssrc)) { + ssrcList.add(ssrc); + + } + } + if (redisTemplate.opsForSet().size(redisKey) != null) { + redisTemplate.delete(redisKey); + } + redisTemplate.opsForSet().add(redisKey, ssrcList.toArray(new String[0])); + } + + + /** + * 获取视频预览的SSRC值,第一位固定为0 + * + * @return ssrc + */ + public String getPlaySsrc(String mediaServerId) { + return "0" + getSN(mediaServerId); + } + + /** + * 获取录像回放的SSRC值,第一位固定为1 + */ + public String getPlayBackSsrc(String mediaServerId) { + return "1" + getSN(mediaServerId); + } + + /** + * 释放ssrc,主要用完的ssrc一定要释放,否则会耗尽 + * + * @param ssrc 需要重置的ssrc + */ + public void releaseSsrc(String mediaServerId, String ssrc) { + if (ssrc == null) { + return; + } + String sn = ssrc.substring(1); + String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId; + redisTemplate.opsForSet().add(redisKey, sn); + } + + /** + * 获取后四位数SN,随机数 + */ + private String getSN(String mediaServerId) { + String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId; + Long size = redisTemplate.opsForSet().size(redisKey); + if (size == null || size == 0) { + log.info("[获取 SSRC 失败] redisKey: {}", redisKey); + throw new RuntimeException("ssrc已经用完"); + } else { + // 在集合中移除并返回一个随机成员。 + return redisTemplate.opsForSet().pop(redisKey); + } + } + + /** + * 重置一个流媒体服务的所有ssrc + * + * @param mediaServerId 流媒体服务ID + */ + public void reset(String mediaServerId) { + this.initMediaServerSSRC(mediaServerId, null); + } + + /** + * 是否已经存在了某个MediaServer的SSRC信息 + * + * @param mediaServerId 流媒体服务ID + */ + public boolean hasMediaServerSSRC(String mediaServerId) { + String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId; + return Boolean.TRUE.equals(redisTemplate.hasKey(redisKey)); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/SipInviteSessionManager.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/SipInviteSessionManager.java new file mode 100755 index 0000000..a4468d9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/SipInviteSessionManager.java @@ -0,0 +1,90 @@ +package com.genersoft.iot.vmp.gb28181.session; + +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 视频流session管理器,管理视频预览、预览回放的通信句柄 + */ +@Component +public class SipInviteSessionManager { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 添加一个点播/回放的事务信息 + */ + public void put(SsrcTransaction ssrcTransaction){ + redisTemplate.opsForHash().put(VideoManagerConstants.SIP_INVITE_SESSION_STREAM + userSetting.getServerId() + , ssrcTransaction.getApp() + ssrcTransaction.getStream(), ssrcTransaction); + + redisTemplate.opsForHash().put(VideoManagerConstants.SIP_INVITE_SESSION_CALL_ID + userSetting.getServerId() + , ssrcTransaction.getCallId(), ssrcTransaction); + } + + public SsrcTransaction getSsrcTransactionByStream(String app, String stream){ + String key = VideoManagerConstants.SIP_INVITE_SESSION_STREAM + userSetting.getServerId(); + return (SsrcTransaction)redisTemplate.opsForHash().get(key, app + stream); + } + + public SsrcTransaction getSsrcTransactionByCallId(String callId){ + String key = VideoManagerConstants.SIP_INVITE_SESSION_CALL_ID + userSetting.getServerId(); + return (SsrcTransaction)redisTemplate.opsForHash().get(key, callId); + } + + public List getSsrcTransactionByDeviceId(String deviceId){ + String key = VideoManagerConstants.SIP_INVITE_SESSION_CALL_ID + userSetting.getServerId(); + List values = redisTemplate.opsForHash().values(key); + List result = new ArrayList<>(); + for (Object value : values) { + SsrcTransaction ssrcTransaction = (SsrcTransaction) value; + if (ssrcTransaction != null && deviceId.equals(ssrcTransaction.getDeviceId())) { + result.add(ssrcTransaction); + } + } + return result; + } + + public void removeByStream(String app, String stream) { + SsrcTransaction ssrcTransaction = getSsrcTransactionByStream(app, stream); + if (ssrcTransaction == null ) { + return; + } + redisTemplate.opsForHash().delete(VideoManagerConstants.SIP_INVITE_SESSION_STREAM + userSetting.getServerId(), app + stream); + if (ssrcTransaction.getCallId() != null) { + redisTemplate.opsForHash().delete(VideoManagerConstants.SIP_INVITE_SESSION_CALL_ID + userSetting.getServerId(), ssrcTransaction.getCallId()); + } + } + + public void removeByCallId(String callId) { + SsrcTransaction ssrcTransaction = getSsrcTransactionByCallId(callId); + if (ssrcTransaction == null ) { + return; + } + redisTemplate.opsForHash().delete(VideoManagerConstants.SIP_INVITE_SESSION_CALL_ID + userSetting.getServerId(), callId); + if (ssrcTransaction.getStream() != null) { + redisTemplate.opsForHash().delete(VideoManagerConstants.SIP_INVITE_SESSION_STREAM + userSetting.getServerId(), ssrcTransaction.getApp() + ssrcTransaction.getStream()); + } + } + + public List getAll() { + String key = VideoManagerConstants.SIP_INVITE_SESSION_CALL_ID + userSetting.getServerId(); + List values = redisTemplate.opsForHash().values(key); + List result = new ArrayList<>(); + for (Object value : values) { + result.add((SsrcTransaction) value); + } + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/SseSessionManager.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/SseSessionManager.java new file mode 100644 index 0000000..6c88f7d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/SseSessionManager.java @@ -0,0 +1,72 @@ +package com.genersoft.iot.vmp.gb28181.session; + +import com.genersoft.iot.vmp.conf.DynamicTask; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +@Slf4j +public class SseSessionManager { + + private static final Map sseSessionMap = new ConcurrentHashMap<>(); + + @Autowired + private DynamicTask dynamicTask; + + public SseEmitter conect(String browserId){ + SseEmitter sseEmitter = new SseEmitter(0L); + sseEmitter.onError((err)-> { + log.error("[SSE推送] 连接错误, 浏览器 ID: {}, {}", browserId, err.getMessage()); + sseSessionMap.remove(browserId); + sseEmitter.completeWithError(err); + }); + +// sseEmitter.onTimeout(() -> { +// log.info("[SSE推送] 连接超时, 浏览器 ID: {}", browserId); +// sseSessionMap.remove(browserId); +// sseEmitter.complete(); +// dynamicTask.stop(key); +// }); + + sseEmitter.onCompletion(() -> { + log.info("[SSE推送] 连接结束, 浏览器 ID: {}", browserId); + sseSessionMap.remove(browserId); + }); + + sseSessionMap.put(browserId, sseEmitter); + + log.info("[SSE推送] 连接已建立, 浏览器 ID: {}, 当前在线数: {}", browserId, sseSessionMap.size()); + return sseEmitter; + } + + @Scheduled(fixedRate = 1000) //每1秒执行一次 + public void execute(){ + if (sseSessionMap.isEmpty()){ + return; + } + sendForAll("keepalive", "alive"); + } + + + public void sendForAll(String event, Object data) { + for (String browserId : sseSessionMap.keySet()) { + SseEmitter sseEmitter = sseSessionMap.get(browserId); + if (sseEmitter == null) { + continue; + }; + try { + sseEmitter.send(SseEmitter.event().name(event).data(data)); + } catch (Exception e) { + log.error("[SSE推送] 发送失败: {}", e.getMessage()); + sseSessionMap.remove(browserId); + sseEmitter.completeWithError(e); + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTask.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTask.java new file mode 100644 index 0000000..0e75435 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTask.java @@ -0,0 +1,60 @@ +package com.genersoft.iot.vmp.gb28181.task.deviceStatus; + +import com.genersoft.iot.vmp.common.DeviceStatusCallback; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Data +public class DeviceStatusTask implements Delayed { + + private String deviceId; + + private SipTransactionInfo transactionInfo; + + /** + * 超时时间(单位: 毫秒) + */ + private long delayTime; + + private DeviceStatusCallback callback; + + public static DeviceStatusTask getInstance(String deviceId, SipTransactionInfo transactionInfo, long delayTime, DeviceStatusCallback callback) { + DeviceStatusTask deviceStatusTask = new DeviceStatusTask(); + deviceStatusTask.setDeviceId(deviceId); + deviceStatusTask.setTransactionInfo(transactionInfo); + deviceStatusTask.setDelayTime(delayTime); + deviceStatusTask.setCallback(callback); + return deviceStatusTask; + } + + public void expired() { + if (callback == null) { + log.info("[设备离线] 未找到过期处理回调, {}", deviceId); + return; + } + callback.run(deviceId, transactionInfo); + } + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } + + public DeviceStatusTaskInfo getInfo(){ + DeviceStatusTaskInfo taskInfo = new DeviceStatusTaskInfo(); + taskInfo.setTransactionInfo(transactionInfo); + taskInfo.setDeviceId(deviceId); + return taskInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTaskInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTaskInfo.java new file mode 100644 index 0000000..af2aa75 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTaskInfo.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.gb28181.task.deviceStatus; + +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import lombok.Data; + +@Data +public class DeviceStatusTaskInfo{ + + private String deviceId; + + private SipTransactionInfo transactionInfo; + + /** + * 过期时间,单位毫秒 + */ + private long expireTime; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTaskRunner.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTaskRunner.java new file mode 100644 index 0000000..a8e9c79 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTaskRunner.java @@ -0,0 +1,131 @@ +package com.genersoft.iot.vmp.gb28181.task.deviceStatus; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.utils.redis.RedisUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class DeviceStatusTaskRunner { + + private final Map subscribes = new ConcurrentHashMap<>(); + + private final DelayQueue delayQueue = new DelayQueue<>(); + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private UserSetting userSetting; + + private final String prefix = "VMP_DEVICE_STATUS"; + + // 状态过期检查 + @Scheduled(fixedDelay = 500, timeUnit = TimeUnit.MILLISECONDS) + public void expirationCheck(){ + while (!delayQueue.isEmpty()) { + DeviceStatusTask take = null; + try { + take = delayQueue.take(); + try { + removeTask(take.getDeviceId()); + take.expired(); + }catch (Exception e) { + log.error("[设备状态到期] 到期处理时出现异常, 设备编号: {} ", take.getDeviceId()); + } + } catch (InterruptedException e) { + log.error("[设备状态任务] ", e); + } + } + } + + public void addTask(DeviceStatusTask task) { + Duration duration = Duration.ofSeconds((task.getDelayTime() - System.currentTimeMillis())/1000); + if (duration.getSeconds() < 0) { + return; + } + subscribes.put(task.getDeviceId(), task); + String key = String.format("%s_%s_%s", prefix, userSetting.getServerId(), task.getDeviceId()); + redisTemplate.opsForValue().set(key, task.getInfo(), duration); + delayQueue.offer(task); + } + + public boolean removeTask(String key) { + DeviceStatusTask task = subscribes.get(key); + if (task == null) { + return false; + } + String redisKey = String.format("%s_%s_%s", prefix, userSetting.getServerId(), task.getDeviceId()); + redisTemplate.delete(redisKey); + subscribes.remove(key); + if (delayQueue.contains(task)) { + boolean remove = delayQueue.remove(task); + if (!remove) { + log.info("[移除状态任务] 从延时队列内移除失败: {}", key); + } + } + return true; + } + + public SipTransactionInfo getTransactionInfo(String key) { + DeviceStatusTask task = subscribes.get(key); + if (task == null) { + return null; + } + return task.getTransactionInfo(); + } + + public boolean updateDelay(String key, long expirationTime) { + DeviceStatusTask task = subscribes.get(key); + if (task == null) { + return false; + } + log.debug("[更新状态任务时间] 编号: {}", key); + // 如果值更改时间,如果队列中有多个元素时 超时无法出发。目前采用移除再加入的方法 + delayQueue.remove(task); + task.setDelayTime(expirationTime); + delayQueue.offer(task); + String redisKey = String.format("%s_%s_%s", prefix, userSetting.getServerId(), task.getDeviceId()); + Duration duration = Duration.ofSeconds((expirationTime - System.currentTimeMillis())/1000); + redisTemplate.expire(redisKey, duration); + return true; + } + + public boolean containsKey(String key) { + return subscribes.containsKey(key); + } + + public List getAllTaskInfo(){ + String scanKey = String.format("%s_%s_*", prefix, userSetting.getServerId()); + List values = RedisUtil.scan(redisTemplate, scanKey); + if (values.isEmpty()) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + for (Object value : values) { + String redisKey = (String)value; + DeviceStatusTaskInfo taskInfo = (DeviceStatusTaskInfo)redisTemplate.opsForValue().get(redisKey); + if (taskInfo == null) { + continue; + } + Long expire = redisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS); + taskInfo.setExpireTime(expire); + result.add(taskInfo); + } + return result; + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTask.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTask.java new file mode 100644 index 0000000..e33ac38 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTask.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.gb28181.task.deviceSubscribe; + +import com.genersoft.iot.vmp.common.SubscribeCallback; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +@Data +public abstract class SubscribeTask implements Delayed { + + private String deviceId; + + private SubscribeCallback callback; + + private SipTransactionInfo transactionInfo; + + /** + * 超时时间(单位: 毫秒) + */ + private long delayTime; + + public abstract void expired(); + + public abstract String getKey(); + + public abstract String getName(); + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } + + public SubscribeTaskInfo getInfo(){ + SubscribeTaskInfo subscribeTaskInfo = new SubscribeTaskInfo(); + subscribeTaskInfo.setName(getName()); + subscribeTaskInfo.setTransactionInfo(transactionInfo); + subscribeTaskInfo.setDeviceId(deviceId); + subscribeTaskInfo.setKey(getKey()); + return subscribeTaskInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTaskInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTaskInfo.java new file mode 100644 index 0000000..aa9dd8c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTaskInfo.java @@ -0,0 +1,22 @@ +package com.genersoft.iot.vmp.gb28181.task.deviceSubscribe; + +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import lombok.Data; + +@Data +public class SubscribeTaskInfo { + + private String deviceId; + + private SipTransactionInfo transactionInfo; + + private String name; + + private String key; + + /** + * 过期时间,单位: 秒 + */ + private long expireTime; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTaskRunner.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTaskRunner.java new file mode 100644 index 0000000..7e70935 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTaskRunner.java @@ -0,0 +1,130 @@ +package com.genersoft.iot.vmp.gb28181.task.deviceSubscribe; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.utils.redis.RedisUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class SubscribeTaskRunner{ + + private final Map subscribes = new ConcurrentHashMap<>(); + + private final DelayQueue delayQueue = new DelayQueue<>(); + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private UserSetting userSetting; + + private final String prefix = "VMP_DEVICE_SUBSCRIBE"; + + // 订阅过期检查 + @Scheduled(fixedDelay = 500, timeUnit = TimeUnit.MILLISECONDS) + public void expirationCheck(){ + while (!delayQueue.isEmpty()) { + SubscribeTask take = null; + try { + take = delayQueue.take(); + try { + removeSubscribe(take.getKey()); + take.expired(); + }catch (Exception e) { + log.error("[设备订阅到期] {} 到期处理时出现异常, 设备编号: {} ", take.getName(), take.getDeviceId()); + } + } catch (InterruptedException e) { + log.error("[设备订阅任务] ", e); + } + } + } + + public void addSubscribe(SubscribeTask task) { + Duration duration = Duration.ofSeconds((task.getDelayTime() - System.currentTimeMillis())/1000); + if (duration.getSeconds() < 0) { + return; + } + subscribes.put(task.getKey(), task); + String key = String.format("%s_%s_%s", prefix, userSetting.getServerId(), task.getKey()); + redisTemplate.opsForValue().set(key, task.getInfo(), duration); + delayQueue.offer(task); + } + + public boolean removeSubscribe(String key) { + SubscribeTask task = subscribes.get(key); + if (task == null) { + return false; + } + String redisKey = String.format("%s_%s_%s", prefix, userSetting.getServerId(), task.getKey()); + redisTemplate.delete(redisKey); + subscribes.remove(key); + if (delayQueue.contains(task)) { + boolean remove = delayQueue.remove(task); + if (!remove) { + log.info("[移除订阅任务] 从延时队列内移除失败: {}", key); + } + } + return true; + } + + public SipTransactionInfo getTransactionInfo(String key) { + SubscribeTask task = subscribes.get(key); + if (task == null) { + return null; + } + return task.getTransactionInfo(); + } + + public boolean updateDelay(String key, long expirationTime) { + SubscribeTask task = subscribes.get(key); + if (task == null) { + return false; + } + log.info("[更新订阅任务时间] {}, 编号: {}", task.getName(), key); + delayQueue.remove(task); + task.setDelayTime(expirationTime); + delayQueue.offer(task); + String redisKey = String.format("%s_%s_%s", prefix, userSetting.getServerId(), task.getKey()); + Duration duration = Duration.ofSeconds((expirationTime - System.currentTimeMillis())/1000); + redisTemplate.expire(redisKey, duration); + return true; + } + + public boolean containsKey(String key) { + return subscribes.containsKey(key); + } + + public List getAllTaskInfo(){ + String scanKey = String.format("%s_%s_*", prefix, userSetting.getServerId()); + List values = RedisUtil.scan(redisTemplate, scanKey); + if (values.isEmpty()) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + for (Object value : values) { + String redisKey = (String)value; + SubscribeTaskInfo taskInfo = (SubscribeTaskInfo)redisTemplate.opsForValue().get(redisKey); + if (taskInfo == null) { + continue; + } + Long expire = redisTemplate.getExpire(redisKey, TimeUnit.SECONDS); + taskInfo.setExpireTime(expire); + result.add(taskInfo); + } + return result; + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/impl/SubscribeTaskForCatalog.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/impl/SubscribeTaskForCatalog.java new file mode 100644 index 0000000..8ca7b39 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/impl/SubscribeTaskForCatalog.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.impl; + +import com.genersoft.iot.vmp.common.SubscribeCallback; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.SubscribeTask; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SubscribeTaskForCatalog extends SubscribeTask { + + public static final String name = "catalog"; + + public static SubscribeTask getInstance(Device device, SubscribeCallback callback, SipTransactionInfo transactionInfo) { + if (device.getSubscribeCycleForCatalog() <= 0) { + return null; + } + SubscribeTaskForCatalog subscribeTaskForCatalog = new SubscribeTaskForCatalog(); + subscribeTaskForCatalog.setDelayTime((device.getSubscribeCycleForCatalog() * 1000L - 500L) + System.currentTimeMillis()); + subscribeTaskForCatalog.setDeviceId(device.getDeviceId()); + subscribeTaskForCatalog.setCallback(callback); + subscribeTaskForCatalog.setTransactionInfo(transactionInfo); + return subscribeTaskForCatalog; + } + + @Override + public void expired() { + if (super.getCallback() == null) { + log.info("[设备订阅到期] 目录订阅 未找到到期处理回调, 编号: {}", getDeviceId()); + return; + } + getCallback().run(getDeviceId(), getTransactionInfo()); + } + + @Override + public String getKey() { + return String.format("%s_%s", name, getDeviceId()); + } + + @Override + public String getName() { + return name; + } + + public static String getKey(Device device) { + return String.format("%s_%s", SubscribeTaskForCatalog.name, device.getDeviceId()); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/impl/SubscribeTaskForMobilPosition.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/impl/SubscribeTaskForMobilPosition.java new file mode 100644 index 0000000..48bf9c5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/impl/SubscribeTaskForMobilPosition.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.impl; + +import com.genersoft.iot.vmp.common.SubscribeCallback; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.SubscribeTask; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SubscribeTaskForMobilPosition extends SubscribeTask { + + public static final String name = "mobilPosition"; + + public static SubscribeTask getInstance(Device device, SubscribeCallback callback, SipTransactionInfo transactionInfo) { + if (device.getSubscribeCycleForMobilePosition() <= 0) { + return null; + } + SubscribeTaskForMobilPosition subscribeTaskForMobilPosition = new SubscribeTaskForMobilPosition(); + subscribeTaskForMobilPosition.setDelayTime((device.getSubscribeCycleForMobilePosition() * 1000L - 500L) + System.currentTimeMillis()); + subscribeTaskForMobilPosition.setDeviceId(device.getDeviceId()); + subscribeTaskForMobilPosition.setCallback(callback); + subscribeTaskForMobilPosition.setTransactionInfo(transactionInfo); + return subscribeTaskForMobilPosition; + } + + @Override + public void expired() { + if (super.getCallback() == null) { + log.info("[设备订阅到期] 移动位置订阅 未找到到期处理回调, 编号: {}", getDeviceId()); + return; + } + getCallback().run(getDeviceId(), getTransactionInfo()); + } + + @Override + public String getKey() { + return String.format("%s_%s", name, getDeviceId()); + } + + @Override + public String getName() { + return name; + } + + public static String getKey(Device device) { + return String.format("%s_%s", SubscribeTaskForMobilPosition.name, device.getDeviceId()); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformKeepaliveTask.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformKeepaliveTask.java new file mode 100644 index 0000000..a1b2f40 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformKeepaliveTask.java @@ -0,0 +1,64 @@ +package com.genersoft.iot.vmp.gb28181.task.platformStatus; + +import com.genersoft.iot.vmp.gb28181.bean.PlatformKeepaliveCallback; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +/** + * 平台心跳任务 + */ +@Slf4j +public class PlatformKeepaliveTask implements Delayed { + + @Getter + private String platformServerId; + + /** + * 超时时间(单位: 毫秒) + */ + @Getter + @Setter + private long delayTime; + + /** + * 到期回调 + */ + @Getter + private PlatformKeepaliveCallback callback; + + /** + * 心跳发送失败次数 + */ + @Getter + @Setter + private int failCount; + + public PlatformKeepaliveTask(String platformServerId, long delayTime, PlatformKeepaliveCallback callback) { + this.platformServerId = platformServerId; + this.delayTime = System.currentTimeMillis() + delayTime; + this.callback = callback; + } + + public void expired() { + if (callback == null) { + log.info("[平台心跳到期] 未找到到期处理回调, 平台上级编号: {}", platformServerId); + return; + } + getCallback().run(platformServerId, failCount); + } + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformRegisterTask.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformRegisterTask.java new file mode 100644 index 0000000..781dc96 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformRegisterTask.java @@ -0,0 +1,70 @@ +package com.genersoft.iot.vmp.gb28181.task.platformStatus; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +/** + * 平台注册任务 + */ +@Slf4j +public class PlatformRegisterTask implements Delayed { + + @Getter + private String platformServerId; + + /** + * 超时时间(单位: 毫秒) + */ + @Getter + @Setter + private long delayTime; + + @Getter + private SipTransactionInfo sipTransactionInfo; + + /** + * 到期回调 + */ + @Getter + private CommonCallback callback; + + + public PlatformRegisterTask(String platformServerId, long delayTime, SipTransactionInfo sipTransactionInfo, CommonCallback callback) { + this.platformServerId = platformServerId; + this.delayTime = System.currentTimeMillis() + delayTime; + this.callback = callback; + this.sipTransactionInfo = sipTransactionInfo; + } + + public void expired() { + if (callback == null) { + log.info("[平台注册到期] 未找到到期处理回调, 平台上级编号: {}", platformServerId); + return; + } + getCallback().run(platformServerId); + } + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } + + public PlatformRegisterTaskInfo getInfo() { + PlatformRegisterTaskInfo taskInfo = new PlatformRegisterTaskInfo(); + taskInfo.setPlatformServerId(platformServerId); + taskInfo.setSipTransactionInfo(sipTransactionInfo); + return taskInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformRegisterTaskInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformRegisterTaskInfo.java new file mode 100644 index 0000000..3b3ba6c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformRegisterTaskInfo.java @@ -0,0 +1,22 @@ +package com.genersoft.iot.vmp.gb28181.task.platformStatus; + +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +/** + * 平台注册任务可序列化的信息 + */ +@Slf4j +@Data +public class PlatformRegisterTaskInfo{ + + private String platformServerId; + + private SipTransactionInfo sipTransactionInfo; + + /** + * 过期时间,单位: 毫秒 + */ + private long expireTime; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformStatusTaskRunner.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformStatusTaskRunner.java new file mode 100644 index 0000000..a043d85 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformStatusTaskRunner.java @@ -0,0 +1,165 @@ +package com.genersoft.iot.vmp.gb28181.task.platformStatus; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.utils.redis.RedisUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class PlatformStatusTaskRunner { + + private final Map registerSubscribes = new ConcurrentHashMap<>(); + + private final DelayQueue registerDelayQueue = new DelayQueue<>(); + + private final Map keepaliveSubscribes = new ConcurrentHashMap<>(); + + private final DelayQueue keepaliveTaskDelayQueue = new DelayQueue<>(); + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private UserSetting userSetting; + + private final String prefix = "VMP_PLATFORM_STATUS"; + + // 订阅过期检查 + @Scheduled(fixedDelay = 500, timeUnit = TimeUnit.MILLISECONDS) + public void expirationCheckForRegister(){ + while (!registerDelayQueue.isEmpty()) { + PlatformRegisterTask take = null; + try { + take = registerDelayQueue.take(); + try { + removeRegisterTask(take.getPlatformServerId()); + take.expired(); + }catch (Exception e) { + log.error("[平台注册到期] 到期处理时出现异常, 平台上级编号: {} ", take.getPlatformServerId()); + } + } catch (InterruptedException e) { + log.error("[平台注册到期] ", e); + } + } + } + @Scheduled(fixedDelay = 500, timeUnit = TimeUnit.MILLISECONDS) + public void expirationCheckForKeepalive(){ + while (!keepaliveTaskDelayQueue.isEmpty()) { + PlatformKeepaliveTask take = null; + try { + take = keepaliveTaskDelayQueue.take(); + try { + removeKeepAliveTask(take.getPlatformServerId()); + take.expired(); + }catch (Exception e) { + log.error("[平台心跳到期] 到期处理时出现异常, 平台上级编号: {} ", take.getPlatformServerId()); + } + } catch (InterruptedException e) { + log.error("[平台心跳到期] ", e); + } + } + } + + public void addRegisterTask(PlatformRegisterTask task) { + Duration duration = Duration.ofSeconds((task.getDelayTime() - System.currentTimeMillis())/1000); + if (duration.getSeconds() < 0) { + return; + } + registerSubscribes.put(task.getPlatformServerId(), task); + String key = String.format("%s_%s_%s", prefix, userSetting.getServerId(), task.getPlatformServerId()); + redisTemplate.opsForValue().set(key, task.getInfo(), duration); + registerDelayQueue.offer(task); + } + + public boolean removeRegisterTask(String platformServerId) { + PlatformRegisterTask task = registerSubscribes.get(platformServerId); + if (task != null) { + registerSubscribes.remove(platformServerId); + } + String redisKey = String.format("%s_%s_%s", prefix, userSetting.getServerId(), platformServerId); + redisTemplate.delete(redisKey); + if (registerDelayQueue.contains(task)) { + boolean remove = registerDelayQueue.remove(task); + if (!remove) { + log.info("[移除平台注册任务] 从延时队列内移除失败: {}", platformServerId); + } + } + return true; + } + + public SipTransactionInfo getRegisterTransactionInfo(String platformServerId) { + PlatformRegisterTask task = registerSubscribes.get(platformServerId); + if (task == null) { + return null; + } + return task.getSipTransactionInfo(); + } + + public boolean containsRegister(String platformServerId) { + return registerSubscribes.containsKey(platformServerId); + } + + public List getAllRegisterTaskInfo(){ + return getRegisterTransactionInfoByServerId(userSetting.getServerId()); + } + + public void addKeepAliveTask(PlatformKeepaliveTask task) { + Duration duration = Duration.ofSeconds((task.getDelayTime() - System.currentTimeMillis())/1000); + if (duration.getSeconds() < 0) { + return; + } + keepaliveSubscribes.put(task.getPlatformServerId(), task); + keepaliveTaskDelayQueue.offer(task); + } + + public boolean removeKeepAliveTask(String platformServerId) { + PlatformKeepaliveTask task = keepaliveSubscribes.get(platformServerId); + if (task != null) { + keepaliveSubscribes.remove(platformServerId); + } + if (keepaliveTaskDelayQueue.contains(task)) { + boolean remove = keepaliveTaskDelayQueue.remove(task); + if (!remove) { + log.info("[移除平台心跳任务] 从延时队列内移除失败: {}", platformServerId); + } + } + return true; + } + + public boolean containsKeepAlive(String platformServerId) { + return keepaliveSubscribes.containsKey(platformServerId); + } + + public List getRegisterTransactionInfoByServerId(String serverId) { + String scanKey = String.format("%s_%s_*", prefix, serverId); + List values = RedisUtil.scan(redisTemplate, scanKey); + if (values.isEmpty()) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + for (Object value : values) { + String redisKey = (String)value; + PlatformRegisterTaskInfo taskInfo = (PlatformRegisterTaskInfo)redisTemplate.opsForValue().get(redisKey); + if (taskInfo == null) { + continue; + } + Long expire = redisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS); + taskInfo.setExpireTime(expire); + result.add(taskInfo); + } + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/ISIPProcessorObserver.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/ISIPProcessorObserver.java new file mode 100644 index 0000000..2480f37 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/ISIPProcessorObserver.java @@ -0,0 +1,6 @@ +package com.genersoft.iot.vmp.gb28181.transmit; + +import javax.sip.SipListener; + +public interface ISIPProcessorObserver extends SipListener { +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPProcessorObserver.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPProcessorObserver.java new file mode 100644 index 0000000..3ac4be1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPProcessorObserver.java @@ -0,0 +1,200 @@ +package com.genersoft.iot.vmp.gb28181.transmit; + +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.event.sip.SipEvent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.response.ISIPResponseProcessor; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.sip.*; +import javax.sip.header.CSeqHeader; +import javax.sip.header.CallIdHeader; +import javax.sip.message.Response; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @description: SIP信令处理类观察者 + * @author: panlinlin + * @date: 2021年11月5日 下午15:32 + */ +@Slf4j +@Component +public class SIPProcessorObserver implements ISIPProcessorObserver { + + private static final Map requestProcessorMap = new ConcurrentHashMap<>(); + private static final Map responseProcessorMap = new ConcurrentHashMap<>(); + + @Autowired + private SipSubscribe sipSubscribe; + + @Autowired + private EventPublisher eventPublisher; + + /** + * 添加 request订阅 + * @param method 方法名 + * @param processor 处理程序 + */ + public void addRequestProcessor(String method, ISIPRequestProcessor processor) { + requestProcessorMap.put(method, processor); + } + + /** + * 添加 response订阅 + * @param method 方法名 + * @param processor 处理程序 + */ + public void addResponseProcessor(String method, ISIPResponseProcessor processor) { + responseProcessorMap.put(method, processor); + } + + /** + * 分发RequestEvent事件 + * @param requestEvent RequestEvent事件 + */ + @Override + @Async("taskExecutor") + public void processRequest(RequestEvent requestEvent) { + String method = requestEvent.getRequest().getMethod(); + ISIPRequestProcessor sipRequestProcessor = requestProcessorMap.get(method); + if (sipRequestProcessor == null) { + log.warn("不支持方法{}的request", method); + // TODO 回复错误玛 + return; + } + requestProcessorMap.get(method).process(requestEvent); + + } + + /** + * 分发ResponseEvent事件 + * @param responseEvent responseEvent事件 + */ + @Override + @Async("taskExecutor") + public void processResponse(ResponseEvent responseEvent) { + SIPResponse response = (SIPResponse)responseEvent.getResponse(); + int status = response.getStatusCode(); + + // Success + if (((status >= Response.OK) && (status < Response.MULTIPLE_CHOICES)) || status == Response.UNAUTHORIZED) { + ISIPResponseProcessor sipRequestProcessor = responseProcessorMap.get(response.getCSeqHeader().getMethod()); + if (sipRequestProcessor != null) { + sipRequestProcessor.process(responseEvent); + } + + CallIdHeader callIdHeader = response.getCallIdHeader(); + CSeqHeader cSeqHeader = response.getCSeqHeader(); + if (callIdHeader != null) { + SipEvent sipEvent = sipSubscribe.getSubscribe(callIdHeader.getCallId() + cSeqHeader.getSeqNumber()); + if (sipEvent != null) { + if (sipEvent.getOkEvent() != null) { + SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult<>(responseEvent); + sipEvent.getOkEvent().response(eventResult); + } + sipSubscribe.removeSubscribe(callIdHeader.getCallId() + cSeqHeader.getSeqNumber()); + } + } + } else if ((status >= Response.TRYING) && (status < Response.OK)) { + // 增加其它无需回复的响应,如101、180等 + // 更新sip订阅的时间 +// sipSubscribe.updateTimeout(response.getCallIdHeader().getCallId()); + } else { + log.warn("接收到失败的response响应!status:" + status + ",message:" + response.getReasonPhrase()); + if (responseEvent.getResponse() != null && !sipSubscribe.isEmpty() ) { + CallIdHeader callIdHeader = response.getCallIdHeader(); + CSeqHeader cSeqHeader = response.getCSeqHeader(); + if (callIdHeader != null) { + SipEvent sipEvent = sipSubscribe.getSubscribe(callIdHeader.getCallId() + cSeqHeader.getSeqNumber()); + if (sipEvent != null ) { + if (sipEvent.getErrorEvent() != null) { + SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult<>(responseEvent); + sipEvent.getErrorEvent().response(eventResult); + } + sipSubscribe.removeSubscribe(callIdHeader.getCallId() + cSeqHeader.getSeqNumber()); + } + } + } + if (responseEvent.getDialog() != null) { + responseEvent.getDialog().delete(); + } + } + + + } + + /** + * 向超时订阅发送消息 + * @param timeoutEvent timeoutEvent事件 + */ + @Override + public void processTimeout(TimeoutEvent timeoutEvent) { + log.info("[消息发送超时]"); +// ClientTransaction clientTransaction = timeoutEvent.getClientTransaction(); +// +// if (clientTransaction != null) { +// log.info("[发送错误订阅] clientTransaction != null"); +// Request request = clientTransaction.getRequest(); +// if (request != null) { +// log.info("[发送错误订阅] request != null"); +// CallIdHeader callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME); +// if (callIdHeader != null) { +// log.info("[发送错误订阅]"); +// SipSubscribe.Event subscribe = sipSubscribe.getErrorSubscribe(callIdHeader.getCallId()); +// SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(timeoutEvent); +// if (subscribe != null){ +// subscribe.response(eventResult); +// } +// sipSubscribe.removeOkSubscribe(callIdHeader.getCallId()); +// sipSubscribe.removeErrorSubscribe(callIdHeader.getCallId()); +// } +// } +// } +// eventPublisher.requestTimeOut(timeoutEvent); + } + + @Override + public void processIOException(IOExceptionEvent exceptionEvent) { + System.out.println("processIOException"); + } + + @Override + public void processTransactionTerminated(TransactionTerminatedEvent transactionTerminatedEvent) { +// if (transactionTerminatedEvent.isServerTransaction()) { +// ServerTransaction serverTransaction = transactionTerminatedEvent.getServerTransaction(); +// serverTransaction.get +// } + + +// Transaction transaction = null; +// System.out.println("processTransactionTerminated"); +// if (transactionTerminatedEvent.isServerTransaction()) { +// transaction = transactionTerminatedEvent.getServerTransaction(); +// }else { +// transaction = transactionTerminatedEvent.getClientTransaction(); +// } +// +// System.out.println(transaction.getBranchId()); +// System.out.println(transaction.getState()); +// System.out.println(transaction.getRequest().getMethod()); +// CallIdHeader header = (CallIdHeader)transaction.getRequest().getHeader(CallIdHeader.NAME); +// SipSubscribe.EventResult terminatedEventEventResult = new SipSubscribe.EventResult<>(transactionTerminatedEvent); + +// sipSubscribe.getErrorSubscribe(header.getCallId()).response(terminatedEventEventResult); + } + + @Override + public void processDialogTerminated(DialogTerminatedEvent dialogTerminatedEvent) { + CallIdHeader callId = dialogTerminatedEvent.getDialog().getCallId(); + } + + + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java new file mode 100644 index 0000000..c2d9080 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java @@ -0,0 +1,170 @@ +package com.genersoft.iot.vmp.gb28181.transmit; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.gb28181.SipLayer; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.event.sip.SipEvent; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.utils.GitUtil; +import gov.nist.javax.sip.SipProviderImpl; +import gov.nist.javax.sip.address.SipUri; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.SipException; +import javax.sip.header.*; +import javax.sip.message.Message; +import javax.sip.message.Request; +import javax.sip.message.Response; +import java.text.ParseException; + +/** + * 发送SIP消息 + * + * @author lin + */ +@Slf4j +@Component +public class SIPSender { + + @Autowired + private SipLayer sipLayer; + + @Autowired + private GitUtil gitUtil; + + @Autowired + private SipSubscribe sipSubscribe; + + @Autowired + private SipConfig sipConfig; + + public void transmitRequest(String ip, Message message) throws SipException, ParseException { + transmitRequest(ip, message, null, null, null); + } + + public void transmitRequest(String ip, Message message, SipSubscribe.Event errorEvent) throws SipException, ParseException { + transmitRequest(ip, message, errorEvent, null, null); + } + + public void transmitRequest(String ip, Message message, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException { + transmitRequest(ip, message, errorEvent, okEvent, null); + } + + public void transmitRequest(String ip, Message message, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent, Long timeout) throws SipException { + ViaHeader viaHeader = (ViaHeader) message.getHeader(ViaHeader.NAME); + String transport = "UDP"; + if (viaHeader == null) { + log.warn("[消息头缺失]: ViaHeader, 使用默认的UDP方式处理数据"); + } else { + transport = viaHeader.getTransport(); + } + if (message.getHeader(UserAgentHeader.NAME) == null) { + try { + message.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + } catch (ParseException e) { + log.error("添加UserAgentHeader失败", e); + } + } + CallIdHeader callIdHeader = (CallIdHeader) message.getHeader(CallIdHeader.NAME); + CSeqHeader cSeqHeader = (CSeqHeader) message.getHeader(CSeqHeader.NAME); + String key = callIdHeader.getCallId() + cSeqHeader.getSeqNumber(); + if (okEvent != null || errorEvent != null) { + + FromHeader fromHeader = (FromHeader) message.getHeader(FromHeader.NAME); + SipEvent sipEvent = SipEvent.getInstance(key, eventResult -> { + sipSubscribe.removeSubscribe(key); + if(okEvent != null) { + okEvent.response(eventResult); + } + }, (eventResult -> { + sipSubscribe.removeSubscribe(key); + if (errorEvent != null) { + errorEvent.response(eventResult); + } + }), timeout == null ? sipConfig.getTimeout() : timeout); + SipTransactionInfo sipTransactionInfo = new SipTransactionInfo(); + sipTransactionInfo.setFromTag(fromHeader.getTag()); + sipTransactionInfo.setCallId(callIdHeader.getCallId()); + + if (message instanceof SIPResponse) { + SIPResponse response = (SIPResponse) message; + sipTransactionInfo.setToTag(response.getToHeader().getTag()); + sipTransactionInfo.setViaBranch(response.getTopmostViaHeader().getBranch()); + }else if (message instanceof SIPRequest) { + SIPRequest request = (SIPRequest) message; + sipTransactionInfo.setViaBranch(request.getTopmostViaHeader().getBranch()); + SipUri sipUri = (SipUri)request.getRequestLine().getUri(); + sipTransactionInfo.setUser(sipUri.getUser()); + } + + ExpiresHeader expiresHeader = (ExpiresHeader) message.getHeader(ExpiresHeader.NAME); + if (expiresHeader != null) { + sipTransactionInfo.setExpires(expiresHeader.getExpires()); + } + sipEvent.setSipTransactionInfo(sipTransactionInfo); + sipSubscribe.addSubscribe(key, sipEvent); + } + try { + if ("TCP".equals(transport)) { + SipProviderImpl tcpSipProvider = sipLayer.getTcpSipProvider(ip); + if (tcpSipProvider == null) { + log.error("[发送信息失败] 未找到tcp://{}的监听信息", ip); + return; + } + if (message instanceof Request) { + tcpSipProvider.sendRequest((Request) message); + } else if (message instanceof Response) { + tcpSipProvider.sendResponse((Response) message); + } + + } else if ("UDP".equals(transport)) { + SipProviderImpl sipProvider = sipLayer.getUdpSipProvider(ip); + if (sipProvider == null) { + log.error("[发送信息失败] 未找到udp://{}的监听信息", ip); + return; + } + if (message instanceof Request) { + sipProvider.sendRequest((Request) message); + } else if (message instanceof Response) { + sipProvider.sendResponse((Response) message); + } + } + }catch (SipException e) { + sipSubscribe.removeSubscribe(key); + throw e; + } + } + + public CallIdHeader getNewCallIdHeader(String ip, String transport) { + if (ObjectUtils.isEmpty(transport)) { + return sipLayer.getUdpSipProvider().getNewCallId(); + } + SipProviderImpl sipProvider; + if (ObjectUtils.isEmpty(ip)) { + sipProvider = transport.equalsIgnoreCase("TCP") ? sipLayer.getTcpSipProvider() + : sipLayer.getUdpSipProvider(); + } else { + sipProvider = transport.equalsIgnoreCase("TCP") ? sipLayer.getTcpSipProvider(ip) + : sipLayer.getUdpSipProvider(ip); + } + + if (sipProvider == null) { + sipProvider = sipLayer.getUdpSipProvider(); + } + + if (sipProvider != null) { + return sipProvider.getNewCallId(); + } else { + log.warn("[新建CallIdHeader失败], ip={}, transport={}", ip, transport); + return null; + } + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java new file mode 100755 index 0000000..d39ce28 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java @@ -0,0 +1,133 @@ +package com.genersoft.iot.vmp.gb28181.transmit.callback; + +import com.genersoft.iot.vmp.vmanager.bean.DeferredResultEx; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; +import org.springframework.web.context.request.async.DeferredResult; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @description: 异步请求处理 + * @author: swwheihei + * @date: 2020年5月8日 下午7:59:05 + */ +@SuppressWarnings(value = {"rawtypes", "unchecked"}) +@Component +public class DeferredResultHolder { + + public static final String CALLBACK_CMD_PLAY = "CALLBACK_PLAY"; + + public static final String CALLBACK_CMD_PLAYBACK = "CALLBACK_PLAYBACK"; + + public static final String CALLBACK_CMD_DOWNLOAD = "CALLBACK_DOWNLOAD"; + + + public static final String UPLOAD_FILE_CHANNEL = "UPLOAD_FILE_CHANNEL"; + + public static final String CALLBACK_CMD_MOBILE_POSITION = "CALLBACK_CMD_MOBILE_POSITION"; + + public static final String CALLBACK_CMD_SNAP= "CALLBACK_SNAP"; + + private Map> map = new ConcurrentHashMap<>(); + + + public void put(String key, String id, DeferredResultEx result) { + Map deferredResultMap = map.get(key); + if (deferredResultMap == null) { + deferredResultMap = new ConcurrentHashMap<>(); + map.put(key, deferredResultMap); + } + deferredResultMap.put(id, result); + } + + public void put(String key, String id, DeferredResult result) { + Map deferredResultMap = map.computeIfAbsent(key, k -> new ConcurrentHashMap<>()); + deferredResultMap.put(id, new DeferredResultEx(result)); + } + + public DeferredResultEx get(String key, String id) { + Map deferredResultMap = map.get(key); + if (deferredResultMap == null || ObjectUtils.isEmpty(id)) { + return null; + } + return deferredResultMap.get(id); + } + + public Collection getAllByKey(String key) { + Map deferredResultMap = map.get(key); + if (deferredResultMap == null) { + return null; + } + return deferredResultMap.values(); + } + + public boolean exist(String key, String id){ + if (key == null) { + return false; + } + Map deferredResultMap = map.get(key); + if (id == null) { + return deferredResultMap != null; + }else { + return deferredResultMap != null && deferredResultMap.get(id) != null; + } + } + + /** + * 释放单个请求 + * @param msg + */ + public void invokeResult(RequestMessage msg) { + Map deferredResultMap = map.get(msg.getKey()); + if (deferredResultMap == null) { + return; + } + DeferredResultEx result = deferredResultMap.get(msg.getId()); + if (result == null) { + return; + } + result.getDeferredResult().setResult(msg.getData()); + deferredResultMap.remove(msg.getId()); + if (deferredResultMap.size() == 0) { + map.remove(msg.getKey()); + } + } + + /** + * 释放所有的请求 + * @param msg + */ + public void invokeAllResult(RequestMessage msg) { + Map deferredResultMap = map.get(msg.getKey()); + if (deferredResultMap == null) { + return; + } + synchronized (this) { + deferredResultMap = map.get(msg.getKey()); + if (deferredResultMap == null) { + return; + } + Set ids = deferredResultMap.keySet(); + for (String id : ids) { + DeferredResultEx result = deferredResultMap.get(id); + if (result == null) { + return; + } + if (result.getFilter() != null) { + Object handler = result.getFilter().handler(msg.getData()); + result.getDeferredResult().setResult(handler); + }else { + result.getDeferredResult().setResult(msg.getData()); + } + + } + map.remove(msg.getKey()); + } + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/RequestMessage.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/RequestMessage.java new file mode 100755 index 0000000..5a22f6d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/RequestMessage.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.gb28181.transmit.callback; + +import lombok.Data; + +/** + * @description: 请求信息定义 + * @author: swwheihei + * @date: 2020年5月8日 下午1:09:18 + */ +@Data +public class RequestMessage { + + private String id; + + private String key; + + private Object data; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java new file mode 100755 index 0000000..0fffa2a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java @@ -0,0 +1,317 @@ +package com.genersoft.iot.vmp.gb28181.transmit.cmd; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import gov.nist.javax.sip.message.SIPRequest; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.List; + +/** + * @description:设备能力接口,用于定义设备的控制、查询能力 + * @author: swwheihei + * @date: 2020年5月3日 下午9:16:34 + */ +public interface ISIPCommander { + + /** + * 云台控制,支持方向与缩放控制 + * + * @param device 控制设备 + * @param channelId 预览通道 + * @param leftRight 镜头左移右移 0:停止 1:左移 2:右移 + * @param upDown 镜头上移下移 0:停止 1:上移 2:下移 + * @param inOut 镜头放大缩小 0:停止 1:缩小 2:放大 + * @param moveSpeed 镜头移动速度 + * @param zoomSpeed 镜头缩放速度 + */ + void ptzCmd(Device device,String channelId,int leftRight, int upDown, int inOut, int moveSpeed, int zoomSpeed) throws InvalidArgumentException, SipException, ParseException; + + /** + * 前端控制,包括PTZ指令、FI指令、预置位指令、巡航指令、扫描指令和辅助开关指令 + * + * @param device 控制设备 + * @param channelId 预览通道 + * @param cmdCode 指令码 + * @param parameter1 数据1 + * @param parameter2 数据2 + * @param combineCode2 组合码2 + */ + void frontEndCmd(Device device, String channelId, int cmdCode, int parameter1, int parameter2, int combineCode2) throws SipException, InvalidArgumentException, ParseException; + + /** + * 前端控制指令(用于转发上级指令) + * @param device 控制设备 + * @param channelId 预览通道 + * @param cmdString 前端控制指令串 + */ + void fronEndCmd(Device device, String channelId, String cmdString, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException; + + /** + * 请求预览视频流 + * @param device 视频设备 + * @param channel 预览通道 + */ + void playStreamCmd(MediaServer mediaServerItem, SSRCInfo ssrcInfo, Device device, DeviceChannel channel, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent, Long timeout) throws InvalidArgumentException, SipException, ParseException; + + /** + * 请求回放视频流 + * + * @param device 视频设备 + * @param channel 预览通道 + * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss + * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss + */ + void playbackStreamCmd(MediaServer mediaServerItem, SSRCInfo ssrcInf, Device device, DeviceChannel channel, String startTime, String endTime, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent, Long timeout) throws InvalidArgumentException, SipException, ParseException; + + /** + * 请求历史媒体下载 + * + * @param device 视频设备 + * @param channel 预览通道 + * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss + * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss + * @param downloadSpeed 下载倍速参数 + */ + void downloadStreamCmd(MediaServer mediaServerItem, SSRCInfo ssrcInfo, Device device, DeviceChannel channel, + String startTime, String endTime, int downloadSpeed, + SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent, Long timeout) throws InvalidArgumentException, SipException, ParseException; + + + /** + * 视频流停止 + */ + void streamByeCmd(Device device, String channelId, String app, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException; + + void talkStreamCmd(MediaServer mediaServerItem, SendRtpInfo sendRtpItem, Device device, DeviceChannel channelId, String callId, HookSubscribe.Event event, HookSubscribe.Event eventForPush, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent, Long timeout) throws InvalidArgumentException, SipException, ParseException; + + void streamByeCmd(Device device, String channelId, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException; + + /** + * 回放暂停 + */ + void playPauseCmd(Device device, DeviceChannel channel, StreamInfo streamInfo) throws InvalidArgumentException, ParseException, SipException; + + /** + * 回放恢复 + */ + void playResumeCmd(Device device, DeviceChannel channel, StreamInfo streamInfo) throws InvalidArgumentException, ParseException, SipException; + + /** + * 回放拖动播放 + */ + void playSeekCmd(Device device, DeviceChannel channel, StreamInfo streamInfo, long seekTime) throws InvalidArgumentException, ParseException, SipException; + + /** + * 回放倍速播放 + */ + void playSpeedCmd(Device device, DeviceChannel channel, StreamInfo streamInfo, Double speed) throws InvalidArgumentException, ParseException, SipException; + + /** + * 回放控制 + * @param device + * @param streamInfo + * @param content + */ + void playbackControlCmd(Device device, DeviceChannel channel, StreamInfo streamInfo, String content,SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException; + + + void streamByeCmdForDeviceInvite(Device device, String channelId, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException; + + /** + * /** + * 语音广播 + * + * @param device 视频设备 + */ + void audioBroadcastCmd(Device device, String channelId, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException; + + /** + * 音视频录像控制 + * + * @param device 视频设备 + * @param channelId 预览通道 + * @param recordCmdStr 录像命令:Record / StopRecord + */ + void recordCmd(Device device, String channelId, String recordCmdStr, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 远程启动控制命令 + * + * @param device 视频设备 + */ + void teleBootCmd(Device device) throws InvalidArgumentException, SipException, ParseException; + + /** + * 报警布防/撤防命令 + * + * @param device 视频设备 + */ + void guardCmd(Device device, String guardCmdStr, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 报警复位命令 + * + * @param device 视频设备 + * @param alarmMethod 报警方式(可选) + * @param alarmType 报警类型(可选) + */ + void alarmResetCmd(Device device, String alarmMethod, String alarmType, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 强制关键帧命令,设备收到此命令应立刻发送一个IDR帧 + * + * @param device 视频设备 + * @param channelId 预览通道 + */ + void iFrameCmd(Device device, String channelId) throws InvalidArgumentException, SipException, ParseException; + + /** + * 看守位控制命令 + * + */ + void homePositionCmd(Device device, String channelId, Boolean enabled, Integer resetTime, Integer presetIndex, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 设备配置命令 + * + * @param device 视频设备 + */ + void deviceConfigCmd(Device device); + + /** + * 设备配置命令:basicParam + */ + void deviceBasicConfigCmd(Device device, BasicParam basicParam, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 查询设备状态 + * + * @param device 视频设备 + */ + void deviceStatusQuery(Device device, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 查询设备信息 + * + * @param device 视频设备 + * @param callback + * @return + */ + void deviceInfoQuery(Device device, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 查询目录列表 + * + * @param device 视频设备 + */ + void catalogQuery(Device device, int sn, SipSubscribe.Event errorEvent) throws SipException, InvalidArgumentException, ParseException; + + /** + * 查询录像信息 + * + * @param device 视频设备 + * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss + * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss + * @param sn + */ + void recordInfoQuery(Device device, String channelId, String startTime, String endTime, int sn, Integer Secrecy, String type, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException; + + /** + * 查询报警信息 + * + * @param device 视频设备 + * @param startPriority 报警起始级别(可选) + * @param endPriority 报警终止级别(可选) + * @param alarmMethod 报警方式条件(可选) + * @param alarmType 报警类型 + * @param startTime 报警发生起始时间(可选) + * @param endTime 报警发生终止时间(可选) + * @return true = 命令发送成功 + */ + void alarmInfoQuery(Device device, String startPriority, String endPriority, String alarmMethod, + String alarmType, String startTime, String endTime, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 查询设备配置 + * + * @param device 视频设备 + * @param channelId 通道编码(可选) + * @param configType 配置类型: + */ + void deviceConfigQuery(Device device, String channelId, String configType, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 查询设备预置位置 + * + * @param device 视频设备 + */ + void presetQuery(Device device, String channelId, ErrorCallback> callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 查询移动设备位置数据 + * + * @param device 视频设备 + */ + void mobilePostitionQuery(Device device, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException; + + /** + * 订阅、取消订阅移动位置 + * + * @param device 视频设备 + * @return true = 命令发送成功 + */ + SIPRequest mobilePositionSubscribe(Device device, SipTransactionInfo transactionInfo, SipSubscribe.Event okEvent , SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException; + + /** + * 订阅、取消订阅报警信息 + * @param device 视频设备 + * @param expires 订阅过期时间(0 = 取消订阅) + * @param startPriority 报警起始级别(可选) + * @param endPriority 报警终止级别(可选) + * @param startTime 报警发生起始时间(可选) + * @param endTime 报警发生终止时间(可选) + * @return true = 命令发送成功 + */ + void alarmSubscribe(Device device, int expires, String startPriority, String endPriority, String alarmMethod, String startTime, String endTime) throws InvalidArgumentException, SipException, ParseException; + + /** + * 订阅、取消订阅目录信息 + * @param device 视频设备 + * @return true = 命令发送成功 + */ + SIPRequest catalogSubscribe(Device device, SipTransactionInfo transactionInfo, SipSubscribe.Event okEvent ,SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException; + + /** + * 拉框控制命令 + * + * @param device 控制设备 + * @param channelId 通道id + * @param cmdString 前端控制指令串 + */ + void dragZoomCmd(Device device, String channelId, String cmdString, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + + void playbackControlCmd(Device device, DeviceChannel channel, String stream, String content, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException; + + /** + * 向设备发送报警NOTIFY消息, 用于互联结构下,此时将设备当成一个平级平台看待 + * @param device 设备 + * @param deviceAlarm 报警信息信息 + * @return + */ + void sendAlarmMessage(Device device, DeviceAlarm deviceAlarm) throws InvalidArgumentException, SipException, ParseException; + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommanderForPlatform.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommanderForPlatform.java new file mode 100755 index 0000000..c94a072 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommanderForPlatform.java @@ -0,0 +1,154 @@ +package com.genersoft.iot.vmp.gb28181.transmit.cmd; + +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import javax.sip.header.WWWAuthenticateHeader; +import java.text.ParseException; +import java.util.List; + +public interface ISIPCommanderForPlatform { + + /** + * 向上级平台注册 + * + * @param parentPlatform + * @return + */ + void register(Platform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException; + + void register(Platform parentPlatform, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException; + + + void register(Platform parentPlatform, SipTransactionInfo sipTransactionInfo, WWWAuthenticateHeader www, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent, boolean isRegister) throws SipException, InvalidArgumentException, ParseException; + + /** + * 向上级平台注销 + * + * @param parentPlatform + * @return + */ + void unregister(Platform parentPlatform, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException; + + + /** + * 向上级平发送心跳信息 + * + * @param parentPlatform + * @return callId(作为接受回复的判定) + */ + String keepalive(Platform parentPlatform, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) + throws SipException, InvalidArgumentException, ParseException; + + + /** + * 向上级回复通道信息 + * + * @param channel 通道信息 + * @param parentPlatform 平台信息 + * @param sn + * @param fromTag + * @param size + * @return + */ + void catalogQuery(CommonGBChannel channel, Platform parentPlatform, String sn, String fromTag, int size) + throws SipException, InvalidArgumentException, ParseException; + + void catalogQuery(List channels, Platform parentPlatform, String sn, String fromTag) + throws InvalidArgumentException, ParseException, SipException; + + /** + * 向上级回复DeviceInfo查询信息 + * + * @param parentPlatform 平台信息 + * @param sn SN + * @param fromTag FROM头的tag信息 + * @return + */ + void deviceInfoResponse(Platform parentPlatform, Device device, String sn, String fromTag) throws SipException, InvalidArgumentException, ParseException; + + /** + * 向上级回复DeviceStatus查询信息 + * + * @param parentPlatform 平台信息 + * @param sn + * @param fromTag + * @return + */ + void deviceStatusResponse(Platform parentPlatform, String channelId, String sn, String fromTag, boolean status) throws SipException, InvalidArgumentException, ParseException; + + /** + * 向上级回复移动位置订阅消息 + * + * @param parentPlatform 平台信息 + * @param gpsMsgInfo GPS信息 + * @param subscribeInfo 订阅相关的信息 + * @return + */ + void sendNotifyMobilePosition(Platform parentPlatform, GPSMsgInfo gpsMsgInfo, CommonGBChannel channel, SubscribeInfo subscribeInfo) + throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException; + + /** + * 向上级回复报警消息 + * + * @param parentPlatform 平台信息 + * @param deviceAlarm 报警信息信息 + * @return + */ + void sendAlarmMessage(Platform parentPlatform, DeviceAlarm deviceAlarm) throws SipException, InvalidArgumentException, ParseException; + + /** + * 回复catalog事件-增加/更新 + * + * @param parentPlatform + * @param deviceChannels + */ + void sendNotifyForCatalogAddOrUpdate(String type, Platform parentPlatform, List deviceChannels, SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException; + + /** + * 回复catalog事件-删除 + * + * @param parentPlatform + * @param deviceChannels + */ + void sendNotifyForCatalogOther(String type, Platform parentPlatform, List deviceChannels, + SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException, + ParseException, NoSuchFieldException, SipException, IllegalAccessException; + + /** + * 回复recordInfo + * + * @param deviceChannel 通道信息 + * @param parentPlatform 平台信息 + * @param fromTag fromTag + * @param recordInfo 录像信息 + */ + void recordInfo(CommonGBChannel deviceChannel, Platform parentPlatform, String fromTag, RecordInfo recordInfo) + throws SipException, InvalidArgumentException, ParseException; + + /** + * 录像播放推送完成时发送MediaStatus消息 + * + * @param platform + * @param sendRtpItem + * @return + */ + void sendMediaStatusNotify(Platform platform, SendRtpInfo sendRtpItem, CommonGBChannel channel) throws SipException, InvalidArgumentException, ParseException; + + void streamByeCmd(Platform platform, SendRtpInfo sendRtpItem, CommonGBChannel channel) throws SipException, InvalidArgumentException, ParseException; + + void streamByeCmd(Platform platform, CommonGBChannel channel, String app, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException; + + void broadcastInviteCmd(Platform platform, CommonGBChannel channel, String sourceId, MediaServer mediaServerItem, + SSRCInfo ssrcInfo, HookSubscribe.Event event, SipSubscribe.Event okEvent, + SipSubscribe.Event errorEvent) throws ParseException, SipException, InvalidArgumentException; + + void broadcastResultCmd(Platform platform, CommonGBChannel deviceChannel, String sn, boolean result, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java new file mode 100755 index 0000000..f82ec20 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java @@ -0,0 +1,391 @@ +package com.genersoft.iot.vmp.gb28181.transmit.cmd; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.gb28181.SipLayer; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.GitUtil; +import com.genersoft.iot.vmp.utils.IpPortUtil; +import gov.nist.javax.sip.message.MessageFactoryImpl; +import gov.nist.javax.sip.message.SIPRequest; +import jakarta.validation.constraints.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.DigestUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.PeerUnavailableException; +import javax.sip.SipFactory; +import javax.sip.address.Address; +import javax.sip.address.SipURI; +import javax.sip.header.*; +import javax.sip.message.Request; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.UUID; + +/** + * @description: 平台命令request创造器 TODO 冗余代码太多待优化 + * @author: panll + * @date: 2020年5月6日 上午9:29:02 + */ +@Component +public class SIPRequestHeaderPlarformProvider { + + @Autowired + private SipConfig sipConfig; + + @Autowired + private SipLayer sipLayer; + + @Autowired + private GitUtil gitUtil; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + public Request createRegisterRequest(@NotNull Platform parentPlatform, long CSeq, String fromTag, String toTag, CallIdHeader callIdHeader, int expires) throws ParseException, InvalidArgumentException, PeerUnavailableException { + Request request = null; + String sipAddress = parentPlatform.getDeviceIp() + ":" + parentPlatform.getDevicePort(); + //请求行 + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), + parentPlatform.getServerIp() + ":" + parentPlatform.getServerPort()); + //via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(), + parentPlatform.getDevicePort(), parentPlatform.getTransport(), SipUtils.getNewViaTag()); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + //from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); + //to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), sipConfig.getDomain()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress,toTag); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(CSeq, Request.REGISTER); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.REGISTER, callIdHeader, + cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory() + .createSipURI(parentPlatform.getDeviceGBId(), sipAddress)); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + ExpiresHeader expiresHeader = SipFactory.getInstance().createHeaderFactory().createExpiresHeader(expires); + request.addHeader(expiresHeader); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + return request; + } + + public Request createRegisterRequest(@NotNull Platform parentPlatform, String fromTag, String toTag, + WWWAuthenticateHeader www , CallIdHeader callIdHeader, int expires) throws ParseException, PeerUnavailableException, InvalidArgumentException { + + + Request registerRequest = createRegisterRequest(parentPlatform, redisCatchStorage.getCSEQ(), fromTag, toTag, callIdHeader, expires); + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), IpPortUtil.concatenateIpAndPort(parentPlatform.getServerIp(), String.valueOf(parentPlatform.getServerPort()))); + if (www == null) { + AuthorizationHeader authorizationHeader = SipFactory.getInstance().createHeaderFactory().createAuthorizationHeader("Digest"); + String username = parentPlatform.getUsername(); + if ( username == null || username.isEmpty()) + { + authorizationHeader.setUsername(parentPlatform.getDeviceGBId()); + } else { + authorizationHeader.setUsername(username); + } + authorizationHeader.setURI(requestURI); + authorizationHeader.setAlgorithm("MD5"); + registerRequest.addHeader(authorizationHeader); + return registerRequest; + } + String realm = www.getRealm(); + String nonce = www.getNonce(); + String scheme = www.getScheme(); + + // 参考 https://blog.csdn.net/y673533511/article/details/88388138 + // qop 保护质量 包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略 + String qop = www.getQop(); + + String cNonce = null; + String nc = "00000001"; + if (qop != null) { + if ("auth".equalsIgnoreCase(qop)) { + // 客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。 + // 这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护 + cNonce = UUID.randomUUID().toString(); + + }else if ("auth-int".equalsIgnoreCase(qop)){ + // TODO + } + } + String HA1 = DigestUtils.md5DigestAsHex((parentPlatform.getDeviceGBId() + ":" + realm + ":" + parentPlatform.getPassword()).getBytes()); + String HA2=DigestUtils.md5DigestAsHex((Request.REGISTER + ":" + requestURI.toString()).getBytes()); + + StringBuffer reStr = new StringBuffer(200); + reStr.append(HA1); + reStr.append(":"); + reStr.append(nonce); + reStr.append(":"); + if (qop != null) { + reStr.append(nc); + reStr.append(":"); + reStr.append(cNonce); + reStr.append(":"); + reStr.append(qop); + reStr.append(":"); + } + reStr.append(HA2); + + String RESPONSE = DigestUtils.md5DigestAsHex(reStr.toString().getBytes()); + + AuthorizationHeader authorizationHeader = SipFactory.getInstance().createHeaderFactory().createAuthorizationHeader(scheme); + authorizationHeader.setUsername(parentPlatform.getDeviceGBId()); + authorizationHeader.setRealm(realm); + authorizationHeader.setNonce(nonce); + authorizationHeader.setURI(requestURI); + authorizationHeader.setResponse(RESPONSE); + authorizationHeader.setAlgorithm("MD5"); + if (qop != null) { + authorizationHeader.setQop(qop); + authorizationHeader.setCNonce(cNonce); + authorizationHeader.setNonceCount(1); + } + registerRequest.addHeader(authorizationHeader); + + return registerRequest; + } + + public Request createMessageRequest(Platform parentPlatform, String content, SendRtpInfo sendRtpItem) throws PeerUnavailableException, ParseException, InvalidArgumentException { + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(sendRtpItem.getCallId()); + callIdHeader.setCallId(sendRtpItem.getCallId()); + return createMessageRequest(parentPlatform, content, sendRtpItem.getToTag(), SipUtils.getNewViaTag(), sendRtpItem.getFromTag(), callIdHeader); + } + + public Request createMessageRequest(Platform parentPlatform, String content, String fromTag, String viaTag, CallIdHeader callIdHeader) throws PeerUnavailableException, ParseException, InvalidArgumentException { + return createMessageRequest(parentPlatform, content, fromTag, viaTag, null, callIdHeader); + } + + + public Request createMessageRequest(Platform parentPlatform, String content, String fromTag, String viaTag, String toTag, CallIdHeader callIdHeader) throws PeerUnavailableException, ParseException, InvalidArgumentException { + Request request = null; + String serverAddress = parentPlatform.getServerIp()+ ":" + parentPlatform.getServerPort(); + // sipuri + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), serverAddress); + // via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(), parentPlatform.getDevicePort(), + parentPlatform.getTransport(), viaTag); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + // from + // SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), parentPlatform.getDeviceIp() + ":" + parentPlatform.getDeviceIp()); + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); + // to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), serverAddress); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, toTag); + + // Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + // ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.MESSAGE); + MessageFactoryImpl messageFactory = (MessageFactoryImpl) SipFactory.getInstance().createMessageFactory(); + // 设置编码, 防止中文乱码 + messageFactory.setDefaultContentEncodingCharset(parentPlatform.getCharacterSet()); + request = messageFactory.createRequest(requestURI, Request.MESSAGE, callIdHeader, cSeqHeader, fromHeader, + toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); + request.setContent(content, contentTypeHeader); + return request; + } + + public SIPRequest createNotifyRequest(Platform parentPlatform, String content, SubscribeInfo subscribeInfo) throws PeerUnavailableException, ParseException, InvalidArgumentException { + SIPRequest request = null; + // sipuri + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), IpPortUtil.concatenateIpAndPort(parentPlatform.getServerIp(), String.valueOf(parentPlatform.getServerPort()))); + // via + ArrayList viaHeaders = new ArrayList<>(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(), parentPlatform.getDevicePort(), + parentPlatform.getTransport(), SipUtils.getNewViaTag()); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + // from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), + parentPlatform.getDeviceIp() + ":" + parentPlatform.getDevicePort()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, subscribeInfo.getTransactionInfo() != null ? subscribeInfo.getTransactionInfo() .getToTag(): subscribeInfo.getSimulatedToTag()); + // to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), parentPlatform.getServerGBDomain()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, subscribeInfo.getTransactionInfo() != null ?subscribeInfo.getTransactionInfo().getFromTag(): subscribeInfo.getSimulatedFromTag()); + + // Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + // ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.NOTIFY); + MessageFactoryImpl messageFactory = (MessageFactoryImpl) SipFactory.getInstance().createMessageFactory(); + // 设置编码, 防止中文乱码 + messageFactory.setDefaultContentEncodingCharset("gb2312"); + + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(subscribeInfo.getTransactionInfo() != null ? subscribeInfo.getTransactionInfo().getCallId(): subscribeInfo.getSimulatedCallId()); + + request = (SIPRequest) messageFactory.createRequest(requestURI, Request.NOTIFY, callIdHeader, cSeqHeader, fromHeader, + toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + EventHeader event = SipFactory.getInstance().createHeaderFactory().createEventHeader(subscribeInfo.getEventType()); + if (subscribeInfo.getEventId() != null) { + event.setEventId(subscribeInfo.getEventId()); + } + + request.addHeader(event); + + SubscriptionStateHeader active = SipFactory.getInstance().createHeaderFactory().createSubscriptionStateHeader("active"); + request.setHeader(active); + + String sipAddress = parentPlatform.getDeviceIp() + ":" + parentPlatform.getDevicePort(); + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory() + .createSipURI(parentPlatform.getDeviceGBId(), sipAddress)); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); + request.setContent(content, contentTypeHeader); + return request; + } + + public SIPRequest createByeRequest(Platform platform, SendRtpInfo sendRtpItem, CommonGBChannel channel) throws PeerUnavailableException, ParseException, InvalidArgumentException { + + if (sendRtpItem == null ) { + return null; + } + + SIPRequest request = null; + // sipuri + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(platform.getServerGBId(), IpPortUtil.concatenateIpAndPort(platform.getServerIp(), String.valueOf(platform.getServerPort()))); + // via + ArrayList viaHeaders = new ArrayList<>(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(platform.getDeviceIp(), platform.getDevicePort(), + platform.getTransport(), SipUtils.getNewViaTag()); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + // from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channel.getGbDeviceId(), + platform.getDeviceIp() + ":" + platform.getDevicePort()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, sendRtpItem.getToTag()); + // to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(platform.getServerGBId(), platform.getServerGBDomain()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, sendRtpItem.getFromTag()); + + // Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + // ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.BYE); + + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(sendRtpItem.getCallId()); + + request = (SIPRequest) SipFactory.getInstance().createMessageFactory().createRequest(requestURI, Request.BYE, callIdHeader, cSeqHeader, fromHeader, + toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + String sipAddress = platform.getDeviceIp() + ":" + platform.getDevicePort(); + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory() + .createSipURI(platform.getDeviceGBId(), sipAddress)); + + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + return request; + } + + public Request createInviteRequest(Platform platform,String sourceId, String channelId, String content, String viaTag, String fromTag, String ssrc, CallIdHeader callIdHeader) throws PeerUnavailableException, ParseException, InvalidArgumentException { + Request request = null; + //请求行 + String platformHostAddress = platform.getServerIp() + ":" + platform.getServerPort(); + String localHostAddress = sipLayer.getLocalIp(platform.getDeviceIp())+":"+ platform.getDevicePort(); + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(sourceId, platformHostAddress); + //via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(platform.getDeviceIp()), platform.getDevicePort(), platform.getTransport(), viaTag); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + + //from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(platform.getDeviceGBId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack + //to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sourceId, platformHostAddress); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress,null); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INVITE); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(),localHostAddress)); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + // Subject + SubjectHeader subjectHeader = SipFactory.getInstance().createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", sourceId, ssrc, channelId, 0)); + request.addHeader(subjectHeader); + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP"); + request.setContent(content, contentTypeHeader); + return request; + } + + public Request createByteRequest(Platform platform, String channelId, SipTransactionInfo transactionInfo) throws PeerUnavailableException, ParseException, InvalidArgumentException { + String deviceHostAddress = platform.getDeviceIp() + ":" + platform.getDevicePort(); + Request request = null; + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, deviceHostAddress); + + // via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(platform.getDeviceIp()), platform.getDevicePort(), platform.getTransport(), SipUtils.getNewViaTag()); + viaHeaders.add(viaHeader); + //from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.isAsSender()?transactionInfo.getFromTag():transactionInfo.getToTag()); + //to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, deviceHostAddress); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress,transactionInfo.isAsSender()?transactionInfo.getToTag():transactionInfo.getFromTag()); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.BYE); + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(transactionInfo.getCallId()); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(sipLayer.getLocalIp(platform.getDeviceIp()), String.valueOf(platform.getDevicePort())))); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + return request; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java new file mode 100755 index 0000000..cd50c0c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java @@ -0,0 +1,359 @@ +package com.genersoft.iot.vmp.gb28181.transmit.cmd; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.gb28181.SipLayer; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.GitUtil; +import com.genersoft.iot.vmp.utils.IpPortUtil; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.PeerUnavailableException; +import javax.sip.SipException; +import javax.sip.SipFactory; +import javax.sip.address.Address; +import javax.sip.address.SipURI; +import javax.sip.header.*; +import javax.sip.message.Request; +import java.text.ParseException; +import java.util.ArrayList; + +/** + * @description:摄像头命令request创造器 TODO 冗余代码太多待优化 + * @author: swwheihei + * @date: 2020年5月6日 上午9:29:02 + */ +@Component +public class SIPRequestHeaderProvider { + + @Autowired + private SipConfig sipConfig; + + @Autowired + private SipLayer sipLayer; + + @Autowired + private GitUtil gitUtil; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + + public Request createMessageRequest(Device device, String content, String viaTag, String fromTag, String toTag, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException { + Request request = null; + // sipuri + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); + // via + ArrayList viaHeaders = new ArrayList<>(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + // from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); + // to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, toTag); + + // Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + // ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.MESSAGE); + + request = SipFactory.getInstance().createMessageFactory().createRequest(requestURI, Request.MESSAGE, callIdHeader, cSeqHeader, fromHeader, + toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); + request.setContent(content, contentTypeHeader); + return request; + } + + public Request createInviteRequest(Device device, String channelId, String content, String viaTag, String fromTag, String toTag, String ssrc, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException { + Request request = null; + //请求行 + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + //via + ArrayList viaHeaders = new ArrayList(); + HeaderFactory headerFactory = SipFactory.getInstance().createHeaderFactory(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + + //from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack + //to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress,null); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INVITE); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(sipLayer.getLocalIp(device.getLocalIp()), String.valueOf(sipConfig.getPort())))); + // Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), device.getHost().getIp()+":"+device.getHost().getPort())); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + // Subject + SubjectHeader subjectHeader = SipFactory.getInstance().createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", channelId, ssrc, sipConfig.getId(), 0)); + request.addHeader(subjectHeader); + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP"); + request.setContent(content, contentTypeHeader); + return request; + } + + public Request createPlaybackInviteRequest(Device device, String channelId, String content, String viaTag, String fromTag, String toTag, CallIdHeader callIdHeader, String ssrc) throws ParseException, InvalidArgumentException, PeerUnavailableException { + Request request = null; + //请求行 + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + // via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + //from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack + //to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress,null); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INVITE); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(sipLayer.getLocalIp(device.getLocalIp()), String.valueOf(sipConfig.getPort())))); + // Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), device.getHost().getIp()+":"+device.getHost().getPort())); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + // Subject + SubjectHeader subjectHeader = SipFactory.getInstance().createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", channelId, ssrc, sipConfig.getId(), 0)); + request.addHeader(subjectHeader); + + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP"); + request.setContent(content, contentTypeHeader); + return request; + } + + public Request createByteRequest(Device device, String channelId, SipTransactionInfo transactionInfo) throws ParseException, InvalidArgumentException, PeerUnavailableException { + Request request = null; + //请求行 + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); +// SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); + // via + ArrayList viaHeaders = new ArrayList(); +// ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), transactionInfo.getViaBranch()); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag()); +// viaHeader.setRPort(); + viaHeaders.add(viaHeader); + //from +// SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain()); + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(sipLayer.getLocalIp(device.getLocalIp()), String.valueOf(sipConfig.getPort()))); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.getFromTag()); + //to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId,device.getHostAddress()); +// SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(),device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, transactionInfo.getToTag()); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.BYE); + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(transactionInfo.getCallId()); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(sipLayer.getLocalIp(device.getLocalIp()), String.valueOf(sipConfig.getPort())))); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + return request; + } + + public Request createByteRequestForDeviceInvite(Device device, String channelId, SipTransactionInfo transactionInfo) throws ParseException, InvalidArgumentException, PeerUnavailableException { + Request request = null; + //请求行 + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + // via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag()); + viaHeaders.add(viaHeader); + //from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.getToTag()); + //to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId,device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, transactionInfo.getFromTag()); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.BYE); + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(transactionInfo.getCallId()); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(sipLayer.getLocalIp(device.getLocalIp()), String.valueOf(sipConfig.getPort())))); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + return request; + } + + public Request createSubscribeRequest(Device device, String content, SipTransactionInfo sipTransactionInfo, Integer expires, String event, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException { + Request request = null; + // sipuri + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); + // via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), + device.getTransport(), SipUtils.getNewViaTag()); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + // from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, sipTransactionInfo == null ? SipUtils.getNewFromTag() :sipTransactionInfo.getFromTag()); + // to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, sipTransactionInfo == null ? null :sipTransactionInfo.getToTag()); + + // Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + // ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.SUBSCRIBE); + + request = SipFactory.getInstance().createMessageFactory().createRequest(requestURI, Request.SUBSCRIBE, callIdHeader, cSeqHeader, fromHeader, + toHeader, viaHeaders, maxForwards); + + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(sipLayer.getLocalIp(device.getLocalIp()), String.valueOf(sipConfig.getPort())))); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + // Expires + ExpiresHeader expireHeader = SipFactory.getInstance().createHeaderFactory().createExpiresHeader(expires); + request.addHeader(expireHeader); + + // Event + EventHeader eventHeader = SipFactory.getInstance().createHeaderFactory().createEventHeader(event); + if (sipTransactionInfo != null && sipTransactionInfo.getEventId() != null) { + eventHeader.setEventId(sipTransactionInfo.getEventId()); + }else { + int random = (int) Math.floor(Math.random() * 10000); + eventHeader.setEventId(random + ""); + } + request.addHeader(eventHeader); + + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); + request.setContent(content, contentTypeHeader); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + return request; + } + + public SIPRequest createInfoRequest(Device device, String channelId, String content, SipTransactionInfo transactionInfo) + throws SipException, ParseException, InvalidArgumentException { + if (device == null || transactionInfo == null) { + return null; + } + SIPRequest request = null; + //请求行 + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + // via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag()); + viaHeaders.add(viaHeader); + //from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.getFromTag()); + //to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId,device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, transactionInfo.getToTag()); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INFO); + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(transactionInfo.getCallId()); + request = (SIPRequest)SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.INFO, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(sipLayer.getLocalIp(device.getLocalIp()), String.valueOf(sipConfig.getPort())))); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + if (content != null) { + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", + "MANSRTSP"); + request.setContent(content, contentTypeHeader); + } + return request; + } + + public Request createAckRequest(String localIp, SipURI sipURI, SIPResponse sipResponse) throws ParseException, InvalidArgumentException, PeerUnavailableException { + + + // via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(localIp, sipConfig.getPort(), sipResponse.getTopmostViaHeader().getTransport(), SipUtils.getNewViaTag()); + viaHeaders.add(viaHeader); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(sipResponse.getCSeqHeader().getSeqNumber(), Request.ACK); + + Request request = SipFactory.getInstance().createMessageFactory().createRequest(sipURI, Request.ACK, sipResponse.getCallIdHeader(), cSeqHeader, sipResponse.getFromHeader(), sipResponse.getToHeader(), viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(localIp, String.valueOf(sipConfig.getPort())))); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + return request; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java new file mode 100755 index 0000000..eacfcdb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java @@ -0,0 +1,1438 @@ +package com.genersoft.iot.vmp.gb28181.transmit.cmd.impl; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; +import com.genersoft.iot.vmp.gb28181.SipLayer; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.MessageSubscribe; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.event.sip.MessageEvent; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.SIPRequestHeaderProvider; +import com.genersoft.iot.vmp.gb28181.utils.NumericUtil; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.DependsOn; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.ResponseEvent; +import javax.sip.SipException; +import javax.sip.SipFactory; +import javax.sip.header.CallIdHeader; +import javax.sip.message.Request; +import java.text.ParseException; +import java.util.List; + +/** + * @description:设备能力接口,用于定义设备的控制、查询能力 + * @author: swwheihei + * @date: 2020年5月3日 下午9:22:48 + */ +@Component +@DependsOn("sipLayer") +@Slf4j +public class SIPCommander implements ISIPCommander { + + @Autowired + private SipConfig sipConfig; + + @Autowired + private SipLayer sipLayer; + + @Autowired + private SIPSender sipSender; + + @Autowired + private SIPRequestHeaderProvider headerProvider; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private UserSetting userSetting; + + @Autowired + private HookSubscribe subscribe; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private MessageSubscribe messageSubscribe; + + /** + * 云台指令码计算 + * + * @param cmdCode 指令码 + * @param parameter1 数据1 + * @param parameter2 数据2 + * @param combineCode2 组合码2 + */ + public static String frontEndCmdString(int cmdCode, int parameter1, int parameter2, int combineCode2) { + StringBuilder builder = new StringBuilder("A50F01"); + String strTmp; + strTmp = String.format("%02X", cmdCode); + builder.append(strTmp, 0, 2); + strTmp = String.format("%02X", parameter1); + builder.append(strTmp, 0, 2); + strTmp = String.format("%02X", parameter2); + builder.append(strTmp, 0, 2); + strTmp = String.format("%02X", combineCode2 << 4); + builder.append(strTmp, 0, 2); + //计算校验码 + int checkCode = (0XA5 + 0X0F + 0X01 + cmdCode + parameter1 + parameter2 + (combineCode2 << 4)) % 0X100; + strTmp = String.format("%02X", checkCode); + builder.append(strTmp, 0, 2); + return builder.toString(); + } + + /** + * 云台控制,支持方向与缩放控制 + * + * @param device 控制设备 + * @param channelId 预览通道 + * @param leftRight 镜头左移右移 0:停止 1:左移 2:右移 + * @param upDown 镜头上移下移 0:停止 1:上移 2:下移 + * @param inOut 镜头放大缩小 0:停止 1:缩小 2:放大 + * @param moveSpeed 镜头移动速度 + * @param zoomSpeed 镜头缩放速度 + */ + @Override + public void ptzCmd(Device device, String channelId, int leftRight, int upDown, int inOut, int moveSpeed, + int zoomSpeed) throws InvalidArgumentException, SipException, ParseException { + String cmdStr = SipUtils.cmdString(leftRight, upDown, inOut, moveSpeed, zoomSpeed); + StringBuilder ptzXml = new StringBuilder(200); + String charset = device.getCharset(); + ptzXml.append("\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("DeviceControl\r\n"); + ptzXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + ptzXml.append("" + channelId + "\r\n"); + ptzXml.append("" + cmdStr + "\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("5\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("\r\n"); + + Request request = headerProvider.createMessageRequest(device, ptzXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null, sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()),request); + } + + /** + * 前端控制,包括PTZ指令、FI指令、预置位指令、巡航指令、扫描指令和辅助开关指令 + * + * @param device 控制设备 + * @param channelId 预览通道 + * @param cmdCode 指令码 + * @param parameter1 数据1 + * @param parameter2 数据2 + * @param combineCode2 组合码2 + */ + @Override + public void frontEndCmd(Device device, String channelId, int cmdCode, int parameter1, int parameter2, int combineCode2) throws SipException, InvalidArgumentException, ParseException { + + String cmdStr = frontEndCmdString(cmdCode, parameter1, parameter2, combineCode2); + StringBuffer ptzXml = new StringBuffer(200); + String charset = device.getCharset(); + ptzXml.append("\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("DeviceControl\r\n"); + ptzXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + ptzXml.append("" + channelId + "\r\n"); + ptzXml.append("" + cmdStr + "\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("5\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("\r\n"); + + SIPRequest request = (SIPRequest) headerProvider.createMessageRequest(device, ptzXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()),request); + + } + + /** + * 前端控制指令(用于转发上级指令) + * + * @param device 控制设备 + * @param channelId 预览通道 + * @param cmdString 前端控制指令串 + */ + @Override + public void fronEndCmd(Device device, String channelId, String cmdString, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException { + + StringBuffer ptzXml = new StringBuffer(200); + String charset = device.getCharset(); + ptzXml.append("\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("DeviceControl\r\n"); + ptzXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + ptzXml.append("" + channelId + "\r\n"); + ptzXml.append("" + cmdString + "\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("5\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("\r\n"); + + + Request request = headerProvider.createMessageRequest(device, ptzXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()),request, errorEvent, okEvent); + + } + + /** + * 请求预览视频流 + * + * @param device 视频设备 + * @param channel 预览通道 + * @param errorEvent sip错误订阅 + */ + @Override + public void playStreamCmd(MediaServer mediaServerItem, SSRCInfo ssrcInfo, Device device, DeviceChannel channel, + SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent, Long timeout) throws InvalidArgumentException, SipException, ParseException { + String stream = ssrcInfo.getStream(); + + if (device == null) { + return; + } + String sdpIp; + if (!ObjectUtils.isEmpty(device.getSdpIp())) { + sdpIp = device.getSdpIp(); + }else { + sdpIp = mediaServerItem.getSdpIp(); + } + StringBuffer content = new StringBuffer(200); + content.append("v=0\r\n"); + content.append("o=" + device.getDeviceId() + " 0 0 IN IP4 " + sdpIp + "\r\n"); + content.append("s=Play\r\n"); + content.append("c=IN IP4 " + sdpIp + "\r\n"); + content.append("t=0 0\r\n"); + + if (userSetting.getSeniorSdp()) { + if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n"); + } else if ("UDP".equalsIgnoreCase(device.getStreamMode())) { + content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 126 125 99 34 98 97\r\n"); + } + content.append("a=recvonly\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + content.append("a=fmtp:126 profile-level-id=42e01e\r\n"); + content.append("a=rtpmap:126 H264/90000\r\n"); + content.append("a=rtpmap:125 H264S/90000\r\n"); + content.append("a=fmtp:125 profile-level-id=42e01e\r\n"); + content.append("a=rtpmap:99 H265/90000\r\n"); + content.append("a=rtpmap:98 H264/90000\r\n"); + content.append("a=rtpmap:97 MPEG4/90000\r\n"); + if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) { // tcp被动模式 + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) { // tcp主动模式 + content.append("a=setup:active\r\n"); + content.append("a=connection:new\r\n"); + } + } else { + if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n"); + } else if ("UDP".equalsIgnoreCase(device.getStreamMode())) { + content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 97 98 99\r\n"); + } + content.append("a=recvonly\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + content.append("a=rtpmap:98 H264/90000\r\n"); + content.append("a=rtpmap:97 MPEG4/90000\r\n"); + content.append("a=rtpmap:99 H265/90000\r\n"); + if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) { // tcp被动模式 + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) { // tcp主动模式 + content.append("a=setup:active\r\n"); + content.append("a=connection:new\r\n"); + } + } + + if (!ObjectUtils.isEmpty(channel.getStreamIdentification())) { + content.append("a=" + channel.getStreamIdentification() + "\r\n"); + } + + content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc + // f字段:f= v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率 +// content.append("f=v/2/5/25/1/4000a/1/8/1" + "\r\n"); // 未发现支持此特性的设备 + + Request request = headerProvider.createInviteRequest(device, channel.getDeviceId(), content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null, ssrcInfo.getSsrc(),sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, (e -> { + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + errorEvent.response(e); + }), e -> { + ResponseEvent responseEvent = (ResponseEvent) e.event; + SIPResponse response = (SIPResponse) responseEvent.getResponse(); + String callId = response.getCallIdHeader().getCallId(); + SsrcTransaction ssrcTransaction = SsrcTransaction.buildForDevice(device.getDeviceId(), channel.getId(), + callId,ssrcInfo.getApp(), ssrcInfo.getStream(), ssrcInfo.getSsrc(), mediaServerItem.getId(), response, + InviteSessionType.PLAY); + sessionManager.put(ssrcTransaction); + okEvent.response(e); + }, timeout); + } + + /** + * 请求回放视频流 + * + * @param device 视频设备 + * @param channel 预览通道 + * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss + * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss + */ + @Override + public void playbackStreamCmd(MediaServer mediaServerItem, SSRCInfo ssrcInfo, Device device, DeviceChannel channel, + String startTime, String endTime, + SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent, Long timeout) throws InvalidArgumentException, SipException, ParseException { + + + log.info("{} 分配的ZLM为: {} [{}:{}]", ssrcInfo.getStream(), mediaServerItem.getId(), mediaServerItem.getSdpIp(), ssrcInfo.getPort()); + String sdpIp; + if (!ObjectUtils.isEmpty(device.getSdpIp())) { + sdpIp = device.getSdpIp(); + }else { + sdpIp = mediaServerItem.getSdpIp(); + } + StringBuffer content = new StringBuffer(200); + content.append("v=0\r\n"); + content.append("o=" + device.getDeviceId() + " 0 0 IN IP4 " + sdpIp + "\r\n"); + content.append("s=Playback\r\n"); + content.append("u=" + channel.getDeviceId() + ":0\r\n"); + content.append("c=IN IP4 " + sdpIp + "\r\n"); + content.append("t=" + DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime) + " " + + DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime) + "\r\n"); + + String streamMode = device.getStreamMode(); + + if (userSetting.getSeniorSdp()) { + if ("TCP-PASSIVE".equalsIgnoreCase(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n"); + } else if ("UDP".equalsIgnoreCase(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 126 125 99 34 98 97\r\n"); + } + content.append("a=recvonly\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + content.append("a=fmtp:126 profile-level-id=42e01e\r\n"); + content.append("a=rtpmap:126 H264/90000\r\n"); + content.append("a=rtpmap:125 H264S/90000\r\n"); + content.append("a=fmtp:125 profile-level-id=42e01e\r\n"); + content.append("a=rtpmap:99 H265/90000\r\n"); + content.append("a=rtpmap:98 H264/90000\r\n"); + content.append("a=rtpmap:97 MPEG4/90000\r\n"); + if ("TCP-PASSIVE".equalsIgnoreCase(streamMode)) { // tcp被动模式 + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(streamMode)) { // tcp主动模式 + content.append("a=setup:active\r\n"); + content.append("a=connection:new\r\n"); + } + } else { + if ("TCP-PASSIVE".equalsIgnoreCase(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n"); + } else if ("UDP".equalsIgnoreCase(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 97 98 99\r\n"); + } + content.append("a=recvonly\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + content.append("a=rtpmap:97 MPEG4/90000\r\n"); + content.append("a=rtpmap:98 H264/90000\r\n"); + content.append("a=rtpmap:99 H265/90000\r\n"); + if ("TCP-PASSIVE".equalsIgnoreCase(streamMode)) { + // tcp被动模式 + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(streamMode)) { + // tcp主动模式 + content.append("a=setup:active\r\n"); + content.append("a=connection:new\r\n"); + } + } + + //ssrc + content.append("y=" + ssrcInfo.getSsrc() + "\r\n"); + + Request request = headerProvider.createPlaybackInviteRequest(device, channel.getDeviceId(), content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()), ssrcInfo.getSsrc()); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, event -> { + ResponseEvent responseEvent = (ResponseEvent) event.event; + SIPResponse response = (SIPResponse) responseEvent.getResponse(); + SsrcTransaction ssrcTransaction = SsrcTransaction.buildForDevice(device.getDeviceId(), + channel.getId(), sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()), + device.getTransport()).getCallId(), ssrcInfo.getApp(), ssrcInfo.getStream(), ssrcInfo.getSsrc(), + mediaServerItem.getId(), response, InviteSessionType.PLAYBACK); + sessionManager.put(ssrcTransaction); + okEvent.response(event); + }, timeout); + } + + /** + * 请求历史媒体下载 + */ + @Override + public void downloadStreamCmd(MediaServer mediaServerItem, SSRCInfo ssrcInfo, Device device, DeviceChannel channel, + String startTime, String endTime, int downloadSpeed, + SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent, Long timeout) throws InvalidArgumentException, SipException, ParseException { + + log.info("[发送-请求历史媒体下载-命令] 流ID: {},节点为: {} [{}:{}]", ssrcInfo.getStream(), mediaServerItem.getId(), mediaServerItem.getSdpIp(), ssrcInfo.getPort()); + String sdpIp; + if (!ObjectUtils.isEmpty(device.getSdpIp())) { + sdpIp = device.getSdpIp(); + }else { + sdpIp = mediaServerItem.getSdpIp(); + } + StringBuffer content = new StringBuffer(200); + content.append("v=0\r\n"); + content.append("o=" + device.getDeviceId() + " 0 0 IN IP4 " + sdpIp + "\r\n"); + content.append("s=Download\r\n"); + content.append("u=" + channel.getDeviceId() + ":0\r\n"); + content.append("c=IN IP4 " + sdpIp + "\r\n"); + content.append("t=" + DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime) + " " + + DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime) + "\r\n"); + + String streamMode = device.getStreamMode().toUpperCase(); + + if (userSetting.getSeniorSdp()) { + if ("TCP-PASSIVE".equals(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n"); + } else if ("TCP-ACTIVE".equals(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n"); + } else if ("UDP".equals(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 126 125 99 34 98 97\r\n"); + } + content.append("a=recvonly\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + content.append("a=fmtp:126 profile-level-id=42e01e\r\n"); + content.append("a=rtpmap:126 H264/90000\r\n"); + content.append("a=rtpmap:125 H264S/90000\r\n"); + content.append("a=fmtp:125 profile-level-id=42e01e\r\n"); + content.append("a=rtpmap:99 MP4V-ES/90000\r\n"); + content.append("a=fmtp:99 profile-level-id=3\r\n"); + content.append("a=rtpmap:98 H264/90000\r\n"); + content.append("a=rtpmap:97 MPEG4/90000\r\n"); + if ("TCP-PASSIVE".equals(streamMode)) { // tcp被动模式 + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + } else if ("TCP-ACTIVE".equals(streamMode)) { // tcp主动模式 + content.append("a=setup:active\r\n"); + content.append("a=connection:new\r\n"); + } + } else { + if ("TCP-PASSIVE".equals(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n"); + } else if ("TCP-ACTIVE".equals(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n"); + } else if ("UDP".equals(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 97 98 99\r\n"); + } + content.append("a=recvonly\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + content.append("a=rtpmap:97 MPEG4/90000\r\n"); + content.append("a=rtpmap:98 H264/90000\r\n"); + content.append("a=rtpmap:99 H265/90000\r\n"); + if ("TCP-PASSIVE".equals(streamMode)) { // tcp被动模式 + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + } else if ("TCP-ACTIVE".equals(streamMode)) { // tcp主动模式 + content.append("a=setup:active\r\n"); + content.append("a=connection:new\r\n"); + } + } + content.append("a=downloadspeed:" + downloadSpeed + "\r\n"); + + content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc + log.debug("此时请求下载信令的ssrc===>{}",ssrcInfo.getSsrc()); + // 添加订阅 + CallIdHeader newCallIdHeader = sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()), device.getTransport()); + Request request = headerProvider.createPlaybackInviteRequest(device, channel.getDeviceId(), content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,newCallIdHeader, ssrcInfo.getSsrc()); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, event -> { + ResponseEvent responseEvent = (ResponseEvent) event.event; + SIPResponse response = (SIPResponse) responseEvent.getResponse(); + String contentString =new String(response.getRawContent()); + String ssrc = SipUtils.getSsrcFromSdp(contentString); + SsrcTransaction ssrcTransaction = SsrcTransaction.buildForDevice(device.getDeviceId(), channel.getId(), + response.getCallIdHeader().getCallId(), ssrcInfo.getApp(), ssrcInfo.getStream(), ssrc, + mediaServerItem.getId(), response, InviteSessionType.DOWNLOAD); + sessionManager.put(ssrcTransaction); + okEvent.response(event); + }, timeout); + } + + @Override + public void talkStreamCmd(MediaServer mediaServerItem, SendRtpInfo sendRtpItem, Device device, DeviceChannel channel, + String callId, HookSubscribe.Event event, HookSubscribe.Event eventForPush, SipSubscribe.Event okEvent, + SipSubscribe.Event errorEvent, Long timeout) throws InvalidArgumentException, SipException, ParseException { + + String stream = sendRtpItem.getStream(); + + if (device == null) { + return; + } + if (!mediaServerItem.isRtpEnable()) { + // 单端口暂不支持语音喊话 + log.info("[语音喊话] 单端口暂不支持此操作"); + return; + } + + log.info("[语音喊话] {} 分配的ZLM为: {} [{}:{}]", stream, mediaServerItem.getId(), mediaServerItem.getIp(), sendRtpItem.getPort()); + Hook hook = Hook.getInstance(HookType.on_media_arrival, "rtp", stream, mediaServerItem.getId()); + subscribe.addSubscribe(hook, (hookData) -> { + if (event != null) { + event.response(hookData); + subscribe.removeSubscribe(hook); + } + }); + + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()), device.getTransport()); + callIdHeader.setCallId(callId); + Hook publishHook = Hook.getInstance(HookType.on_publish, "rtp", stream, mediaServerItem.getId()); + subscribe.addSubscribe(publishHook, (hookData) -> { + if (eventForPush != null) { + eventForPush.response(hookData); + } + }); + // + StringBuffer content = new StringBuffer(200); + content.append("v=0\r\n"); + content.append("o=" + device.getDeviceId() + " 0 0 IN IP4 " + mediaServerItem.getSdpIp() + "\r\n"); + content.append("s=Talk\r\n"); + content.append("c=IN IP4 " + mediaServerItem.getSdpIp() + "\r\n"); + content.append("t=0 0\r\n"); + + content.append("m=audio " + sendRtpItem.getPort() + " TCP/RTP/AVP 8\r\n"); + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + content.append("a=sendrecv\r\n"); + content.append("a=rtpmap:8 PCMA/8000\r\n"); + + content.append("y=" + sendRtpItem.getSsrc() + "\r\n");//ssrc + // f字段:f= v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率 + content.append("f=v/////a/1/8/1" + "\r\n"); + + Request request = headerProvider.createInviteRequest(device, channel.getDeviceId(), content.toString(), + SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null, sendRtpItem.getSsrc(), callIdHeader); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, (e -> { + sessionManager.removeByStream(sendRtpItem.getApp(), sendRtpItem.getStream()); + mediaServerService.releaseSsrc(mediaServerItem.getId(), sendRtpItem.getSsrc()); + errorEvent.response(e); + }), e -> { + // 这里为例避免一个通道的点播只有一个callID这个参数使用一个固定值 + ResponseEvent responseEvent = (ResponseEvent) e.event; + SIPResponse response = (SIPResponse) responseEvent.getResponse(); + SsrcTransaction ssrcTransaction = SsrcTransaction.buildForDevice(device.getDeviceId(), channel.getId(), "talk",sendRtpItem.getApp(), stream, sendRtpItem.getSsrc(), mediaServerItem.getId(), response, InviteSessionType.TALK); + sessionManager.put(ssrcTransaction); + okEvent.response(e); + }, timeout); + } + + /** + * 视频流停止 + */ + @Override + public void streamByeCmd(Device device, String channelId, String app, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException { + if (device == null) { + log.warn("[发送BYE] device为null"); + return; + } + SsrcTransaction ssrcTransaction = null; + if (callId != null) { + ssrcTransaction = sessionManager.getSsrcTransactionByCallId(callId); + }else if (stream != null) { + ssrcTransaction = sessionManager.getSsrcTransactionByStream(app, stream); + } + + if (ssrcTransaction == null) { + log.info("[发送BYE] 未找到事务信息,设备: device: {}, channel: {}", device.getDeviceId(), channelId); + throw new SsrcTransactionNotFoundException(device.getDeviceId(), channelId, callId, stream); + } + + log.info("[发送BYE] 设备: device: {}, channel: {}, callId: {}", device.getDeviceId(), channelId, ssrcTransaction.getCallId()); + sessionManager.removeByCallId(ssrcTransaction.getCallId()); + Request byteRequest = headerProvider.createByteRequest(device, channelId, ssrcTransaction.getSipTransactionInfo()); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), byteRequest, null, okEvent); + } + + @Override + public void streamByeCmd(Device device, String channelId, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException { + Request byteRequest = headerProvider.createByteRequest(device, channelId, sipTransactionInfo); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), byteRequest, null, okEvent); + } + + @Override + public void streamByeCmdForDeviceInvite(Device device, String channelId, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException { + Request byteRequest = headerProvider.createByteRequestForDeviceInvite(device, channelId, sipTransactionInfo); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), byteRequest, null, okEvent); + } + + /** + * 语音广播 + * + * @param device 视频设备 + */ + @Override + public void audioBroadcastCmd(Device device, String channelId, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException { + StringBuffer broadcastXml = new StringBuffer(200); + String charset = device.getCharset(); + broadcastXml.append("\r\n"); + broadcastXml.append("\r\n"); + broadcastXml.append("Broadcast\r\n"); + broadcastXml.append("" + (int)((Math.random()*9+1)*100000) + "\r\n"); + broadcastXml.append("" + sipConfig.getId() + "\r\n"); + broadcastXml.append("" + channelId + "\r\n"); + broadcastXml.append("\r\n"); + + Request request = headerProvider.createMessageRequest(device, broadcastXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, okEvent); + + } + + + /** + * 音视频录像控制 + * + * @param device 视频设备 + * @param channelId 预览通道 + * @param recordCmdStr 录像命令:Record / StopRecord + */ + @Override + public void recordCmd(Device device, String channelId, String recordCmdStr, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + final String cmdType = "DeviceControl"; + final int sn = (int) ((Math.random() * 9 + 1) * 100000); + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("" + cmdType + "\r\n"); + cmdXml.append("" + sn + "\r\n"); + if (ObjectUtils.isEmpty(channelId)) { + cmdXml.append("" + device.getDeviceId() + "\r\n"); + } else { + cmdXml.append("" + channelId + "\r\n"); + } + cmdXml.append("" + recordCmdStr + "\r\n"); + cmdXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn + "", channelId, 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + },null); + } + + /** + * 远程启动控制命令 + * + * @param device 视频设备 + */ + @Override + public void teleBootCmd(Device device) throws InvalidArgumentException, SipException, ParseException { + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("DeviceControl\r\n"); + cmdXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + cmdXml.append("" + device.getDeviceId() + "\r\n"); + cmdXml.append("Boot\r\n"); + cmdXml.append("\r\n"); + + + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request); + } + + /** + * 报警布防/撤防命令 + * + * @param device 视频设备 + * @param guardCmdStr "SetGuard"/"ResetGuard" + */ + @Override + public void guardCmd(Device device, String guardCmdStr, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "DeviceControl"; + int sn = (int) ((Math.random() * 9 + 1) * 100000); + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("" + cmdType + "\r\n"); + cmdXml.append("" + sn + "\r\n"); + cmdXml.append("" + device.getDeviceId() + "\r\n"); + cmdXml.append("" + guardCmdStr + "\r\n"); + cmdXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn + "", device.getDeviceId(), 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + }); + } + + /** + * 报警复位命令 + * + * @param device 视频设备 + */ + @Override + public void alarmResetCmd(Device device, String alarmMethod, String alarmType, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "DeviceControl"; + int sn = (int) ((Math.random() * 9 + 1) * 100000); + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("" + cmdType + "\r\n"); + cmdXml.append("" + sn + "\r\n"); + cmdXml.append("" + device.getDeviceId() + "\r\n"); + cmdXml.append("ResetAlarm\r\n"); + if (!ObjectUtils.isEmpty(alarmMethod) || !ObjectUtils.isEmpty(alarmType)) { + cmdXml.append("\r\n"); + } + if (!ObjectUtils.isEmpty(alarmMethod)) { + cmdXml.append("" + alarmMethod + "\r\n"); + } + if (!ObjectUtils.isEmpty(alarmType)) { + cmdXml.append("" + alarmType + "\r\n"); + } + if (!ObjectUtils.isEmpty(alarmMethod) || !ObjectUtils.isEmpty(alarmType)) { + cmdXml.append("\r\n"); + } + cmdXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn + "", device.getDeviceId(), 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + }); + } + + /** + * 强制关键帧命令,设备收到此命令应立刻发送一个IDR帧 + * + * @param device 视频设备 + * @param channelId 预览通道 + */ + @Override + public void iFrameCmd(Device device, String channelId) throws InvalidArgumentException, SipException, ParseException { + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("DeviceControl\r\n"); + cmdXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + if (ObjectUtils.isEmpty(channelId)) { + cmdXml.append("" + device.getDeviceId() + "\r\n"); + } else { + cmdXml.append("" + channelId + "\r\n"); + } + cmdXml.append("Send\r\n"); + cmdXml.append("\r\n"); + + + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request); + } + + /** + * 看守位控制命令 + * + * @param device 视频设备 + * @param channelId 通道id,非通道则是设备本身 + * @param enabled 看守位使能:1 = 开启,0 = 关闭 + * @param resetTime 自动归位时间间隔,开启看守位时使用,单位:秒(s) + * @param presetIndex 调用预置位编号,开启看守位时使用,取值范围0~255 + */ + @Override + public void homePositionCmd(Device device, String channelId, Boolean enabled, Integer resetTime, Integer presetIndex, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "DeviceControl"; + int sn = (int) ((Math.random() * 9 + 1) * 100000); + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("" + cmdType + "\r\n"); + cmdXml.append("" + sn + "\r\n"); + if (ObjectUtils.isEmpty(channelId)) { + channelId = device.getDeviceId(); + } + cmdXml.append("" + channelId + "\r\n"); + cmdXml.append("\r\n"); + if (enabled) { + cmdXml.append("1\r\n"); + cmdXml.append("" + resetTime + "\r\n"); + cmdXml.append("" + presetIndex + "\r\n"); + } else { + cmdXml.append("0\r\n"); + } + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn + "", channelId, 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + }); + } + + /** + * 设备配置命令 + * + * @param device 视频设备 + */ + @Override + public void deviceConfigCmd(Device device) { + // TODO Auto-generated method stub + } + + /** + * 设备配置命令:basicParam + */ + @Override + public void deviceBasicConfigCmd(Device device, BasicParam basicParam, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + int sn = (int) ((Math.random() * 9 + 1) * 100000); + String cmdType = "DeviceConfig"; + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("" + cmdType + "\r\n"); + cmdXml.append("" + sn + "\r\n"); + String channelId = basicParam.getChannelId(); + if (ObjectUtils.isEmpty(channelId)) { + channelId = device.getDeviceId(); + } + cmdXml.append("" + channelId + "\r\n"); + cmdXml.append("\r\n"); + if (!ObjectUtils.isEmpty(basicParam.getName())) { + cmdXml.append("" + basicParam.getName() + "\r\n"); + } + if (NumericUtil.isInteger(basicParam.getExpiration())) { + if (Integer.parseInt(basicParam.getExpiration()) > 0) { + cmdXml.append("" + basicParam.getExpiration() + "\r\n"); + } + } + if (basicParam.getHeartBeatInterval() != null && basicParam.getHeartBeatInterval() > 0) { + cmdXml.append("" + basicParam.getHeartBeatInterval() + "\r\n"); + } + if (basicParam.getHeartBeatCount() != null && basicParam.getHeartBeatCount() > 0) { + cmdXml.append("" + basicParam.getHeartBeatCount() + "\r\n"); + } + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn + "", channelId, 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + }); + } + + /** + * 查询设备状态 + * + * @param device 视频设备 + */ + @Override + public void deviceStatusQuery(Device device, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "DeviceStatus"; + int sn = (int) ((Math.random() * 9 + 1) * 100000); + + String charset = device.getCharset(); + StringBuffer catalogXml = new StringBuffer(200); + catalogXml.append("\r\n"); + catalogXml.append("\r\n"); + catalogXml.append("" + cmdType + "\r\n"); + catalogXml.append("" + sn + "\r\n"); + catalogXml.append("" + device.getDeviceId() + "\r\n"); + catalogXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn + "", device.getDeviceId(), 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, catalogXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + }); + } + + /** + * 查询设备信息 + * + * @param device 视频设备 + * @param callback + */ + @Override + public void deviceInfoQuery(Device device, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "DeviceInfo"; + String sn = (int) ((Math.random() * 9 + 1) * 100000) + ""; + + StringBuffer catalogXml = new StringBuffer(200); + String charset = device.getCharset(); + catalogXml.append("\r\n"); + catalogXml.append("\r\n"); + catalogXml.append("" + cmdType +"\r\n"); + catalogXml.append("" + sn + "\r\n"); + catalogXml.append("" + device.getDeviceId() + "\r\n"); + catalogXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn, device.getDeviceId(), 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, catalogXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + if (callback != null) { + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + } + }); + + } + + /** + * 查询目录列表 + * + * @param device 视频设备 + */ + @Override + public void catalogQuery(Device device, int sn, SipSubscribe.Event errorEvent) throws SipException, InvalidArgumentException, ParseException { + + StringBuffer catalogXml = new StringBuffer(200); + String charset = device.getCharset(); + catalogXml.append("\r\n"); + catalogXml.append("\r\n"); + catalogXml.append(" Catalog\r\n"); + catalogXml.append(" " + sn + "\r\n"); + catalogXml.append(" " + device.getDeviceId() + "\r\n"); + catalogXml.append("\r\n"); + + Request request = headerProvider.createMessageRequest(device, catalogXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent); + } + + /** + * 查询录像信息 + * + * @param device 视频设备 + * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss + * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss + */ + @Override + public void recordInfoQuery(Device device, String channelId, String startTime, String endTime, int sn, Integer secrecy, String type, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException { + if (secrecy == null) { + secrecy = 0; + } + if (type == null) { + type = "all"; + } + + StringBuffer recordInfoXml = new StringBuffer(200); + String charset = device.getCharset(); + recordInfoXml.append("\r\n"); + recordInfoXml.append("\r\n"); + recordInfoXml.append("RecordInfo\r\n"); + recordInfoXml.append("" + sn + "\r\n"); + recordInfoXml.append("" + channelId + "\r\n"); + if (startTime != null) { + recordInfoXml.append("" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(startTime) + "\r\n"); + } + if (endTime != null) { + recordInfoXml.append("" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(endTime) + "\r\n"); + } + if (secrecy != null) { + recordInfoXml.append(" " + secrecy + " \r\n"); + } + if (type != null) { + // 大华NVR要求必须增加一个值为all的文本元素节点Type + recordInfoXml.append("" + type + "\r\n"); + } + recordInfoXml.append("\r\n"); + + + + Request request = headerProvider.createMessageRequest(device, recordInfoXml.toString(), + SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, okEvent); + } + + /** + * 查询报警信息 + * + * @param device 视频设备 + * @param startPriority 报警起始级别(可选) + * @param endPriority 报警终止级别(可选) + * @param alarmMethod 报警方式条件(可选) + * @param alarmType 报警类型 + * @param startTime 报警发生起始时间(可选) + * @param endTime 报警发生终止时间(可选) + * @return true = 命令发送成功 + */ + @Override + public void alarmInfoQuery(Device device, String startPriority, String endPriority, String alarmMethod, String alarmType, + String startTime, String endTime, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "Alarm"; + String sn = (int) ((Math.random() * 9 + 1) * 100000) + ""; + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("" + cmdType + "\r\n"); + cmdXml.append("" + sn + "\r\n"); + cmdXml.append("" + device.getDeviceId() + "\r\n"); + if (!ObjectUtils.isEmpty(startPriority)) { + cmdXml.append("" + startPriority + "\r\n"); + } + if (!ObjectUtils.isEmpty(endPriority)) { + cmdXml.append("" + endPriority + "\r\n"); + } + if (!ObjectUtils.isEmpty(alarmMethod)) { + cmdXml.append("" + alarmMethod + "\r\n"); + } + if (!ObjectUtils.isEmpty(alarmType)) { + cmdXml.append("" + alarmType + "\r\n"); + } + if (!ObjectUtils.isEmpty(startTime)) { + cmdXml.append("" + startTime + "\r\n"); + } + if (!ObjectUtils.isEmpty(endTime)) { + cmdXml.append("" + endTime + "\r\n"); + } + cmdXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn, device.getDeviceId(), 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + }); + } + + /** + * 查询设备配置 + * + * @param device 视频设备 + * @param channelId 通道编码(可选) + * @param configType 配置类型: + */ + @Override + public void deviceConfigQuery(Device device, String channelId, String configType, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "ConfigDownload"; + int sn = (int) ((Math.random() * 9 + 1) * 100000); + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("" + cmdType + "\r\n"); + cmdXml.append("" + sn + "\r\n"); + if (ObjectUtils.isEmpty(channelId)) { + cmdXml.append("" + device.getDeviceId() + "\r\n"); + } else { + cmdXml.append("" + channelId + "\r\n"); + } + cmdXml.append("" + configType + "\r\n"); + cmdXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn + "", channelId, 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + if (callback != null) { + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + } + }); + } + + /** + * 查询设备预置位置 + * + * @param device 视频设备 + */ + @Override + public void presetQuery(Device device, String channelId, ErrorCallback> callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "PresetQuery"; + int sn = (int) ((Math.random() * 9 + 1) * 100000); + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("" + cmdType + "\r\n"); + cmdXml.append("" + sn + "\r\n"); + if (ObjectUtils.isEmpty(channelId)) { + cmdXml.append("" + device.getDeviceId() + "\r\n"); + } else { + cmdXml.append("" + channelId + "\r\n"); + } + cmdXml.append("\r\n"); + + MessageEvent> messageEvent = MessageEvent.getInstance(cmdType, sn + "", channelId, 4000L, callback); + messageSubscribe.addSubscribe(messageEvent); + log.info("[预置位查询] 设备编号: {}, 通道编号: {}, SN: {}", device.getDeviceId(), channelId, sn); + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + }); + } + + /** + * 查询移动设备位置数据 + * + * @param device 视频设备 + */ + @Override + public void mobilePostitionQuery(Device device, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException { + + StringBuffer mobilePostitionXml = new StringBuffer(200); + String charset = device.getCharset(); + mobilePostitionXml.append("\r\n"); + mobilePostitionXml.append("\r\n"); + mobilePostitionXml.append("MobilePosition\r\n"); + mobilePostitionXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + mobilePostitionXml.append("" + device.getDeviceId() + "\r\n"); + mobilePostitionXml.append("60\r\n"); + mobilePostitionXml.append("\r\n"); + + + + Request request = headerProvider.createMessageRequest(device, mobilePostitionXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent); + + } + + /** + * 订阅、取消订阅移动位置 + * + * @param device 视频设备 + * @return true = 命令发送成功 + */ + @Override + public SIPRequest mobilePositionSubscribe(Device device, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException { + + StringBuffer subscribePostitionXml = new StringBuffer(200); + String charset = device.getCharset(); + subscribePostitionXml.append("\r\n"); + subscribePostitionXml.append("\r\n"); + subscribePostitionXml.append("MobilePosition\r\n"); + subscribePostitionXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + subscribePostitionXml.append("" + device.getDeviceId() + "\r\n"); + if (device.getSubscribeCycleForMobilePosition() > 0) { + subscribePostitionXml.append("" + device.getMobilePositionSubmissionInterval() + "\r\n"); + }else { + subscribePostitionXml.append("5\r\n"); + } + subscribePostitionXml.append("\r\n"); + + CallIdHeader callIdHeader; + + if (sipTransactionInfo != null) { + callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(sipTransactionInfo.getCallId()); + } else { + callIdHeader = sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()); + } + + int subscribeCycleForMobilePosition = device.getSubscribeCycleForMobilePosition(); + if (subscribeCycleForMobilePosition > 0) { + // 移动位置订阅有效期不小于 30 秒 + subscribeCycleForMobilePosition = Math.max(subscribeCycleForMobilePosition, 30); + } + SIPRequest request = (SIPRequest) headerProvider.createSubscribeRequest(device, subscribePostitionXml.toString(), sipTransactionInfo, subscribeCycleForMobilePosition, "presence",callIdHeader); //Position;id=" + tm.substring(tm.length() - 4)); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, okEvent); + return request; + } + + /** + * 订阅、取消订阅报警信息 + * + * @param device 视频设备 + * @param expires 订阅过期时间(0 = 取消订阅) + * @param startPriority 报警起始级别(可选) + * @param endPriority 报警终止级别(可选) + * @param alarmMethod 报警方式条件(可选) + * @param startTime 报警发生起始时间(可选) + * @param endTime 报警发生终止时间(可选) + * @return true = 命令发送成功 + */ + @Override + public void alarmSubscribe(Device device, int expires, String startPriority, String endPriority, String alarmMethod, String startTime, String endTime) throws InvalidArgumentException, SipException, ParseException { + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("Alarm\r\n"); + cmdXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + cmdXml.append("" + device.getDeviceId() + "\r\n"); + if (!ObjectUtils.isEmpty(startPriority)) { + cmdXml.append("" + startPriority + "\r\n"); + } + if (!ObjectUtils.isEmpty(endPriority)) { + cmdXml.append("" + endPriority + "\r\n"); + } + if (!ObjectUtils.isEmpty(alarmMethod)) { + cmdXml.append("" + alarmMethod + "\r\n"); + } + if (!ObjectUtils.isEmpty(startTime)) { + cmdXml.append("" + startTime + "\r\n"); + } + if (!ObjectUtils.isEmpty(endTime)) { + cmdXml.append("" + endTime + "\r\n"); + } + cmdXml.append("\r\n"); + + + + Request request = headerProvider.createSubscribeRequest(device, cmdXml.toString(), null, expires, "presence",sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request); + + } + + @Override + public SIPRequest catalogSubscribe(Device device, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException { + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("Catalog\r\n"); + cmdXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + cmdXml.append("" + device.getDeviceId() + "\r\n"); + cmdXml.append("\r\n"); + + CallIdHeader callIdHeader; + + if (sipTransactionInfo != null) { + callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(sipTransactionInfo.getCallId()); + } else { + callIdHeader = sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()); + } + + int subscribeCycleForCatalog = device.getSubscribeCycleForCatalog(); + if (subscribeCycleForCatalog > 0) { + // 目录订阅有效期不小于 30 秒 + subscribeCycleForCatalog = Math.max(subscribeCycleForCatalog, 30); + } + // 有效时间默认为60秒以上 + SIPRequest request = (SIPRequest) headerProvider.createSubscribeRequest(device, cmdXml.toString(), sipTransactionInfo, subscribeCycleForCatalog, "Catalog", + callIdHeader); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, okEvent); + return request; + } + + @Override + public void dragZoomCmd(Device device, String channelId, String cmdString, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "DeviceControl"; + int sn = (int) ((Math.random() * 9 + 1) * 100000); + + StringBuffer dragXml = new StringBuffer(200); + String charset = device.getCharset(); + dragXml.append("\r\n"); + dragXml.append("\r\n"); + dragXml.append("" + cmdType + "\r\n"); + dragXml.append("" + sn + "\r\n"); + if (ObjectUtils.isEmpty(channelId)) { + dragXml.append("" + device.getDeviceId() + "\r\n"); + } else { + dragXml.append("" + channelId + "\r\n"); + } + dragXml.append(cmdString); + dragXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn + "", channelId, 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, dragXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()),request); + } + + + + /** + * 回放暂停 + */ + @Override + public void playPauseCmd(Device device, DeviceChannel channel, StreamInfo streamInfo) throws InvalidArgumentException, ParseException, SipException { + StringBuffer content = new StringBuffer(200); + content.append("PAUSE RTSP/1.0\r\n"); + content.append("CSeq: " + getInfoCseq() + "\r\n"); + content.append("PauseTime: now\r\n"); + + playbackControlCmd(device, channel, streamInfo, content.toString(), null, null); + } + + + /** + * 回放恢复 + */ + @Override + public void playResumeCmd(Device device, DeviceChannel channel, StreamInfo streamInfo) throws InvalidArgumentException, ParseException, SipException { + StringBuffer content = new StringBuffer(200); + content.append("PLAY RTSP/1.0\r\n"); + content.append("CSeq: " + getInfoCseq() + "\r\n"); + content.append("Range: npt=now-\r\n"); + + playbackControlCmd(device, channel, streamInfo, content.toString(), null, null); + } + + /** + * 回放拖动播放 + */ + @Override + public void playSeekCmd(Device device, DeviceChannel channel, StreamInfo streamInfo, long seekTime) throws InvalidArgumentException, ParseException, SipException { + StringBuffer content = new StringBuffer(200); + content.append("PLAY RTSP/1.0\r\n"); + content.append("CSeq: " + getInfoCseq() + "\r\n"); + content.append("Range: npt=" + Math.abs(seekTime) + "-\r\n"); + + playbackControlCmd(device, channel, streamInfo, content.toString(), null, null); + } + + /** + * 回放倍速播放 + */ + @Override + public void playSpeedCmd(Device device, DeviceChannel channel, StreamInfo streamInfo, Double speed) throws InvalidArgumentException, ParseException, SipException { + StringBuffer content = new StringBuffer(200); + content.append("PLAY RTSP/1.0\r\n"); + content.append("CSeq: " + getInfoCseq() + "\r\n"); + content.append("Scale: " + String.format("%.6f", speed) + "\r\n"); + + playbackControlCmd(device, channel, streamInfo, content.toString(), null, null); + } + + private int getInfoCseq() { + return (int) ((Math.random() * 9 + 1) * Math.pow(10, 8)); + } + + @Override + public void playbackControlCmd(Device device, DeviceChannel channel, StreamInfo streamInfo, String content, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException { + + playbackControlCmd(device, channel, streamInfo.getStream(), content, errorEvent, okEvent); + } + + @Override + public void playbackControlCmd(Device device, DeviceChannel channel, String stream, String content, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException { + + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByStream("rtp", stream); + if (ssrcTransaction == null) { + log.info("[回放控制]未找到视频流信息,设备:{}, 流ID: {}", device.getDeviceId(), stream); + return; + } + + SIPRequest request = headerProvider.createInfoRequest(device, channel.getDeviceId(), content, ssrcTransaction.getSipTransactionInfo()); + if (request == null) { + log.info("[回放控制]构建Request信息失败,设备:{}, 流ID: {}", device.getDeviceId(), stream); + return; + } + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, okEvent); + } + + @Override + public void sendAlarmMessage(Device device, DeviceAlarm deviceAlarm) throws InvalidArgumentException, SipException, ParseException { + if (device == null) { + return; + } + log.info("[发送报警通知]设备: {}/{}->{},{}", device.getDeviceId(), deviceAlarm.getChannelId(), + deviceAlarm.getLongitude(), deviceAlarm.getLatitude()); + + String characterSet = device.getCharset(); + StringBuffer deviceStatusXml = new StringBuffer(600); + deviceStatusXml.append("\r\n"); + deviceStatusXml.append("\r\n"); + deviceStatusXml.append("Alarm\r\n"); + deviceStatusXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + deviceStatusXml.append("" + deviceAlarm.getChannelId() + "\r\n"); + deviceStatusXml.append("" + deviceAlarm.getAlarmPriority() + "\r\n"); + deviceStatusXml.append("" + deviceAlarm.getAlarmMethod() + "\r\n"); + deviceStatusXml.append("" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(deviceAlarm.getAlarmTime()) + "\r\n"); + deviceStatusXml.append("" + deviceAlarm.getAlarmDescription() + "\r\n"); + deviceStatusXml.append("" + deviceAlarm.getLongitude() + "\r\n"); + deviceStatusXml.append("" + deviceAlarm.getLatitude() + "\r\n"); + deviceStatusXml.append("\r\n"); + deviceStatusXml.append("" + deviceAlarm.getAlarmType() + "\r\n"); + deviceStatusXml.append("\r\n"); + deviceStatusXml.append("\r\n"); + + + Request request = headerProvider.createMessageRequest(device, deviceStatusXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()),request); + + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderForPlatform.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderForPlatform.java new file mode 100755 index 0000000..1df3824 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderForPlatform.java @@ -0,0 +1,757 @@ +package com.genersoft.iot.vmp.gb28181.transmit.cmd.impl; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; +import com.genersoft.iot.vmp.gb28181.SipLayer; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.SIPRequestHeaderPlarformProvider; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.GitUtil; +import gov.nist.javax.sip.message.MessageFactoryImpl; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.DependsOn; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.ResponseEvent; +import javax.sip.SipException; +import javax.sip.SipFactory; +import javax.sip.header.CallIdHeader; +import javax.sip.header.WWWAuthenticateHeader; +import javax.sip.message.Request; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +@Component +@DependsOn("sipLayer") +public class SIPCommanderForPlatform implements ISIPCommanderForPlatform { + + @Autowired + private SIPRequestHeaderPlarformProvider headerProviderPlatformProvider; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private SipLayer sipLayer; + + @Autowired + private SIPSender sipSender; + + @Autowired + private HookSubscribe subscribe; + + @Autowired + private UserSetting userSetting; + + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private GitUtil gitUtil; + + @Override + public void register(Platform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException { + register(parentPlatform, null, null, errorEvent, okEvent, true); + } + + @Override + public void register(Platform parentPlatform, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException { + + register(parentPlatform, sipTransactionInfo, null, errorEvent, okEvent, true); + } + + @Override + public void unregister(Platform parentPlatform, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException { + register(parentPlatform, sipTransactionInfo, null, errorEvent, okEvent, false); + } + + @Override + public void register(Platform parentPlatform, @Nullable SipTransactionInfo sipTransactionInfo, @Nullable WWWAuthenticateHeader www, + SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent, boolean isRegister) throws SipException, InvalidArgumentException, ParseException { + Request request; + + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport()); + String fromTag = SipUtils.getNewFromTag(); + String toTag = null; + if (sipTransactionInfo != null ) { + if (sipTransactionInfo.getCallId() != null) { + callIdHeader.setCallId(sipTransactionInfo.getCallId()); + } + if (sipTransactionInfo.getFromTag() != null) { + fromTag = sipTransactionInfo.getFromTag(); + } + if (sipTransactionInfo.getToTag() != null) { + toTag = sipTransactionInfo.getToTag(); + } + } + + if (www == null ) { + request = headerProviderPlatformProvider.createRegisterRequest(parentPlatform, + redisCatchStorage.getCSEQ(), fromTag, + toTag, callIdHeader, isRegister? parentPlatform.getExpires() : 0); + }else { + request = headerProviderPlatformProvider.createRegisterRequest(parentPlatform, fromTag, toTag, www, callIdHeader, isRegister? parentPlatform.getExpires() : 0); + } + + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request, (event)->{ + if (event != null) { + log.info("[国标级联]:{}, 注册失败: {} ", parentPlatform.getServerGBId(), event.msg); + } + if (errorEvent != null ) { + errorEvent.response(event); + } + }, okEvent, 2000L); + } + + @Override + public String keepalive(Platform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException { + log.info("[国标级联] 发送心跳, 上级平台编号: {}", parentPlatform.getServerGBId()); + String characterSet = parentPlatform.getCharacterSet(); + StringBuffer keepaliveXml = new StringBuffer(200); + keepaliveXml.append("\r\n") + .append("\r\n") + .append("Keepalive\r\n") + .append("" + (int)((Math.random()*9+1)*100000) + "\r\n") + .append("" + parentPlatform.getDeviceGBId() + "\r\n") + .append("OK\r\n") + .append("\r\n"); + + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport()); + + Request request = headerProviderPlatformProvider.createMessageRequest( + parentPlatform, + keepaliveXml.toString(), + SipUtils.getNewFromTag(), + SipUtils.getNewViaTag(), + callIdHeader); + + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request, errorEvent, okEvent); + return callIdHeader.getCallId(); + } + + /** + * 向上级回复通道信息 + * @param channel 通道信息 + * @param parentPlatform 平台信息 + */ + @Override + public void catalogQuery(CommonGBChannel channel, Platform parentPlatform, String sn, String fromTag, int size) throws SipException, InvalidArgumentException, ParseException { + + if ( parentPlatform ==null) { + return ; + } + List channels = new ArrayList<>(); + if (channel != null) { + channels.add(channel); + } + String catalogXml = getCatalogXml(channels, sn, parentPlatform, size); + + // callid + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport()); + + Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, catalogXml.toString(), fromTag, SipUtils.getNewViaTag(), callIdHeader); + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request); + + } + + @Override + public void catalogQuery(List channels, Platform parentPlatform, String sn, String fromTag) throws InvalidArgumentException, ParseException, SipException { + if ( parentPlatform ==null) { + return ; + } + sendCatalogResponse(channels, parentPlatform, sn, fromTag, 0, true); + } + private String getCatalogXml(List channels, String sn, Platform platform, int size) { + String characterSet = platform.getCharacterSet(); + StringBuffer catalogXml = new StringBuffer(600); + catalogXml.append("\r\n") + .append("\r\n") + .append("Catalog\r\n") + .append("" +sn + "\r\n") + .append("" + platform.getDeviceGBId() + "\r\n") + .append("" + size + "\r\n") + .append("\r\n"); + if (!channels.isEmpty()) { + for (CommonGBChannel channel : channels) { + catalogXml.append(channel.encode(platform.getDeviceGBId())); + } + } + + catalogXml.append("\r\n"); + catalogXml.append("\r\n"); + return catalogXml.toString(); + } + + private void sendCatalogResponse(List channels, Platform parentPlatform, String sn, String fromTag, int index, boolean sendAfterResponse) throws SipException, InvalidArgumentException, ParseException { + if (index > channels.size()) { + return; + } + String catalogXml; + if (channels.isEmpty()) { + catalogXml = getCatalogXml(Collections.emptyList(), sn, parentPlatform, 0); + }else { + List subChannelList; + if (index + parentPlatform.getCatalogGroup() < channels.size()) { + subChannelList = channels.subList(index, index + parentPlatform.getCatalogGroup()); + }else { + subChannelList = channels.subList(index, channels.size()); + } + catalogXml = getCatalogXml(subChannelList, sn, parentPlatform, channels.size()); + } + // callid + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport()); + + SIPRequest request = (SIPRequest)headerProviderPlatformProvider.createMessageRequest(parentPlatform, catalogXml, fromTag, SipUtils.getNewViaTag(), callIdHeader); + + String timeoutTaskKey = "catalog_task_" + parentPlatform.getServerGBId() + sn; + + String callId = request.getCallIdHeader().getCallId(); + + log.info("[命令发送] 国标级联{} 目录查询回复: 共{}条,已发送{}条", parentPlatform.getServerGBId(), + channels.size(), Math.min(index + parentPlatform.getCatalogGroup(), channels.size())); + log.debug(catalogXml); + if (sendAfterResponse) { + // 默认按照收到200回复后发送下一条, 如果超时收不到回复,就以30毫秒的间隔直接发送。 + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request, eventResult -> { + if (eventResult.statusCode == -1024) { + // 消息发送超时, 以30毫秒的间隔直接发送 + int indexNext = index + parentPlatform.getCatalogGroup(); + try { + sendCatalogResponse(channels, parentPlatform, sn, fromTag, indexNext, false); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 目录查询回复: {}", e.getMessage()); + } + return; + } + log.error("[目录推送失败] 国标级联 platform : {}, code: {}, msg: {}, 停止发送", parentPlatform.getServerGBId(), eventResult.statusCode, eventResult.msg); + dynamicTask.stop(timeoutTaskKey); + }, eventResult -> { + dynamicTask.stop(timeoutTaskKey); + int indexNext = index + parentPlatform.getCatalogGroup(); + try { + sendCatalogResponse(channels, parentPlatform, sn, fromTag, indexNext, true); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 目录查询回复: {}", e.getMessage()); + } + }); + }else { + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request, eventResult -> { + log.error("[目录推送失败] 国标级联 platform : {}, code: {}, msg: {}", parentPlatform.getServerGBId(), eventResult.statusCode, eventResult.msg); + }, null); + dynamicTask.startDelay(timeoutTaskKey, ()->{ + int indexNext = index + parentPlatform.getCatalogGroup(); + try { + sendCatalogResponse(channels, parentPlatform, sn, fromTag, indexNext, false); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 目录查询回复: {}", e.getMessage()); + } + }, 100); + } + } + + /** + * 向上级回复DeviceInfo查询信息 + * @param parentPlatform 平台信息 + * @param sn + * @param fromTag + * @return + */ + @Override + public void deviceInfoResponse(Platform parentPlatform, Device device, String sn, String fromTag) throws SipException, InvalidArgumentException, ParseException { + if (parentPlatform == null) { + return; + } + String deviceId = device == null ? parentPlatform.getDeviceGBId() : device.getDeviceId(); + String deviceName = device == null ? parentPlatform.getName() : device.getName(); + String manufacturer = device == null ? "WVP-28181-PRO" : device.getManufacturer(); + String model = device == null ? "platform" : device.getModel(); + String firmware = device == null ? gitUtil.getBuildVersion() : device.getFirmware(); + String characterSet = parentPlatform.getCharacterSet(); + StringBuffer deviceInfoXml = new StringBuffer(600); + deviceInfoXml.append("\r\n"); + deviceInfoXml.append("\r\n"); + deviceInfoXml.append("DeviceInfo\r\n"); + deviceInfoXml.append("" +sn + "\r\n"); + deviceInfoXml.append("" + deviceId + "\r\n"); + deviceInfoXml.append("" + deviceName + "\r\n"); + deviceInfoXml.append("" + manufacturer + "\r\n"); + deviceInfoXml.append("" + model + "\r\n"); + deviceInfoXml.append("" + firmware + "\r\n"); + deviceInfoXml.append("OK\r\n"); + deviceInfoXml.append("\r\n"); + + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport()); + + Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, deviceInfoXml.toString(), fromTag, SipUtils.getNewViaTag(), callIdHeader); + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request); + } + + + /** + * 向上级回复DeviceStatus查询信息 + * @param parentPlatform 平台信息 + * @param sn + * @param fromTag + * @return + */ + @Override + public void deviceStatusResponse(Platform parentPlatform, String channelId, String sn, String fromTag, boolean status) throws SipException, InvalidArgumentException, ParseException { + if (parentPlatform == null) { + return ; + } + String statusStr = (status)?"ONLINE":"OFFLINE"; + String characterSet = parentPlatform.getCharacterSet(); + StringBuffer deviceStatusXml = new StringBuffer(600); + deviceStatusXml.append("\r\n") + .append("\r\n") + .append("DeviceStatus\r\n") + .append("" +sn + "\r\n") + .append("" + channelId + "\r\n") + .append("OK\r\n") + .append(""+statusStr+"\r\n") + .append("OK\r\n") + .append("\r\n"); + + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport()); + + Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, deviceStatusXml.toString(), fromTag, SipUtils.getNewViaTag(), callIdHeader); + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request); + } + + @Override + public void sendNotifyMobilePosition(Platform parentPlatform, GPSMsgInfo gpsMsgInfo, CommonGBChannel channel, SubscribeInfo subscribeInfo) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException { + if (parentPlatform == null) { + return; + } + log.info("[发送 移动位置订阅] {}/{}->{},{}", parentPlatform.getServerGBId(), gpsMsgInfo.getId(), gpsMsgInfo.getLng(), gpsMsgInfo.getLat()); + if (log.isDebugEnabled()) { + log.debug("[发送 移动位置订阅] {}/{}->{},{}", parentPlatform.getServerGBId(), gpsMsgInfo.getId(), gpsMsgInfo.getLng(), gpsMsgInfo.getLat()); + } + + String characterSet = parentPlatform.getCharacterSet(); + StringBuffer deviceStatusXml = new StringBuffer(600); + deviceStatusXml.append("\r\n") + .append("\r\n") + .append("MobilePosition\r\n") + .append("" + (int)((Math.random()*9+1)*100000) + "\r\n") + .append("" + channel.getGbDeviceId() + "\r\n") + .append("\r\n") + .append("" + gpsMsgInfo.getLng() + "\r\n") + .append("" + gpsMsgInfo.getLat() + "\r\n") + .append("" + gpsMsgInfo.getSpeed() + "\r\n") + .append("" + gpsMsgInfo.getDirection() + "\r\n") + .append("" + gpsMsgInfo.getAltitude() + "\r\n") + .append("\r\n"); + + sendNotify(parentPlatform, deviceStatusXml.toString(), subscribeInfo, eventResult -> { + log.error("发送NOTIFY通知消息失败。错误:{} {}", eventResult.statusCode, eventResult.msg); + }, null); + + } + + @Override + public void sendAlarmMessage(Platform parentPlatform, DeviceAlarm deviceAlarm) throws SipException, InvalidArgumentException, ParseException { + if (parentPlatform == null) { + return; + } + log.info("[发送报警通知]平台: {}/{}->{},{}: {}", parentPlatform.getServerGBId(), deviceAlarm.getChannelId(), + deviceAlarm.getLongitude(), deviceAlarm.getLatitude(), JSON.toJSONString(deviceAlarm)); + String characterSet = parentPlatform.getCharacterSet(); + StringBuffer deviceStatusXml = new StringBuffer(600); + deviceStatusXml.append("\r\n") + .append("\r\n") + .append("Alarm\r\n") + .append("" + (int)((Math.random()*9+1)*100000) + "\r\n") + .append("" + deviceAlarm.getChannelId() + "\r\n") + .append("" + deviceAlarm.getAlarmPriority() + "\r\n") + .append("" + deviceAlarm.getAlarmMethod() + "\r\n") + .append("" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(deviceAlarm.getAlarmTime()) + "\r\n") + .append("" + deviceAlarm.getAlarmDescription() + "\r\n") + .append("" + deviceAlarm.getLongitude() + "\r\n") + .append("" + deviceAlarm.getLatitude() + "\r\n") + .append("\r\n") + .append("" + deviceAlarm.getAlarmType() + "\r\n") + .append("\r\n") + .append("\r\n"); + + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport()); + + Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, deviceStatusXml.toString(), SipUtils.getNewFromTag(), SipUtils.getNewViaTag(), callIdHeader); + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request); + + } + + @Override + public void sendNotifyForCatalogAddOrUpdate(String type, Platform parentPlatform, List deviceChannels, SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException { + if (parentPlatform == null || deviceChannels == null || deviceChannels.isEmpty() || subscribeInfo == null) { + return; + } + if (index == null) { + index = 0; + } + if (index >= deviceChannels.size()) { + return; + } + List channels; + if (index + parentPlatform.getCatalogGroup() < deviceChannels.size()) { + channels = deviceChannels.subList(index, index + parentPlatform.getCatalogGroup()); + }else { + channels = deviceChannels.subList(index, deviceChannels.size()); + } + Integer finalIndex = index; + String catalogXmlContent = getCatalogXmlContentForCatalogAddOrUpdate(parentPlatform, channels, + deviceChannels.size(), type, subscribeInfo); + log.info("[发送NOTIFY通知]类型: {},发送数量: {}", type, channels.size()); + sendNotify(parentPlatform, catalogXmlContent, subscribeInfo, eventResult -> { + log.error("发送NOTIFY通知消息失败。错误:{} {}", eventResult.statusCode, eventResult.msg); + log.error(catalogXmlContent); + }, (eventResult -> { + try { + sendNotifyForCatalogAddOrUpdate(type, parentPlatform, deviceChannels, subscribeInfo, + finalIndex + parentPlatform.getCatalogGroup()); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 NOTIFY通知: {}", e.getMessage()); + } + })); + } + + private void sendNotify(Platform parentPlatform, String catalogXmlContent, + SubscribeInfo subscribeInfo, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent ) + throws SipException, ParseException, InvalidArgumentException { + MessageFactoryImpl messageFactory = (MessageFactoryImpl) SipFactory.getInstance().createMessageFactory(); + String characterSet = parentPlatform.getCharacterSet(); + // 设置编码, 防止中文乱码 + messageFactory.setDefaultContentEncodingCharset(characterSet); + + SIPRequest notifyRequest = headerProviderPlatformProvider.createNotifyRequest(parentPlatform, catalogXmlContent, subscribeInfo); + + sipSender.transmitRequest(parentPlatform.getDeviceIp(), notifyRequest, errorEvent, okEvent); + } + + private String getCatalogXmlContentForCatalogAddOrUpdate(Platform platform, List channels, int sumNum, String type, SubscribeInfo subscribeInfo) { + StringBuffer catalogXml = new StringBuffer(600); + String characterSet = platform.getCharacterSet(); + catalogXml.append("\r\n") + .append("\r\n") + .append("Catalog\r\n") + .append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n") + .append("" + platform.getDeviceGBId() + "\r\n") + .append(""+ sumNum +"\r\n") + .append("\r\n"); + if (!channels.isEmpty()) { + for (CommonGBChannel channel : channels) { + catalogXml.append(channel.encode(type, platform.getDeviceGBId())); + } + } + catalogXml.append("\r\n") + .append("\r\n"); + return catalogXml.toString(); + } + + @Override + public void sendNotifyForCatalogOther(String type, Platform parentPlatform, List deviceChannels, + SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException { + if (parentPlatform == null + || deviceChannels == null + || deviceChannels.size() == 0 + || subscribeInfo == null) { + log.warn("[缺少必要参数]"); + return; + } + + if (index == null) { + index = 0; + } + if (index >= deviceChannels.size()) { + return; + } + List channels; + if (index + parentPlatform.getCatalogGroup() < deviceChannels.size()) { + channels = deviceChannels.subList(index, index + parentPlatform.getCatalogGroup()); + }else { + channels = deviceChannels.subList(index, deviceChannels.size()); + } + log.info("[发送NOTIFY通知]类型: {},发送数量: {}", type, channels.size()); + Integer finalIndex = index; + String catalogXmlContent = getCatalogXmlContentForCatalogOther(parentPlatform, channels, type); + sendNotify(parentPlatform, catalogXmlContent, subscribeInfo, eventResult -> { + log.error("发送NOTIFY通知消息失败。错误:{} {}", eventResult.statusCode, eventResult.msg); + }, eventResult -> { + try { + sendNotifyForCatalogOther(type, parentPlatform, deviceChannels, subscribeInfo, + finalIndex + parentPlatform.getCatalogGroup()); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 NOTIFY通知: {}", e.getMessage()); + } + }); + } + + private String getCatalogXmlContentForCatalogOther(Platform platform, List channels, String type) { + + String characterSet = platform.getCharacterSet(); + StringBuffer catalogXml = new StringBuffer(600); + catalogXml.append("\r\n") + .append("\r\n") + .append("Catalog\r\n") + .append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n") + .append("" + platform.getDeviceGBId() + "\r\n") + .append("1\r\n") + .append("\r\n"); + if (!channels.isEmpty()) { + for (CommonGBChannel channel : channels) { + catalogXml.append(channel.encode(type, platform.getDeviceGBId())); + } + } + catalogXml.append("\r\n") + .append("\r\n"); + return catalogXml.toString(); + } + @Override + public void recordInfo(CommonGBChannel deviceChannel, Platform parentPlatform, String fromTag, RecordInfo recordInfo) throws SipException, InvalidArgumentException, ParseException { + if ( parentPlatform ==null) { + return ; + } + log.info("[国标级联] 发送录像数据通道: {}", recordInfo.getChannelId()); + String characterSet = parentPlatform.getCharacterSet(); + StringBuffer recordXml = new StringBuffer(600); + recordXml.append("\r\n") + .append("\r\n") + .append("RecordInfo\r\n") + .append("" +recordInfo.getSn() + "\r\n") + .append("" + deviceChannel.getGbDeviceId() + "\r\n") + .append("" + recordInfo.getSumNum() + "\r\n"); + if (recordInfo.getRecordList() == null ) { + recordXml.append("\r\n"); + }else { + recordXml.append("\r\n"); + if (recordInfo.getRecordList().size() > 0) { + for (RecordItem recordItem : recordInfo.getRecordList()) { + recordXml.append("\r\n"); + if (deviceChannel != null) { + recordXml.append("" + deviceChannel.getGbDeviceId() + "\r\n") + .append("" + recordItem.getName() + "\r\n") + .append("" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(recordItem.getStartTime()) + "\r\n") + .append("" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(recordItem.getEndTime()) + "\r\n") + .append("" + recordItem.getSecrecy() + "\r\n") + .append("" + recordItem.getType() + "\r\n"); + if (!ObjectUtils.isEmpty(recordItem.getFileSize())) { + recordXml.append("" + recordItem.getFileSize() + "\r\n"); + } + if (!ObjectUtils.isEmpty(recordItem.getFilePath())) { + recordXml.append("" + recordItem.getFilePath() + "\r\n"); + } + } + recordXml.append("\r\n"); + } + } + } + + recordXml.append("\r\n") + .append("\r\n"); + log.debug("[国标级联] 发送录像数据通道:{}, 内容: {}", recordInfo.getChannelId(), recordXml); + // callid + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport()); + + Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, recordXml.toString(), fromTag, SipUtils.getNewViaTag(), callIdHeader); + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request, null, eventResult -> { + log.info("[国标级联] 发送录像数据通道:{}, 发送成功", recordInfo.getChannelId()); + }); + + } + + @Override + public void sendMediaStatusNotify(Platform parentPlatform, SendRtpInfo sendRtpInfo, CommonGBChannel channel) throws SipException, InvalidArgumentException, ParseException { + if (channel == null || parentPlatform == null) { + return; + } + + String characterSet = parentPlatform.getCharacterSet(); + StringBuffer mediaStatusXml = new StringBuffer(200); + mediaStatusXml.append("\r\n") + .append("\r\n") + .append("MediaStatus\r\n") + .append("" + (int)((Math.random()*9+1)*100000) + "\r\n") + .append("" + channel.getGbDeviceId() + "\r\n") + .append("121\r\n") + .append("\r\n"); + + SIPRequest messageRequest = (SIPRequest)headerProviderPlatformProvider.createMessageRequest(parentPlatform, mediaStatusXml.toString(), + sendRtpInfo); + + sipSender.transmitRequest(parentPlatform.getDeviceIp(),messageRequest); + + } + + @Override + public synchronized void streamByeCmd(Platform platform, SendRtpInfo sendRtpItem, CommonGBChannel channel) throws SipException, InvalidArgumentException, ParseException { + if (sendRtpItem == null ) { + log.info("[向上级发送BYE], sendRtpItem 为NULL"); + return; + } + if (platform == null) { + log.info("[向上级发送BYE], platform 为NULL"); + return; + } + log.info("[向上级发送BYE], {}/{}", platform.getServerGBId(), sendRtpItem.getChannelId()); + String mediaServerId = sendRtpItem.getMediaServerId(); + MediaServer mediaServerItem = mediaServerService.getOne(mediaServerId); + if (mediaServerItem != null) { + mediaServerService.releaseSsrc(mediaServerItem.getId(), sendRtpItem.getSsrc()); + mediaServerService.closeRTPServer(mediaServerItem, sendRtpItem.getStream()); + } + SIPRequest byeRequest = headerProviderPlatformProvider.createByeRequest(platform, sendRtpItem, channel); + if (byeRequest == null) { + log.warn("[向上级发送bye]:无法创建 byeRequest"); + } + sipSender.transmitRequest(platform.getDeviceIp(),byeRequest); + } + + @Override + public void streamByeCmd(Platform platform, CommonGBChannel channel, String app, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException { + + SsrcTransaction ssrcTransaction = null; + if (callId != null) { + ssrcTransaction = sessionManager.getSsrcTransactionByCallId(callId); + }else if (stream != null) { + ssrcTransaction = sessionManager.getSsrcTransactionByStream(app, stream); + } + if (ssrcTransaction == null) { + throw new SsrcTransactionNotFoundException(platform.getServerGBId(), channel.getGbDeviceId(), callId, stream); + } + + mediaServerService.releaseSsrc(ssrcTransaction.getMediaServerId(), ssrcTransaction.getSsrc()); + mediaServerService.closeRTPServer(ssrcTransaction.getMediaServerId(), ssrcTransaction.getStream()); + sessionManager.removeByStream(ssrcTransaction.getApp(), ssrcTransaction.getStream()); + + Request byteRequest = headerProviderPlatformProvider.createByteRequest(platform, channel.getGbDeviceId(), ssrcTransaction.getSipTransactionInfo()); + sipSender.transmitRequest(sipLayer.getLocalIp(platform.getDeviceIp()), byteRequest, null, okEvent); + } + + @Override + public void broadcastResultCmd(Platform platform, CommonGBChannel deviceChannel, String sn, boolean result, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException { + if (platform == null || deviceChannel == null) { + return; + } + String characterSet = platform.getCharacterSet(); + StringBuffer mediaStatusXml = new StringBuffer(200); + mediaStatusXml.append("\r\n") + .append("\r\n") + .append("Broadcast\r\n") + .append("" + sn + "\r\n") + .append("" + deviceChannel.getGbDeviceId() + "\r\n") + .append("" + (result?"OK":"ERROR") + "\r\n") + .append("\r\n"); + + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(platform.getDeviceIp(), platform.getTransport()); + + SIPRequest messageRequest = (SIPRequest)headerProviderPlatformProvider.createMessageRequest(platform, mediaStatusXml.toString(), + SipUtils.getNewFromTag(), SipUtils.getNewViaTag(), callIdHeader); + + sipSender.transmitRequest(platform.getDeviceIp(),messageRequest, errorEvent, okEvent); + } + + @Override + public void broadcastInviteCmd(Platform platform, CommonGBChannel channel,String sourceId, MediaServer mediaServerItem, + SSRCInfo ssrcInfo, HookSubscribe.Event event, SipSubscribe.Event okEvent, + SipSubscribe.Event errorEvent) throws ParseException, SipException, InvalidArgumentException { + String stream = ssrcInfo.getStream(); + + if (platform == null) { + return; + } + + log.info("{} 分配的ZLM为: {} [{}:{}]", stream, mediaServerItem.getId(), mediaServerItem.getIp(), ssrcInfo.getPort()); + Hook hook = Hook.getInstance(HookType.on_media_arrival, "rtp", stream, mediaServerItem.getId()); + subscribe.addSubscribe(hook, (hookData) -> { + if (event != null) { + event.response(hookData); + subscribe.removeSubscribe(hook); + } + }); + String sdpIp = mediaServerItem.getSdpIp(); + + StringBuffer content = new StringBuffer(200); + content.append("v=0\r\n"); + content.append("o=" + platform.getDeviceGBId() + " 0 0 IN IP4 " + sdpIp + "\r\n"); + content.append("s=Play\r\n"); + content.append("u=" + channel.getGbDeviceId() + ":0\r\n"); + content.append("c=IN IP4 " + sdpIp + "\r\n"); + content.append("t=0 0\r\n"); + + if ("TCP-PASSIVE".equalsIgnoreCase(userSetting.getBroadcastForPlatform())) { + content.append("m=audio " + ssrcInfo.getPort() + " TCP/RTP/AVP 8 96\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(userSetting.getBroadcastForPlatform())) { + content.append("m=audio " + ssrcInfo.getPort() + " TCP/RTP/AVP 8 96\r\n"); + } else if ("UDP".equalsIgnoreCase(userSetting.getBroadcastForPlatform())) { + content.append("m=audio " + ssrcInfo.getPort() + " RTP/AVP 8 96\r\n"); + } + + content.append("a=recvonly\r\n"); + content.append("a=rtpmap:8 PCMA/8000\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + if ("TCP-PASSIVE".equalsIgnoreCase(userSetting.getBroadcastForPlatform())) { + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + }else if ("TCP-ACTIVE".equalsIgnoreCase(userSetting.getBroadcastForPlatform())) { + content.append("a=setup:active\r\n"); + content.append("a=connection:new\r\n"); + } + + content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc + // f字段:f= v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率 + content.append("f=v/2/5/25/1/4096a/1/8/1\r\n"); + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(sipLayer.getLocalIp(platform.getDeviceIp()), platform.getTransport()); + + Request request = headerProviderPlatformProvider.createInviteRequest(platform, sourceId, channel.getGbDeviceId(), + content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), ssrcInfo.getSsrc(), + callIdHeader); + sipSender.transmitRequest(sipLayer.getLocalIp(platform.getDeviceIp()), request, (e -> { + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + subscribe.removeSubscribe(hook); + errorEvent.response(e); + }), e -> { + ResponseEvent responseEvent = (ResponseEvent) e.event; + SIPResponse response = (SIPResponse) responseEvent.getResponse(); + SsrcTransaction ssrcTransaction = SsrcTransaction.buildForPlatform(platform.getServerGBId(), channel.getGbId(), + callIdHeader.getCallId(), ssrcInfo.getApp(), stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response, InviteSessionType.BROADCAST); + sessionManager.put(ssrcTransaction); + okEvent.response(e); + }); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/ISIPRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/ISIPRequestProcessor.java new file mode 100755 index 0000000..8e79941 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/ISIPRequestProcessor.java @@ -0,0 +1,14 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request; + +import javax.sip.RequestEvent; + +/** + * @description: 对SIP事件进行处理,包括request, response, timeout, ioException, transactionTerminated,dialogTerminated + * @author: panlinlin + * @date: 2021年11月5日 15:47 + */ +public interface ISIPRequestProcessor { + + void process(RequestEvent event); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/SIPRequestProcessorParent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/SIPRequestProcessorParent.java new file mode 100755 index 0000000..9a2ff38 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/SIPRequestProcessorParent.java @@ -0,0 +1,234 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request; + +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.utils.IpPortUtil; +import com.google.common.primitives.Bytes; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.ObjectUtils; + +import javax.sip.*; +import javax.sip.address.Address; +import javax.sip.address.SipURI; +import javax.sip.header.ContentTypeHeader; +import javax.sip.header.ExpiresHeader; +import javax.sip.header.HeaderFactory; +import javax.sip.message.MessageFactory; +import javax.sip.message.Request; +import javax.sip.message.Response; +import java.io.ByteArrayInputStream; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @description:处理接收IPCamera发来的SIP协议请求消息 + * @author: songww + * @date: 2020年5月3日 下午4:42:22 + */ +@Slf4j +public abstract class SIPRequestProcessorParent { + + @Autowired + private SIPSender sipSender; + + public HeaderFactory getHeaderFactory() { + try { + return SipFactory.getInstance().createHeaderFactory(); + } catch (PeerUnavailableException e) { + log.error("未处理的异常 ", e); + } + return null; + } + + public MessageFactory getMessageFactory() { + try { + return SipFactory.getInstance().createMessageFactory(); + } catch (PeerUnavailableException e) { + log.error("未处理的异常 ", e); + } + return null; + } + + class ResponseAckExtraParam{ + String content; + ContentTypeHeader contentTypeHeader; + SipURI sipURI; + int expires = -1; + } + + /*** + * 回复状态码 + * 100 trying + * 200 OK + * 400 + * 404 + */ + public SIPResponse responseAck(SIPRequest sipRequest, int statusCode) throws SipException, InvalidArgumentException, ParseException { + return responseAck(sipRequest, statusCode, null); + } + + public SIPResponse responseAck(SIPRequest sipRequest, int statusCode, String msg) throws SipException, InvalidArgumentException, ParseException { + return responseAck(sipRequest, statusCode, msg, null); + } + + + public SIPResponse responseAck(SIPRequest sipRequest, int statusCode, String msg, ResponseAckExtraParam responseAckExtraParam) throws SipException, InvalidArgumentException, ParseException { + if (sipRequest.getToHeader().getTag() == null) { + sipRequest.getToHeader().setTag(SipUtils.getNewTag()); + } + SIPResponse response = (SIPResponse)getMessageFactory().createResponse(statusCode, sipRequest); + response.setStatusCode(statusCode); + if (msg != null) { + response.setReasonPhrase(msg); + } + + if (responseAckExtraParam != null) { + if (responseAckExtraParam.sipURI != null && sipRequest.getMethod().equals(Request.INVITE)) { + log.debug("responseSdpAck SipURI: {}:{}", responseAckExtraParam.sipURI.getHost(), responseAckExtraParam.sipURI.getPort()); + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress( + SipFactory.getInstance().createAddressFactory().createSipURI(responseAckExtraParam.sipURI.getUser(), IpPortUtil.concatenateIpAndPort(responseAckExtraParam.sipURI.getHost(), String.valueOf(responseAckExtraParam.sipURI.getPort())) + )); + response.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + } + if (responseAckExtraParam.contentTypeHeader != null) { + response.setContent(responseAckExtraParam.content, responseAckExtraParam.contentTypeHeader); + } + + if (sipRequest.getMethod().equals(Request.SUBSCRIBE)) { + if (responseAckExtraParam.expires == -1) { + log.error("[参数不全] 2xx的SUBSCRIBE回复,必须设置Expires header"); + }else { + ExpiresHeader expiresHeader = SipFactory.getInstance().createHeaderFactory().createExpiresHeader(responseAckExtraParam.expires); + response.addHeader(expiresHeader); + } + } + }else { + if (sipRequest.getMethod().equals(Request.SUBSCRIBE)) { + log.error("[参数不全] 2xx的SUBSCRIBE回复,必须设置Expires header"); + } + } + + // 发送response + sipSender.transmitRequest(sipRequest.getLocalAddress().getHostAddress(), response); + + return response; + } + + + + /** + * 回复带sdp的200 + */ + public SIPResponse responseSdpAck(SIPRequest request, String sdp, Platform platform) throws SipException, InvalidArgumentException, ParseException { + + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP"); + + // 兼容国标中的使用编码@域名作为RequestURI的情况 + SipURI sipURI = (SipURI)request.getRequestURI(); + if (sipURI.getPort() == -1) { + sipURI = SipFactory.getInstance().createAddressFactory().createSipURI(platform.getServerGBId(), IpPortUtil.concatenateIpAndPort(platform.getServerIp(), String.valueOf(platform.getServerPort()))); + } + ResponseAckExtraParam responseAckExtraParam = new ResponseAckExtraParam(); + responseAckExtraParam.contentTypeHeader = contentTypeHeader; + responseAckExtraParam.content = sdp; + responseAckExtraParam.sipURI = sipURI; + + SIPResponse sipResponse = responseAck(request, Response.OK, null, responseAckExtraParam); + + + return sipResponse; + } + + /** + * 回复带xml的200 + */ + public SIPResponse responseXmlAck(SIPRequest request, String xml, Platform platform, Integer expires) throws SipException, InvalidArgumentException, ParseException { + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); + + SipURI sipURI = (SipURI)request.getRequestURI(); + if (sipURI.getPort() == -1) { + sipURI = SipFactory.getInstance().createAddressFactory().createSipURI(platform.getServerGBId(), IpPortUtil.concatenateIpAndPort(platform.getServerIp(), String.valueOf(platform.getServerPort()))); + } + ResponseAckExtraParam responseAckExtraParam = new ResponseAckExtraParam(); + responseAckExtraParam.contentTypeHeader = contentTypeHeader; + responseAckExtraParam.content = xml; + responseAckExtraParam.sipURI = sipURI; + responseAckExtraParam.expires = expires; + return responseAck(request, Response.OK, null, responseAckExtraParam); + } + + public Element getRootElement(RequestEvent evt) throws DocumentException { + return getRootElement(evt, "gb2312"); + } + public Element getRootElement(RequestEvent evt, String charset) throws DocumentException { + + byte[] rawContent = evt.getRequest().getRawContent(); + if (evt.getRequest().getContentLength().getContentLength() == 0 + || rawContent == null + || rawContent.length == 0 + || ObjectUtils.isEmpty(new String(rawContent))) { + return null; + } + + if (charset == null) { + charset = "gb2312"; + } + SAXReader reader = new SAXReader(); + reader.setEncoding(charset); + // 对海康出现的未转义字符做处理。 + String[] destStrArray = new String[]{"<",">","&","'","""}; + // 或许可扩展兼容其他字符 + char despChar = '&'; + byte destBye = (byte) despChar; + List result = new ArrayList<>(); + for (int i = 0; i < rawContent.length; i++) { + if (rawContent[i] == destBye) { + boolean resul = false; + for (String destStr : destStrArray) { + if (i + destStr.length() <= rawContent.length) { + byte[] bytes = Arrays.copyOfRange(rawContent, i, i + destStr.length()); + resul = resul || (Arrays.equals(bytes,destStr.getBytes())); + } + } + if (resul) { + result.add(rawContent[i]); + } + }else { + result.add(rawContent[i]); + } + } + byte[] bytesResult = Bytes.toArray(result); + + Document xml; + try { + xml = reader.read(new ByteArrayInputStream(bytesResult)); + }catch (DocumentException e) { + log.warn("[xml解析异常]: 原文如下: \r\n{}", new String(bytesResult)); + log.warn("[xml解析异常]: 原文如下: 尝试兼容性处理"); + String[] xmlLineArray = new String(bytesResult).split("\\r?\\n"); + + // 兼容海康的address字段带有<破换xml结构导致无法解析xml的问题 + StringBuilder stringBuilder = new StringBuilder(); + for (String s : xmlLineArray) { + if (s.startsWith("{}", fromUserId); + SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callIdHeader.getCallId()); + if (sendRtpItem == null) { + log.warn("[收到ACK]:未找到来自{},callId: {}", fromUserId, callIdHeader.getCallId()); + return; + } + // tcp主动时,此时是级联下级平台,在回复200ok时,本地已经请求zlm开启监听,跳过下面步骤 + if (sendRtpItem.isTcpActive()) { + log.info("收到ACK,rtp/{} TCP主动方式等收到上级连接后开始发流", sendRtpItem.getStream()); + return; + } + MediaServer mediaServer = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + log.info("收到ACK,rtp/{}开始向上级推流, 目标={}:{},SSRC={}, 协议:{}", + sendRtpItem.getStream(), + sendRtpItem.getIp(), + sendRtpItem.getPort(), + sendRtpItem.getSsrc(), + sendRtpItem.isTcp()?(sendRtpItem.isTcpActive()?"TCP主动":"TCP被动"):"UDP" + ); + Platform parentPlatform = platformService.queryPlatformByServerGBId(fromUserId); + + if (parentPlatform != null) { + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(sendRtpItem.getChannelId()); + if (!userSetting.getServerId().equals(sendRtpItem.getServerId())) { + WVPResult wvpResult = redisRpcService.startSendRtp(callIdHeader.getCallId(), sendRtpItem); + if (wvpResult.getCode() == 0) { + redisCatchStorage.sendPlatformStartPlayMsg(sendRtpItem, deviceChannel, parentPlatform); + } + } else { + try { + if (mediaServer != null) { + if (sendRtpItem.isTcpActive()) { + mediaServerService.startSendRtpPassive(mediaServer,sendRtpItem, null); + } else { + mediaServerService.startSendRtp(mediaServer, sendRtpItem); + } + }else { + // mediaInfo 在集群的其他wvp里 + + } + + redisCatchStorage.sendPlatformStartPlayMsg(sendRtpItem, deviceChannel, parentPlatform); + }catch (ControllerException e) { + log.error("RTP推流失败: {}", e.getMessage()); + playService.startSendRtpStreamFailHand(sendRtpItem, parentPlatform, callIdHeader); + } + } + }else { + Device device = deviceService.getDeviceByDeviceId(fromUserId); + if (device == null) { + log.warn("[收到ACK]:来自{},目标为({})的推流信息为找到流体服务[{}]信息",fromUserId, toUserId, sendRtpItem.getMediaServerId()); + return; + } + // 设置为收到ACK后发送语音的设备已经在发送200OK开始发流了 + if (!device.isBroadcastPushAfterAck()) { + return; + } + if (mediaServer == null) { + log.warn("[收到ACK]:来自{},目标为({})的推流信息为找到流体服务[{}]信息",fromUserId, toUserId, sendRtpItem.getMediaServerId()); + return; + } + try { + if (sendRtpItem.isTcpActive()) { + mediaServerService.startSendRtpPassive(mediaServer, sendRtpItem, null); + } else { + mediaServerService.startSendRtp(mediaServer, sendRtpItem); + } + }catch (ControllerException e) { + log.error("RTP推流失败: {}", e.getMessage()); + playService.startSendRtpStreamFailHand(sendRtpItem, null, callIdHeader); + } + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java new file mode 100755 index 0000000..bacf504 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java @@ -0,0 +1,258 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.*; +import com.genersoft.iot.vmp.gb28181.session.AudioBroadcastManager; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.CallIdHeader; +import javax.sip.message.Response; +import java.text.ParseException; + +/** + * SIP命令类型: BYE请求 + */ +@Slf4j +@Component +public class ByeRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor { + + private final String method = "BYE"; + + @Autowired + private ISIPCommander cmder; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private IPlatformService platformService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private AudioBroadcastManager audioBroadcastManager; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private IPlayService playService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IRedisRpcService redisRpcService; + + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addRequestProcessor(method, this); + } + + /** + * 处理BYE请求 + */ + @Override + public void process(RequestEvent evt) { + SIPRequest request = (SIPRequest) evt.getRequest(); + try { + responseAck(request, Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[回复BYE信息失败],{}", e.getMessage()); + } + CallIdHeader callIdHeader = (CallIdHeader)evt.getRequest().getHeader(CallIdHeader.NAME); + SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callIdHeader.getCallId()); + + // 收流端发送的停止 + if (sendRtpItem != null){ + CommonGBChannel channel = channelService.getOne(sendRtpItem.getChannelId()); + log.info("[收到bye] 来自{},停止通道:{}, 类型: {}, callId: {}", sendRtpItem.getTargetId(), channel.getGbDeviceId(), sendRtpItem.getPlayType(), callIdHeader.getCallId()); + + String streamId = sendRtpItem.getStream(); + log.info("[收到bye] 停止推流:{}, 媒体节点: {}", streamId, sendRtpItem.getMediaServerId()); + + if (sendRtpItem.getPlayType().equals(InviteStreamType.PUSH)) { + // 不是本平台的就发送redis消息让其他wvp停止发流 + Platform platform = platformService.queryPlatformByServerGBId(sendRtpItem.getTargetId()); + if (platform != null) { + redisCatchStorage.sendPlatformStopPlayMsg(sendRtpItem, platform, channel); + if (!userSetting.getServerId().equals(sendRtpItem.getServerId())) { + redisRpcService.stopSendRtp(sendRtpItem.getCallId()); + sendRtpServerService.deleteByCallId(sendRtpItem.getCallId()); + }else { + MediaServer mediaServer = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + sendRtpServerService.deleteByCallId(callIdHeader.getCallId()); + if (mediaServer != null) { + mediaServerService.stopSendRtp(mediaServer, sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getSsrc()); + if (userSetting.getUseCustomSsrcForParentInvite()) { + mediaServerService.releaseSsrc(mediaServer.getId(), sendRtpItem.getSsrc()); + } + } + } + }else { + log.info("[上级平台停止观看] 未找到平台{}的信息,发送redis消息失败", sendRtpItem.getTargetId()); + } + }else { + MediaServer mediaInfo = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + sendRtpServerService.delete(sendRtpItem); + mediaServerService.stopSendRtp(mediaInfo, sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getSsrc()); + if (userSetting.getUseCustomSsrcForParentInvite()) { + mediaServerService.releaseSsrc(mediaInfo.getId(), sendRtpItem.getSsrc()); + } + } + if (sendRtpItem.getServerId().equals(userSetting.getServerId())) { + MediaServer mediaServer = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + if (mediaServer != null) { + AudioBroadcastCatch audioBroadcastCatch = audioBroadcastManager.get(sendRtpItem.getChannelId()); + if (audioBroadcastCatch != null && audioBroadcastCatch.getSipTransactionInfo().getCallId().equals(callIdHeader.getCallId())) { + // 来自上级平台的停止对讲 + log.info("[停止对讲] 来自上级,平台:{}, 通道:{}", sendRtpItem.getTargetId(), sendRtpItem.getChannelId()); + audioBroadcastManager.del(sendRtpItem.getChannelId()); + } + + MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, sendRtpItem.getApp(), streamId); + + if (mediaInfo != null && mediaInfo.getReaderCount() <= 0) { + log.info("[收到bye] {} 无其它观看者,通知设备停止推流", streamId); + if (sendRtpItem.getPlayType().equals(InviteStreamType.PLAY)) { + Device device = deviceService.getDeviceByDeviceId(sendRtpItem.getTargetId()); + if (device == null) { + log.info("[收到bye] {} 通知设备停止推流时未找到设备信息", streamId); + return; + } + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(sendRtpItem.getChannelId()); + if (deviceChannel == null) { + log.info("[收到bye] {} 通知设备停止推流时未找到通道信息", streamId); + return; + } + try { + log.info("[停止点播] {}/{}", sendRtpItem.getTargetId(), sendRtpItem.getChannelId()); + cmder.streamByeCmd(device, deviceChannel.getDeviceId(), sendRtpItem.getApp(), sendRtpItem.getStream(), null, null); + } catch (InvalidArgumentException | ParseException | SipException | + SsrcTransactionNotFoundException e) { + log.error("[收到bye] {} 无其它观看者,通知设备停止推流, 发送BYE失败 {}",streamId, e.getMessage()); + } + } + } + } + } else { + // TODO 流再其他wvp上时应该通知这个wvp停止推流和发送BYE + + } + } + // 可能是设备发送的停止 + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByCallId(callIdHeader.getCallId()); + if (ssrcTransaction == null) { + return; + } + log.info("[收到bye] 来自:{}, 通道: {}, 类型: {}", ssrcTransaction.getDeviceId(), ssrcTransaction.getChannelId(), ssrcTransaction.getType()); + // TODO 结束点播 避免等待 + + if (ssrcTransaction.getPlatformId() != null ) { + Platform platform = platformService.queryPlatformByServerGBId(ssrcTransaction.getPlatformId()); + if (ssrcTransaction.getType().equals(InviteSessionType.BROADCAST)) { + log.info("[收到bye] 上级停止语音对讲,来自:{}, 通道已停止推流: {}", ssrcTransaction.getPlatformId(), ssrcTransaction.getChannelId()); + CommonGBChannel channel = channelService.getOne(ssrcTransaction.getChannelId()); + if (channel == null) { + log.info("[收到bye] 未找到通道,上级:{}, 通道:{}", ssrcTransaction.getPlatformId(), ssrcTransaction.getChannelId()); + return; + } + String mediaServerId = ssrcTransaction.getMediaServerId(); + platformService.stopBroadcast(platform, channel, ssrcTransaction.getApp(), ssrcTransaction.getStream(), false, + mediaServerService.getOne(mediaServerId)); + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + Device device = deviceService.getDevice(channel.getDataDeviceId()); + playService.stopAudioBroadcast(device, deviceChannel); + } + + }else { + Device device = deviceService.getDeviceByDeviceId(ssrcTransaction.getDeviceId()); + if (device == null) { + log.info("[收到bye] 未找到设备:{} ", ssrcTransaction.getDeviceId()); + return; + } + DeviceChannel channel = deviceChannelService.getOneForSourceById(ssrcTransaction.getChannelId()); + if (channel == null) { + log.info("[收到bye] 未找到通道,设备:{}, 通道:{}", ssrcTransaction.getDeviceId(), ssrcTransaction.getChannelId()); + return; + } + switch (ssrcTransaction.getType()){ + case PLAY: + case PLAYBACK: + case DOWNLOAD: + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + if (inviteInfo != null) { + deviceChannelService.stopPlay(channel.getId()); + inviteStreamService.removeInviteInfo(inviteInfo); + if (inviteInfo.getStreamInfo() != null) { + mediaServerService.closeRTPServer(inviteInfo.getStreamInfo().getMediaServer(), inviteInfo.getStreamInfo().getStream()); + } + } + break; + case BROADCAST: + case TALK: + // 查找来源的对讲设备,发送停止 + Device sourceDevice = deviceService.getDeviceByChannelId(ssrcTransaction.getChannelId()); + AudioBroadcastCatch audioBroadcastCatch = audioBroadcastManager.get(channel.getId()); + if (sourceDevice != null) { + playService.stopAudioBroadcast(sourceDevice, channel); + } + if (audioBroadcastCatch != null) { + // 来自上级平台的停止对讲 + log.info("[停止对讲] 来自上级,平台:{}, 通道:{}", ssrcTransaction.getDeviceId(), channel.getDeviceId()); + audioBroadcastManager.del(channel.getId()); + } + break; + } + // 释放ssrc + MediaServer mediaServerItem = mediaServerService.getOne(ssrcTransaction.getMediaServerId()); + if (mediaServerItem != null) { + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcTransaction.getSsrc()); + } + sessionManager.removeByCallId(ssrcTransaction.getCallId()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/CancelRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/CancelRequestProcessor.java new file mode 100755 index 0000000..b04352a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/CancelRequestProcessor.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.RequestEvent; + +/** + * SIP命令类型: CANCEL请求 + */ +@Component +public class CancelRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor { + + private final String method = "CANCEL"; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addRequestProcessor(method, this); + } + + /** + * 处理CANCEL请求 + * + * @param evt 事件 + */ + @Override + public void process(RequestEvent evt) { + // TODO 优先级99 Cancel Request消息实现,此消息一般为级联消息,上级给下级发送请求取消指令 + + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java new file mode 100755 index 0000000..6daf121 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java @@ -0,0 +1,659 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.*; +import com.genersoft.iot.vmp.gb28181.session.AudioBroadcastManager; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import gov.nist.javax.sdp.TimeDescriptionImpl; +import gov.nist.javax.sdp.fields.TimeField; +import gov.nist.javax.sdp.fields.URIField; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sdp.*; +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.CallIdHeader; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.List; +import java.util.Vector; + +/** + * SIP命令类型: INVITE请求 + */ +@Slf4j +@SuppressWarnings("rawtypes") +@Component +public class InviteRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor { + + private final String method = "INVITE"; + + @Autowired + private ISIPCommanderForPlatform cmderFroPlatform; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IGbChannelPlayService channelPlayService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private IPlayService playService; + + @Autowired + private IPlatformService platformService; + + @Autowired + private AudioBroadcastManager audioBroadcastManager; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private SipConfig config; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private UserSetting userSetting; + + @Autowired + private SSRCFactory ssrcFactory; + + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addRequestProcessor(method, this); + } + + /** + * 处理invite请求 + * + * @param evt 请求消息 + */ + @Override + public void process(RequestEvent evt) { + + SIPRequest request = (SIPRequest)evt.getRequest(); + try { + InviteMessageInfo inviteInfo = decode(evt); + + // 查询请求是否来自上级平台\设备 + Platform platform = platformService.queryPlatformByServerGBId(inviteInfo.getRequesterId()); + if (platform == null) { + inviteFromDeviceHandle(request, inviteInfo); + } else { + // 查询平台下是否有该通道 + CommonGBChannel channel= channelService.queryOneWithPlatform(platform.getId(), inviteInfo.getTargetChannelId()); + if (channel == null) { + log.info("[上级INVITE] 通道不存在,返回404: {}", inviteInfo.getTargetChannelId()); + try { + // 通道不存在,发404,资源不存在 + responseAck(request, Response.NOT_FOUND); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] invite 通道不存在: {}", e.getMessage()); + } + return; + } + log.info("[上级INVITE] 平台:{}, 通道:{}({}), 收流地址:{}:{},收流方式:{}, 点播类型:{}, SSRC:{}", + platform.getName(), channel.getGbName(), channel.getGbDeviceId(), inviteInfo.getIp(), + inviteInfo.getPort(), inviteInfo.isTcp()?(inviteInfo.isTcpActive()?"TCP主动":"TCP被动"): "UDP", + inviteInfo.getSessionName(), inviteInfo.getSsrc()); + if(!userSetting.getUseCustomSsrcForParentInvite() && ObjectUtils.isEmpty(inviteInfo.getSsrc())) { + log.warn("[上级INVITE] 点播失败, 上级未携带SSRC, 并且本级未设置使用自定义SSRC"); + // 通道存在,发100,TRYING + try { + responseAck(request, Response.BAD_REQUEST); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 上级INVITE TRYING: {}", e.getMessage()); + } + return; + } + // 通道存在,发100,TRYING + try { + responseAck(request, Response.TRYING); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 上级INVITE TRYING: {}", e.getMessage()); + } + + channelPlayService.startInvite(channel, inviteInfo, platform, ((code, msg, streamInfo) -> { + if (code != InviteErrorCode.SUCCESS.getCode()) { + try { + responseAck(request, Response.BUSY_HERE , msg); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 上级INVITE 点播失败: {}", e.getMessage()); + } + }else { + // 点播成功, TODO 可以在此处检测cancel命令是否存在,存在则不发送 + if (userSetting.getUseCustomSsrcForParentInvite()) { + // 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式 + MediaServer mediaServer = mediaServerService.getOne(streamInfo.getMediaServer().getId()); + if (mediaServer != null) { + String ssrc = "Play".equalsIgnoreCase(inviteInfo.getSessionName()) + ? ssrcFactory.getPlaySsrc(streamInfo.getMediaServer().getId()) + : ssrcFactory.getPlayBackSsrc(streamInfo.getMediaServer().getId()); + inviteInfo.setSsrc(ssrc); + } + } + // 构建sendRTP内容 + SendRtpInfo sendRtpItem = sendRtpServerService.createSendRtpInfo(streamInfo.getMediaServer(), + inviteInfo.getIp(), inviteInfo.getPort(), inviteInfo.getSsrc(), platform.getServerGBId(), + streamInfo.getApp(), streamInfo.getStream(), + channel.getGbId(), inviteInfo.isTcp(), platform.isRtcp()); + if (inviteInfo.isTcp() && inviteInfo.isTcpActive()) { + sendRtpItem.setTcpActive(true); + } + sendRtpItem.setStatus(1); + sendRtpItem.setCallId(inviteInfo.getCallId()); + + sendRtpItem.setPlayTypeByChannelDataType(channel.getDataType(), inviteInfo.getSessionName()); + sendRtpItem.setServerId(streamInfo.getServerId()); + sendRtpServerService.update(sendRtpItem); + String sdpIp = streamInfo.getMediaServer().getSdpIp(); + if (!ObjectUtils.isEmpty(platform.getSendStreamIp())) { + sdpIp = platform.getSendStreamIp(); + } + String content = createSendSdp(sendRtpItem, inviteInfo, sdpIp); + // 超时未收到Ack应该回复bye,当前等待时间为10秒 + dynamicTask.startDelay(inviteInfo.getCallId(), () -> { + log.info("[Ack ] 等待超时, {}/{}", inviteInfo.getCallId(), channel.getGbDeviceId()); + mediaServerService.releaseSsrc(streamInfo.getMediaServer().getId(), sendRtpItem.getSsrc()); + // 回复bye + sendBye(platform, inviteInfo.getCallId()); + }, 60 * 1000); + try { + responseSdpAck(request, content, platform); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 上级INVITE 发送 200(SDP): {}", e.getMessage()); + } + + // tcp主动模式,回复sdp后开启监听 + if (sendRtpItem.isTcpActive()) { + MediaServer mediaServer = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + try { + mediaServerService.startSendRtpPassive(mediaServer, sendRtpItem, 5); + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(sendRtpItem.getChannelId()); + if (deviceChannel != null) { + redisCatchStorage.sendPlatformStartPlayMsg(sendRtpItem, deviceChannel, platform); + } + }catch (ControllerException e) { + log.warn("[上级INVITE] tcp主动模式 发流失败", e); + sendBye(platform, inviteInfo.getCallId()); + } + } + } + })); + } + } catch (SdpException e) { + // 参数不全, 发400,请求错误 + try { + responseAck(request, Response.BAD_REQUEST); + } catch (SipException | InvalidArgumentException | ParseException sendException) { + log.error("[命令发送失败] invite BAD_REQUEST: {}", sendException.getMessage()); + } + } catch (InviteDecodeException e) { + try { + responseAck(request, e.getCode(), e.getMsg()); + } catch (SipException | InvalidArgumentException | ParseException sendException) { + log.error("[命令发送失败] invite BAD_REQUEST: {}", sendException.getMessage()); + } + }catch (PlayException e) { + try { + responseAck(request, e.getCode(), e.getMsg()); + } catch (SipException | InvalidArgumentException | ParseException sendException) { + log.error("[命令发送失败] invite 点播失败: {}", sendException.getMessage()); + } + }catch (Exception e) { + log.error("[Invite处理异常] ", e); + try { + responseAck(request, Response.SERVER_INTERNAL_ERROR, ""); + } catch (SipException | InvalidArgumentException | ParseException sendException) { + log.error("[命令发送失败] invite 点播失败: {}", sendException.getMessage()); + } + } + } + + private InviteMessageInfo decode(RequestEvent evt) throws SdpException { + + InviteMessageInfo inviteInfo = new InviteMessageInfo(); + SIPRequest request = (SIPRequest)evt.getRequest(); + String[] channelIdArrayFromSub = SipUtils.getChannelIdFromRequest(request); + + // 解析sdp消息, 使用jainsip 自带的sdp解析方式 + String contentString = new String(request.getRawContent()); + Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString); + SessionDescription sdp = gb28181Sdp.getBaseSdb(); + String sessionName = sdp.getSessionName().getValue(); + String channelIdFromSdp = null; + if(StringUtils.equalsIgnoreCase("Playback", sessionName)){ + URIField uriField = (URIField)sdp.getURI(); + channelIdFromSdp = uriField.getURI().split(":")[0]; + } + final String channelId = StringUtils.isNotBlank(channelIdFromSdp) ? channelIdFromSdp : + (channelIdArrayFromSub != null? channelIdArrayFromSub[0]: null); + String requesterId = SipUtils.getUserIdFromFromHeader(request); + CallIdHeader callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME); + + if (requesterId == null || channelId == null) { + log.warn("[解析INVITE消息] 无法从请求中获取到来源id,返回400错误"); + throw new InviteDecodeException(Response.BAD_REQUEST, "request decode fail"); + } + log.info("[INVITE] 来源ID: {}, callId: {}, 来自:{}:{}", + requesterId, callIdHeader.getCallId(), request.getRemoteAddress(), request.getRemotePort()); + inviteInfo.setRequesterId(requesterId); + inviteInfo.setTargetChannelId(channelId); + if (channelIdArrayFromSub != null && channelIdArrayFromSub.length == 2) { + inviteInfo.setSourceChannelId(channelIdArrayFromSub[1]); + } + inviteInfo.setSessionName(sessionName); + inviteInfo.setSsrc(gb28181Sdp.getSsrc()); + inviteInfo.setCallId(callIdHeader.getCallId()); + + // 如果是录像回放,则会存在录像的开始时间与结束时间 + Long startTime = null; + Long stopTime = null; + if (sdp.getTimeDescriptions(false) != null && !sdp.getTimeDescriptions(false).isEmpty()) { + TimeDescriptionImpl timeDescription = (TimeDescriptionImpl) (sdp.getTimeDescriptions(false).get(0)); + TimeField startTimeFiled = (TimeField) timeDescription.getTime(); + startTime = startTimeFiled.getStartTime(); + stopTime = startTimeFiled.getStopTime(); + } + // 获取支持的格式 + Vector mediaDescriptions = sdp.getMediaDescriptions(true); + // 查看是否支持PS 负载96 + //String ip = null; + int port = -1; + boolean mediaTransmissionTCP = false; + Boolean tcpActive = null; + for (Object description : mediaDescriptions) { + MediaDescription mediaDescription = (MediaDescription) description; + Media media = mediaDescription.getMedia(); + + Vector mediaFormats = media.getMediaFormats(false); + if (mediaFormats.contains("96") || mediaFormats.contains("8")) { + port = media.getMediaPort(); + //String mediaType = media.getMediaType(); + String protocol = media.getProtocol(); + + // 区分TCP发流还是udp, 当前默认udp + if ("TCP/RTP/AVP".equalsIgnoreCase(protocol)) { + String setup = mediaDescription.getAttribute("setup"); + if (setup != null) { + mediaTransmissionTCP = true; + if ("active".equalsIgnoreCase(setup)) { + tcpActive = true; + } else if ("passive".equalsIgnoreCase(setup)) { + tcpActive = false; + } + } + } + break; + } + } + if (port == -1) { + log.info("[解析INVITE消息] 不支持的媒体格式,返回415"); + throw new InviteDecodeException(Response.UNSUPPORTED_MEDIA_TYPE, "unsupported media type"); + } + inviteInfo.setTcp(mediaTransmissionTCP); + inviteInfo.setTcpActive(tcpActive != null? tcpActive: false); + inviteInfo.setStartTime(startTime); + inviteInfo.setStopTime(stopTime); + + Vector sdpMediaDescriptions = sdp.getMediaDescriptions(true); + MediaDescription mediaDescription = null; + String downloadSpeed = "1"; + if (!sdpMediaDescriptions.isEmpty()) { + mediaDescription = (MediaDescription) sdpMediaDescriptions.get(0); + } + if (mediaDescription != null) { + downloadSpeed = mediaDescription.getAttribute("downloadspeed"); + } + inviteInfo.setIp(sdp.getConnection().getAddress()); + inviteInfo.setPort(port); + inviteInfo.setDownloadSpeed(downloadSpeed); + + return inviteInfo; + + } + + private String createSendSdp(SendRtpInfo sendRtpItem, InviteMessageInfo inviteInfo, String sdpIp) { + StringBuilder content = new StringBuilder(200); + content.append("v=0\r\n"); + content.append("o=" + inviteInfo.getTargetChannelId() + " 0 0 IN IP4 " + sdpIp + "\r\n"); + content.append("s=" + inviteInfo.getSessionName() + "\r\n"); + content.append("c=IN IP4 " + sdpIp + "\r\n"); + if ("Playback".equalsIgnoreCase(inviteInfo.getSessionName())) { + content.append("t=" + inviteInfo.getStartTime() + " " + inviteInfo.getStopTime() + "\r\n"); + } else { + content.append("t=0 0\r\n"); + } + if (sendRtpItem.isTcp()) { + content.append("m=video " + sendRtpItem.getLocalPort() + " TCP/RTP/AVP 96\r\n"); + if (!sendRtpItem.isTcpActive()) { + content.append("a=setup:active\r\n"); + } else { + content.append("a=setup:passive\r\n"); + } + }else { + content.append("m=video " + sendRtpItem.getLocalPort() + " RTP/AVP 96\r\n"); + } + content.append("a=sendonly\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + content.append("y=" + sendRtpItem.getSsrc() + "\r\n"); + content.append("f=\r\n"); + return content.toString(); + } + + private void sendBye(Platform platform, String callId) { + try { + SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId); + if (sendRtpItem == null) { + return; + } + CommonGBChannel channel = channelService.getOne(sendRtpItem.getChannelId()); + if (channel == null) { + return; + } + cmderFroPlatform.streamByeCmd(platform, sendRtpItem, channel); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 上级INVITE 发送BYE: {}", e.getMessage()); + } + } + + public void inviteFromDeviceHandle(SIPRequest request, InviteMessageInfo inviteInfo) { + + if (inviteInfo.getSourceChannelId() == null) { + log.warn("来自设备的Invite请求,无法从请求信息中确定请求来自的通道,已忽略,requesterId: {}", inviteInfo.getRequesterId()); + try { + responseAck(request, Response.FORBIDDEN); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 来自设备的Invite请求,无法从请求信息中确定所属设备 FORBIDDEN: {}", e.getMessage()); + } + return; + } + // 非上级平台请求,查询是否设备请求(通常为接收语音广播的设备) + Device device = redisCatchStorage.getDevice(inviteInfo.getRequesterId()); + // 判断requesterId是设备还是通道 + if (device == null) { + device = deviceService.getDeviceBySourceChannelDeviceId(inviteInfo.getRequesterId()); + } + if (device == null) { + // 检查channelID是否可用 + device = deviceService.getDeviceBySourceChannelDeviceId(inviteInfo.getSourceChannelId()); + } + + if (device == null) { + log.warn("来自设备的Invite请求,无法从请求信息中确定所属设备,已忽略,requesterId: {}/{}", inviteInfo.getRequesterId(), + inviteInfo.getSourceChannelId()); + try { + responseAck(request, Response.FORBIDDEN); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 来自设备的Invite请求,无法从请求信息中确定所属设备 FORBIDDEN: {}", e.getMessage()); + } + return; + } + DeviceChannel deviceChannel = deviceChannelService.getOne(device.getDeviceId(), inviteInfo.getSourceChannelId()); + if (deviceChannel == null) { + List audioBroadcastCatchList = audioBroadcastManager.getByDeviceId(device.getDeviceId()); + if (audioBroadcastCatchList.isEmpty()) { + log.warn("来自设备的Invite请求,无法从请求信息中确定所属通道,已忽略,requesterId: {}/{}", inviteInfo.getRequesterId(), inviteInfo.getSourceChannelId()); + try { + responseAck(request, Response.FORBIDDEN); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 来自设备的Invite请求,无法从请求信息中确定所属设备 FORBIDDEN: {}", e.getMessage()); + } + return; + }else { + deviceChannel = deviceChannelService.getOneForSourceById(audioBroadcastCatchList.get(0).getChannelId()); + } + } + AudioBroadcastCatch broadcastCatch = audioBroadcastManager.get(deviceChannel.getId()); + if (broadcastCatch == null) { + log.warn("来自设备的Invite请求非语音广播,已忽略,requesterId: {}/{}", inviteInfo.getRequesterId(), inviteInfo.getSourceChannelId()); + try { + responseAck(request, Response.FORBIDDEN); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 来自设备的Invite请求非语音广播 FORBIDDEN: {}", e.getMessage()); + } + return; + } + log.info("收到设备" + inviteInfo.getRequesterId() + "的语音广播Invite请求"); + String key = VideoManagerConstants.BROADCAST_WAITE_INVITE + device.getDeviceId(); + if (!SipUtils.isFrontEnd(device.getDeviceId())) { + key += broadcastCatch.getChannelId(); + } + dynamicTask.stop(key); + try { + responseAck(request, Response.TRYING); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] invite BAD_REQUEST: {}", e.getMessage()); + playService.stopAudioBroadcast(device, deviceChannel); + return; + } + String contentString = new String(request.getRawContent()); + + try { + Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString); + SessionDescription sdp = gb28181Sdp.getBaseSdb(); + // 获取支持的格式 + Vector mediaDescriptions = sdp.getMediaDescriptions(true); + + // 查看是否支持PS 负载96 + int port = -1; + boolean mediaTransmissionTCP = false; + Boolean tcpActive = null; + for (int i = 0; i < mediaDescriptions.size(); i++) { + MediaDescription mediaDescription = (MediaDescription) mediaDescriptions.get(i); + Media media = mediaDescription.getMedia(); + + Vector mediaFormats = media.getMediaFormats(false); +// if (mediaFormats.contains("8")) { + port = media.getMediaPort(); + String protocol = media.getProtocol(); + // 区分TCP发流还是udp, 当前默认udp + if ("TCP/RTP/AVP".equals(protocol)) { + String setup = mediaDescription.getAttribute("setup"); + if (setup != null) { + mediaTransmissionTCP = true; + if ("active".equals(setup)) { + tcpActive = true; + } else if ("passive".equals(setup)) { + tcpActive = false; + } + } + } + break; +// } + } + if (port == -1) { + log.info("不支持的媒体格式,返回415"); + // 回复不支持的格式 + try { + responseAck(request, Response.UNSUPPORTED_MEDIA_TYPE); // 不支持的格式,发415 + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] invite 不支持的媒体格式: {}", e.getMessage()); + playService.stopAudioBroadcast(device, deviceChannel); + return; + } + return; + } + String addressStr = sdp.getOrigin().getAddress(); + log.info("设备{}请求语音流,地址:{}:{},ssrc:{}, {}", inviteInfo.getRequesterId(), addressStr, port, gb28181Sdp.getSsrc(), + mediaTransmissionTCP ? (tcpActive ? "TCP主动" : "TCP被动") : "UDP"); + + MediaServer mediaServerItem = broadcastCatch.getMediaServerItem(); + if (mediaServerItem == null) { + log.warn("未找到语音喊话使用的zlm"); + try { + responseAck(request, Response.BUSY_HERE); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] invite 未找到可用的zlm: {}", e.getMessage()); + playService.stopAudioBroadcast(device, deviceChannel); + } + return; + } + log.info("设备{}请求语音流, 收流地址:{}:{},ssrc:{}, {}, 对讲方式:{}", inviteInfo.getRequesterId(), addressStr, port, gb28181Sdp.getSsrc(), + mediaTransmissionTCP ? (tcpActive ? "TCP主动" : "TCP被动") : "UDP", sdp.getSessionName().getValue()); + CallIdHeader callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME); + + SendRtpInfo sendRtpItem = sendRtpServerService.createSendRtpInfo(mediaServerItem, addressStr, port, gb28181Sdp.getSsrc(), inviteInfo.getRequesterId(), + device.getDeviceId(), deviceChannel.getId(), + mediaTransmissionTCP, false); + + if (sendRtpItem == null) { + log.warn("服务器端口资源不足"); + try { + responseAck(request, Response.BUSY_HERE); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] invite 服务器端口资源不足: {}", e.getMessage()); + playService.stopAudioBroadcast(device, deviceChannel); + return; + } + return; + } + + sendRtpItem.setPlayType(InviteStreamType.BROADCAST); + sendRtpItem.setCallId(callIdHeader.getCallId()); + sendRtpItem.setStatus(1); + sendRtpItem.setApp(broadcastCatch.getApp()); + sendRtpItem.setStream(broadcastCatch.getStream()); + sendRtpItem.setPt(8); + sendRtpItem.setUsePs(false); + sendRtpItem.setRtcp(false); + sendRtpItem.setOnlyAudio(true); + sendRtpItem.setTcp(mediaTransmissionTCP); + if (tcpActive != null) { + sendRtpItem.setTcpActive(tcpActive); + } + + sendRtpServerService.update(sendRtpItem); + + Boolean streamReady = mediaServerService.isStreamReady(mediaServerItem, broadcastCatch.getApp(), broadcastCatch.getStream()); + if (streamReady) { + sendOk(device, deviceChannel, sendRtpItem, sdp, request, mediaServerItem, mediaTransmissionTCP, gb28181Sdp.getSsrc()); + } else { + log.warn("[语音通话], 未发现待推送的流,app={},stream={}", broadcastCatch.getApp(), broadcastCatch.getStream()); + try { + responseAck(request, Response.GONE); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 语音通话 回复410失败, {}", e.getMessage()); + return; + } + playService.stopAudioBroadcast(device, deviceChannel); + } + } catch (SdpException e) { + log.error("[SDP解析异常]", e); + playService.stopAudioBroadcast(device, deviceChannel); + } + } + + SIPResponse sendOk(Device device, DeviceChannel channel, SendRtpInfo sendRtpItem, SessionDescription sdp, SIPRequest request, MediaServer mediaServerItem, boolean mediaTransmissionTCP, String ssrc) { + SIPResponse sipResponse = null; + try { + sendRtpItem.setStatus(2); + sendRtpServerService.update(sendRtpItem); + StringBuffer content = new StringBuffer(200); + content.append("v=0\r\n"); + content.append("o=" + config.getId() + " " + sdp.getOrigin().getSessionId() + " " + sdp.getOrigin().getSessionVersion() + " IN IP4 " + mediaServerItem.getSdpIp() + "\r\n"); + content.append("s=Play\r\n"); + content.append("c=IN IP4 " + mediaServerItem.getSdpIp() + "\r\n"); + content.append("t=0 0\r\n"); + + if (mediaTransmissionTCP) { + content.append("m=audio " + sendRtpItem.getLocalPort() + " TCP/RTP/AVP 8\r\n"); + } else { + content.append("m=audio " + sendRtpItem.getLocalPort() + " RTP/AVP 8\r\n"); + } + + content.append("a=rtpmap:8 PCMA/8000/1\r\n"); + + content.append("a=sendonly\r\n"); + if (sendRtpItem.isTcp()) { + content.append("a=connection:new\r\n"); + if (!sendRtpItem.isTcpActive()) { + content.append("a=setup:active\r\n"); + } else { + content.append("a=setup:passive\r\n"); + } + } + content.append("y=" + ssrc + "\r\n"); + content.append("f=v/////a/1/8/1\r\n"); + + Platform parentPlatform = new Platform(); + parentPlatform.setServerIp(device.getIp()); + parentPlatform.setServerPort(device.getPort()); + parentPlatform.setServerGBId(device.getDeviceId()); + + sipResponse = responseSdpAck(request, content.toString(), parentPlatform); + + AudioBroadcastCatch audioBroadcastCatch = audioBroadcastManager.get(sendRtpItem.getChannelId()); + + audioBroadcastCatch.setStatus(AudioBroadcastCatchStatus.Ok); + audioBroadcastCatch.setSipTransactionInfoByRequest(sipResponse); + audioBroadcastManager.update(audioBroadcastCatch); + SsrcTransaction ssrcTransaction = SsrcTransaction.buildForDevice(device.getDeviceId(), sendRtpItem.getChannelId(), + request.getCallIdHeader().getCallId(), sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getSsrc(), sendRtpItem.getMediaServerId(), sipResponse, InviteSessionType.BROADCAST); + sessionManager.put(ssrcTransaction); + // 开启发流,大华在收到200OK后就会开始建立连接 + if (sendRtpItem.isTcpActive() || !device.isBroadcastPushAfterAck()) { + if (sendRtpItem.isTcpActive()) { + log.info("[语音喊话] 监听端口等待设备连接后推流"); + }else { + log.info("[语音喊话] 回复200OK后发现 BroadcastPushAfterAck为False,现在开始推流"); + } + + playService.startPushStream(sendRtpItem, channel, sipResponse, parentPlatform, request.getCallIdHeader()); + } + + } catch (SipException | InvalidArgumentException | ParseException | SdpParseException e) { + log.error("[命令发送失败] 语音喊话 回复200OK(SDP): {}", e.getMessage()); + } + return sipResponse; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java new file mode 100755 index 0000000..6c8ac8f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java @@ -0,0 +1,321 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.channel.ChannelEvent; +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.Coordtransform; +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import javax.sip.RequestEvent; +import javax.sip.header.FromHeader; +import java.lang.reflect.InvocationTargetException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * SIP命令类型: NOTIFY请求中的目录请求处理 + */ +@Slf4j +@Component +public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent { + + private final ConcurrentLinkedQueue channelList = new ConcurrentLinkedQueue<>(); + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Autowired + private UserSetting userSetting; + + @Autowired + private EventPublisher eventPublisher; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IGbChannelService channelService; + +// @Scheduled(fixedRate = 2000) //每400毫秒执行一次 +// public void showSize(){ +// log.warn("[notify-目录订阅] 待处理消息数量: {}", taskQueue.size() ); +// } + + public void process(RequestEvent evt) { + if (taskQueue.size() >= userSetting.getMaxNotifyCountQueue()) { + log.error("[notify-目录订阅] 待处理消息队列已满 {},返回486 BUSY_HERE,消息不做处理", userSetting.getMaxNotifyCountQueue()); + return; + } + taskQueue.offer(new HandlerCatchData(evt, null, null)); + } + + @Scheduled(fixedDelay = 400) //每400毫秒执行一次 + public void executeTaskQueue(){ + if (taskQueue.isEmpty()) { + return; + } + List handlerCatchDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + HandlerCatchData poll = taskQueue.poll(); + if (poll != null) { + handlerCatchDataList.add(poll); + } + } + if (handlerCatchDataList.isEmpty()) { + return; + } + for (HandlerCatchData take : handlerCatchDataList) { + if (take == null) { + continue; + } + RequestEvent evt = take.getEvt(); + try { + FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME); + String deviceId = SipUtils.getUserIdFromFromHeader(fromHeader); + + Device device = redisCatchStorage.getDevice(deviceId); + if (device == null || !device.isOnLine()) { + log.warn("[收到目录订阅]:{}, 但是设备已经离线", (device != null ? device.getDeviceId() : "")); + continue; + } + Element rootElement = getRootElement(evt, device.getCharset()); + if (rootElement == null) { + log.warn("[ 收到目录订阅 ] content cannot be null, {}", evt.getRequest()); + continue; + } + Element deviceListElement = rootElement.element("DeviceList"); + if (deviceListElement == null) { + log.warn("[ 收到目录订阅 ] content cannot be null, {}", evt.getRequest()); + continue; + } + Iterator deviceListIterator = deviceListElement.elementIterator(); + if (deviceListIterator != null) { + + // 遍历DeviceList + while (deviceListIterator.hasNext()) { + Element itemDevice = deviceListIterator.next(); + CatalogChannelEvent catalogChannelEvent = null; + try { + catalogChannelEvent = CatalogChannelEvent.decode(itemDevice); + if (catalogChannelEvent.getChannel() == null) { + log.info("[解析CatalogChannelEvent]成功:但是解析通道信息失败, 原文如下: \n{}", new String(evt.getRequest().getRawContent())); + continue; + } + catalogChannelEvent.getChannel().setDataDeviceId(device.getId()); + if (catalogChannelEvent.getChannel().getLongitude() != null + && catalogChannelEvent.getChannel().getLatitude() != null + && catalogChannelEvent.getChannel().getLongitude() > 0 + && catalogChannelEvent.getChannel().getLatitude() > 0) { + if (device.checkWgs84()) { + catalogChannelEvent.getChannel().setGbLongitude(catalogChannelEvent.getChannel().getLongitude()); + catalogChannelEvent.getChannel().setGbLatitude(catalogChannelEvent.getChannel().getLatitude()); + }else { + Double[] wgs84Position = Coordtransform.GCJ02ToWGS84(catalogChannelEvent.getChannel().getLongitude(), catalogChannelEvent.getChannel().getLatitude()); + catalogChannelEvent.getChannel().setGbLongitude(wgs84Position[0]); + catalogChannelEvent.getChannel().setGbLatitude(wgs84Position[1]); + } + } + } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | + IllegalAccessException e) { + log.error("[解析CatalogChannelEvent]失败,", e); + log.error("[解析CatalogChannelEvent]失败原文: \n{}", new String(evt.getRequest().getRawContent(), Charset.forName(device.getCharset()))); + continue; + } + if (log.isDebugEnabled()){ + log.debug("[收到目录订阅]:{}/{}-{}", device.getDeviceId(), + catalogChannelEvent.getChannel().getDeviceId(), catalogChannelEvent.getEvent()); + } + DeviceChannel channel = catalogChannelEvent.getChannel(); + switch (catalogChannelEvent.getEvent()) { + case CatalogEvent.ON: + // 上线 + log.info("[收到通道上线通知] 来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + channel.setStatus("ON"); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.STATUS_CHANGED, channel)); + + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId(), true); + } + break; + case CatalogEvent.OFF: + // 离线 + log.info("[收到通道离线通知] 来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + if (userSetting.getRefuseChannelStatusChannelFormNotify()) { + log.info("[收到通道离线通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + } else { + channel.setStatus("OFF"); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.STATUS_CHANGED, channel)); + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId(), false); + } + } + break; + case CatalogEvent.VLOST: + // 视频丢失 + log.info("[收到通道视频丢失通知] 来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + if (userSetting.getRefuseChannelStatusChannelFormNotify()) { + log.info("[收到通道视频丢失通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + } else { + channel.setStatus("OFF"); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.STATUS_CHANGED, channel)); + + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId(), false); + } + } + break; + case CatalogEvent.DEFECT: + // 故障 + log.info("[收到通道视频故障通知] 来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + if (userSetting.getRefuseChannelStatusChannelFormNotify()) { + log.info("[收到通道视频故障通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + } else { + channel.setStatus("OFF"); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.STATUS_CHANGED, channel)); + + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId(), false); + } + } + break; + case CatalogEvent.ADD: + // 增加 + log.info("[收到增加通道通知] 来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + // 判断此通道是否存在 + DeviceChannel deviceChannel = deviceChannelService.getOneForSource(device.getId(), catalogChannelEvent.getChannel().getDeviceId()); + if (deviceChannel != null) { + log.info("[增加通道] 已存在,不发送通知只更新,设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + channel.setId(deviceChannel.getId()); + channel.setHasAudio(deviceChannel.isHasAudio()); + channel.setUpdateTime(DateUtil.getNow()); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.UPDATE, channel)); + + } else { + catalogChannelEvent.getChannel().setUpdateTime(DateUtil.getNow()); + catalogChannelEvent.getChannel().setCreateTime(DateUtil.getNow()); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.ADD, channel)); + + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendChannelAddOrDelete(device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId(), true); + } + } + + break; + case CatalogEvent.DEL: + // 删除 + log.info("[收到删除通道通知] 来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.DELETE, channel)); + + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendChannelAddOrDelete(device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId(), false); + } + break; + case CatalogEvent.UPDATE: + // 更新 + log.info("[收到更新通道通知] 来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + // 判断此通道是否存在 + DeviceChannel deviceChannelForUpdate = deviceChannelService.getOneForSource(device.getId(), catalogChannelEvent.getChannel().getDeviceId()); + if (deviceChannelForUpdate != null) { + channel.setId(deviceChannelForUpdate.getId()); + channel.setHasAudio(deviceChannelForUpdate.isHasAudio()); + channel.setUpdateTime(DateUtil.getNow()); + channel.setUpdateTime(DateUtil.getNow()); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.UPDATE, channel)); + + } else { + catalogChannelEvent.getChannel().setCreateTime(DateUtil.getNow()); + catalogChannelEvent.getChannel().setUpdateTime(DateUtil.getNow()); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.ADD, channel)); + + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendChannelAddOrDelete(device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId(), true); + } + } + break; + default: + log.warn("[ NotifyCatalog ] event not found : {}", catalogChannelEvent.getEvent()); + + } + } + } + + } catch (DocumentException e) { + log.error("未处理的异常 ", e); + } + } + if (!channelList.isEmpty()) { + executeSave(); + } + } + + @Transactional + public void executeSave() { + int size = channelList.size(); + List channelListForSave = new ArrayList<>(); + for (int i = 0; i < size; i++) { + channelListForSave.add(channelList.poll()); + } + + for (NotifyCatalogChannel notifyCatalogChannel : channelListForSave) { + try { + switch (notifyCatalogChannel.getType()) { + case STATUS_CHANGED: + deviceChannelService.updateChannelStatusForNotify(notifyCatalogChannel.getChannel()); + CommonGBChannel channelForStatus = channelService.queryCommonChannelByDeviceChannel(notifyCatalogChannel.getChannel()); + if ("ON".equals(notifyCatalogChannel.getChannel().getStatus()) ) { + eventPublisher.channelEventPublish(channelForStatus, ChannelEvent.ChannelEventMessageType.ON); + }else { + eventPublisher.channelEventPublish(channelForStatus, ChannelEvent.ChannelEventMessageType.OFF); + } + break; + case ADD: + deviceChannelService.addChannel(notifyCatalogChannel.getChannel()); + CommonGBChannel channelForAdd = channelService.getOne(notifyCatalogChannel.getChannel().getId()); + eventPublisher.channelEventPublish(channelForAdd, ChannelEvent.ChannelEventMessageType.ADD); + break; + case UPDATE: + CommonGBChannel oldCommonChannel = channelService.getOne(notifyCatalogChannel.getChannel().getId()); + deviceChannelService.updateChannelForNotify(notifyCatalogChannel.getChannel()); + CommonGBChannel channel = channelService.getOne(oldCommonChannel.getGbId()); + eventPublisher.channelEventPublishForUpdate(channel, oldCommonChannel); + break; + case DELETE: + CommonGBChannel oldCommonChannelForDelete = channelService.queryCommonChannelByDeviceChannel(notifyCatalogChannel.getChannel()); + deviceChannelService.deleteForNotify(notifyCatalogChannel.getChannel()); + eventPublisher.channelEventPublish(oldCommonChannelForDelete, ChannelEvent.ChannelEventMessageType.DEL); + break; + } + }catch (Exception e) { + log.error("[存储收到的通道-异常]类型:{},编号:{}", notifyCatalogChannel.getType(), + notifyCatalogChannel.getChannel().getDeviceId(), e); + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForMobilePositionProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForMobilePositionProcessor.java new file mode 100755 index 0000000..5bcf206 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForMobilePositionProcessor.java @@ -0,0 +1,182 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.bean.HandlerCatchData; +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.utils.NumericUtil; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.service.IMobilePositionService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.RequestEvent; +import javax.sip.header.FromHeader; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * SIP命令类型: NOTIFY请求中的移动位置请求处理 + */ +@Slf4j +@Component +public class NotifyRequestForMobilePositionProcessor extends SIPRequestProcessorParent { + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Autowired + private UserSetting userSetting; + + @Autowired + private EventPublisher eventPublisher; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IMobilePositionService mobilePositionService; + + public void process(RequestEvent evt) { + + if (taskQueue.size() >= userSetting.getMaxNotifyCountQueue()) { + log.error("[notify-移动位置] 待处理消息队列已满 {},返回486 BUSY_HERE,消息不做处理", userSetting.getMaxNotifyCountQueue()); + return; + } + taskQueue.offer(new HandlerCatchData(evt, null, null)); + } + + @Scheduled(fixedDelay = 200) //每200毫秒执行一次 + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List handlerCatchDataList = new ArrayList<>(); + while (!taskQueue.isEmpty()) { + handlerCatchDataList.add(taskQueue.poll()); + } + if (handlerCatchDataList.isEmpty()) { + return; + } + for (HandlerCatchData take : handlerCatchDataList) { + if (take == null) { + continue; + } + RequestEvent evt = take.getEvt(); + try { + FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME); + String deviceId = SipUtils.getUserIdFromFromHeader(fromHeader); + long startTime = System.currentTimeMillis(); + // 回复 200 OK + Element rootElement = getRootElement(evt); + if (rootElement == null) { + log.error("处理MobilePosition移动位置Notify时未获取到消息体,{}", evt.getRequest()); + continue; + } + Device device = redisCatchStorage.getDevice(deviceId); + if (device == null) { + log.error("处理MobilePosition移动位置Notify时未获取到device,{}", deviceId); + continue; + } + MobilePosition mobilePosition = new MobilePosition(); + mobilePosition.setDeviceId(device.getDeviceId()); + mobilePosition.setDeviceName(device.getName()); + mobilePosition.setCreateTime(DateUtil.getNow()); + + DeviceChannel deviceChannel = null; + List elements = rootElement.elements(); + readDocument: for (Element element : elements) { + switch (element.getName()){ + case "DeviceID": + String channelId = element.getStringValue(); + deviceChannel = deviceChannelService.getOne(device.getDeviceId(), channelId); + if (deviceChannel != null) { + mobilePosition.setChannelId(deviceChannel.getId()); + mobilePosition.setChannelDeviceId(deviceChannel.getDeviceId()); + }else { + log.error("[notify-移动位置] 未找到通道 {}/{}", device.getDeviceId(), channelId); + break readDocument; + } + break; + case "Time": + String timeVal = element.getStringValue(); + if (ObjectUtils.isEmpty(timeVal)) { + mobilePosition.setTime(DateUtil.getNow()); + } else { + mobilePosition.setTime(SipUtils.parseTime(timeVal)); + } + break; + case "Longitude": + mobilePosition.setLongitude(Double.parseDouble(element.getStringValue())); + break; + case "Latitude": + mobilePosition.setLatitude(Double.parseDouble(element.getStringValue())); + break; + case "Speed": + String speedVal = element.getStringValue(); + if (NumericUtil.isDouble(speedVal)) { + mobilePosition.setSpeed(Double.parseDouble(speedVal)); + } else { + mobilePosition.setSpeed(0.0); + } + break; + case "Direction": + String directionVal = element.getStringValue(); + if (NumericUtil.isDouble(directionVal)) { + mobilePosition.setDirection(Double.parseDouble(directionVal)); + } else { + mobilePosition.setDirection(0.0); + } + break; + case "Altitude": + String altitudeVal = element.getStringValue(); + if (NumericUtil.isDouble(altitudeVal)) { + mobilePosition.setAltitude(Double.parseDouble(altitudeVal)); + } else { + mobilePosition.setAltitude(0.0); + } + break; + + } + } + if (deviceChannel == null) { + continue; + } + + log.info("[收到移动位置订阅通知]:{}/{}->{}.{}, 时间: {}", mobilePosition.getDeviceId(), mobilePosition.getChannelId(), + mobilePosition.getLongitude(), mobilePosition.getLatitude(), System.currentTimeMillis() - startTime); + mobilePosition.setReportSource("Mobile Position"); + + mobilePositionService.add(mobilePosition); + // 向关联了该通道并且开启移动位置订阅的上级平台发送移动位置订阅消息 + try { + eventPublisher.mobilePositionEventPublish(mobilePosition); + }catch (Exception e) { + log.error("[MobilePositionEvent] 发送失败: ", e); + } + } catch (DocumentException e) { + log.error("[收到移动位置订阅通知] 文档解析异常: \r\n{}", evt.getRequest(), e); + } catch ( Exception e) { + log.error("[收到移动位置订阅通知] 异常: ", e); + } + } + } +// @Scheduled(fixedRate = 10000) +// public void execute(){ +// logger.debug("[待处理Notify-移动位置订阅消息数量]: {}", taskQueue.size()); +// } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java new file mode 100755 index 0000000..15184e2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java @@ -0,0 +1,184 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.utils.NumericUtil; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.DateUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.FromHeader; +import javax.sip.message.Response; +import java.text.ParseException; + +/** + * SIP命令类型: NOTIFY请求,这是作为上级发送订阅请求后,设备才会响应的 + */ +@Slf4j +@Component +public class NotifyRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor { + + @Autowired + private SipConfig sipConfig; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private EventPublisher publisher; + + private final String method = "NOTIFY"; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private NotifyRequestForCatalogProcessor notifyRequestForCatalogProcessor; + + @Autowired + private NotifyRequestForMobilePositionProcessor notifyRequestForMobilePositionProcessor; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addRequestProcessor(method, this); + } + + @Override + public void process(RequestEvent evt) { + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK, null, null); + Element rootElement = getRootElement(evt); + if (rootElement == null) { + log.error("处理NOTIFY消息时未获取到消息体,{}", evt.getRequest()); + responseAck((SIPRequest) evt.getRequest(), Response.OK, null, null); + return; + } + String cmd = XmlUtil.getText(rootElement, "CmdType"); + + if (CmdType.CATALOG.equals(cmd)) { + notifyRequestForCatalogProcessor.process(evt); + } else if (CmdType.ALARM.equals(cmd)) { + processNotifyAlarm(evt); + } else if (CmdType.MOBILE_POSITION.equals(cmd)) { + notifyRequestForMobilePositionProcessor.process(evt); + } else { + log.info("接收到消息:" + cmd); + } + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("未处理的异常 ", e); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + + } + /*** + * 处理alarm设备报警Notify + */ + private void processNotifyAlarm(RequestEvent evt) { + if (!sipConfig.isAlarm()) { + return; + } + try { + FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME); + String deviceId = SipUtils.getUserIdFromFromHeader(fromHeader); + + Element rootElement = getRootElement(evt); + if (rootElement == null) { + log.error("处理alarm设备报警Notify时未获取到消息体{}", evt.getRequest()); + return; + } + Element deviceIdElement = rootElement.element("DeviceID"); + String channelId = deviceIdElement.getText().toString(); + + Device device = redisCatchStorage.getDevice(deviceId); + if (device == null) { + log.warn("[ NotifyAlarm ] 未找到设备:{}", deviceId); + return; + } + rootElement = getRootElement(evt, device.getCharset()); + if (rootElement == null) { + log.warn("[ NotifyAlarm ] content cannot be null, {}", evt.getRequest()); + return; + } + DeviceAlarm deviceAlarm = new DeviceAlarm(); + deviceAlarm.setDeviceId(deviceId); + deviceAlarm.setDeviceName(device.getName()); + deviceAlarm.setAlarmPriority(XmlUtil.getText(rootElement, "AlarmPriority")); + deviceAlarm.setAlarmMethod(XmlUtil.getText(rootElement, "AlarmMethod")); + String alarmTime = XmlUtil.getText(rootElement, "AlarmTime"); + if (alarmTime == null) { + log.warn("[ NotifyAlarm ] AlarmTime cannot be null"); + return; + } + deviceAlarm.setAlarmTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(alarmTime)); + if (XmlUtil.getText(rootElement, "AlarmDescription") == null) { + deviceAlarm.setAlarmDescription(""); + } else { + deviceAlarm.setAlarmDescription(XmlUtil.getText(rootElement, "AlarmDescription")); + } + if (NumericUtil.isDouble(XmlUtil.getText(rootElement, "Longitude"))) { + deviceAlarm.setLongitude(Double.parseDouble(XmlUtil.getText(rootElement, "Longitude"))); + } else { + deviceAlarm.setLongitude(0.00); + } + if (NumericUtil.isDouble(XmlUtil.getText(rootElement, "Latitude"))) { + deviceAlarm.setLatitude(Double.parseDouble(XmlUtil.getText(rootElement, "Latitude"))); + } else { + deviceAlarm.setLatitude(0.00); + } + log.info("[收到Notify-Alarm]:{}/{}", device.getDeviceId(), deviceAlarm.getChannelId()); + if ("4".equals(deviceAlarm.getAlarmMethod())) { + DeviceChannel deviceChannel = deviceChannelService.getOne(device.getDeviceId(), channelId); + if (deviceChannel == null) { + log.warn("[解析报警通知] 未找到通道:{}/{}", device.getDeviceId(), channelId); + }else { + MobilePosition mobilePosition = new MobilePosition(); + mobilePosition.setChannelId(deviceChannel.getId()); + mobilePosition.setChannelDeviceId(deviceChannel.getDeviceId()); + mobilePosition.setCreateTime(DateUtil.getNow()); + mobilePosition.setDeviceId(deviceAlarm.getDeviceId()); + mobilePosition.setTime(deviceAlarm.getAlarmTime()); + mobilePosition.setLongitude(deviceAlarm.getLongitude()); + mobilePosition.setLatitude(deviceAlarm.getLatitude()); + mobilePosition.setReportSource("GPS Alarm"); + + // 更新device channel 的经纬度 + deviceChannel.setLongitude(mobilePosition.getLongitude()); + deviceChannel.setLatitude(mobilePosition.getLatitude()); + deviceChannel.setGpsTime(mobilePosition.getTime()); + + deviceChannelService.updateChannelGPS(device, deviceChannel, mobilePosition); + } + } + + // 回复200 OK + if (redisCatchStorage.deviceIsOnline(deviceId)) { + publisher.deviceAlarmEventPublish(deviceAlarm); + } + } catch (DocumentException e) { + log.error("未处理的异常 ", e); + } + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java new file mode 100755 index 0000000..575d27c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java @@ -0,0 +1,243 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.auth.DigestServerAuthenticationHelper; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.GbSipDate; +import com.genersoft.iot.vmp.common.RemoteAddressInfo; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.IpPortUtil; +import gov.nist.javax.sip.address.AddressImpl; +import gov.nist.javax.sip.address.SipUri; +import gov.nist.javax.sip.header.SIPDateHeader; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.AuthorizationHeader; +import javax.sip.header.ContactHeader; +import javax.sip.header.FromHeader; +import javax.sip.header.ViaHeader; +import javax.sip.message.Request; +import javax.sip.message.Response; +import java.security.NoSuchAlgorithmException; +import java.text.ParseException; +import java.util.Calendar; +import java.util.Locale; + +/** + * SIP命令类型: REGISTER请求 + */ +@Slf4j +@Component +public class RegisterRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor { + + public final String method = "REGISTER"; + + @Autowired + private SipConfig sipConfig; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private SIPSender sipSender; + + @Autowired + private UserSetting userSetting; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addRequestProcessor(method, this); + } + + /** + * 收到注册请求 处理 + */ + @Override + public void process(RequestEvent evt) { + try { + SIPRequest request = (SIPRequest) evt.getRequest(); + Response response = null; + boolean passwordCorrect = false; + // 注册标志 + boolean registerFlag = true; + if (request.getExpires().getExpires() == 0) { + // 注销成功 + registerFlag = false; + } + FromHeader fromHeader = (FromHeader) request.getHeader(FromHeader.NAME); + AddressImpl address = (AddressImpl) fromHeader.getAddress(); + SipUri uri = (SipUri) address.getURI(); + String deviceId = uri.getUser(); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RemoteAddressInfo remoteAddressInfo = SipUtils.getRemoteAddressFromRequest(request, + userSetting.getSipUseSourceIpAsRemoteAddress()); + String requestAddress = remoteAddressInfo.getIp() + ":" + remoteAddressInfo.getPort(); + String title = registerFlag ? "[注册请求]" : "[注销请求]"; + log.info(title + "设备:{}, 开始处理: {}", deviceId, requestAddress); + if (device != null && + device.getSipTransactionInfo() != null && + request.getCallIdHeader().getCallId().equals(device.getSipTransactionInfo().getCallId())) { + log.info(title + "设备:{}, 注册续订: {}", device.getDeviceId(), device.getDeviceId()); + if (registerFlag) { + device.setExpires(request.getExpires().getExpires()); + device.setIp(remoteAddressInfo.getIp()); + device.setPort(remoteAddressInfo.getPort()); + device.setHostAddress(IpPortUtil.concatenateIpAndPort(remoteAddressInfo.getIp(), String.valueOf(remoteAddressInfo.getPort()))); + + device.setLocalIp(request.getLocalAddress().getHostAddress()); + Response registerOkResponse = getRegisterOkResponse(request); + // 判断TCP还是UDP + ViaHeader reqViaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME); + String transport = reqViaHeader.getTransport(); + device.setTransport("TCP".equalsIgnoreCase(transport) ? "TCP" : "UDP"); + sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), registerOkResponse); + device.setRegisterTime(DateUtil.getNow()); + deviceService.online(device, null); + } else { + deviceService.offline(deviceId, "主动注销"); + } + return; + } + String password = (device != null && !ObjectUtils.isEmpty(device.getPassword())) ? device.getPassword() : sipConfig.getPassword(); + AuthorizationHeader authHead = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME); + if (authHead == null && !ObjectUtils.isEmpty(password)) { + log.info(title + " 设备:{}, 回复401: {}", deviceId, requestAddress); + response = getMessageFactory().createResponse(Response.UNAUTHORIZED, request); + new DigestServerAuthenticationHelper().generateChallenge(getHeaderFactory(), response, sipConfig.getDomain()); + sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response); + return; + } + + // 校验密码是否正确 + passwordCorrect = ObjectUtils.isEmpty(password) || + new DigestServerAuthenticationHelper().doAuthenticatePlainTextPassword(request, password); + + if (!passwordCorrect) { + // 注册失败 + response = getMessageFactory().createResponse(Response.FORBIDDEN, request); + response.setReasonPhrase("wrong password"); + log.info(title + " 设备:{}, 密码/SIP服务器ID错误, 回复403: {}", deviceId, requestAddress); + sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response); + return; + } + + // 携带授权头并且密码正确 + response = getMessageFactory().createResponse(Response.OK, request); + // 如果主动禁用了Date头,则不添加 + if (!userSetting.isDisableDateHeader()) { + // 添加date头 + SIPDateHeader dateHeader = new SIPDateHeader(); + // 使用自己修改的 + GbSipDate gbSipDate = new GbSipDate(Calendar.getInstance(Locale.ENGLISH).getTimeInMillis()); + dateHeader.setDate(gbSipDate); + response.addHeader(dateHeader); + } + + if (request.getExpires() == null) { + response = getMessageFactory().createResponse(Response.BAD_REQUEST, request); + sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response); + return; + } + // 添加Contact头 + response.addHeader(request.getHeader(ContactHeader.NAME)); + // 添加Expires头 + response.addHeader(request.getExpires()); + + if (device == null) { + device = new Device(); + device.setStreamMode("TCP-PASSIVE"); + device.setCharset("GB2312"); + device.setGeoCoordSys("WGS84"); + device.setMediaServerId("auto"); + device.setDeviceId(deviceId); + device.setOnLine(false); + } else { + if (ObjectUtils.isEmpty(device.getStreamMode())) { + device.setStreamMode("TCP-PASSIVE"); + } + if (ObjectUtils.isEmpty(device.getCharset())) { + device.setCharset("GB2312"); + } + if (ObjectUtils.isEmpty(device.getGeoCoordSys())) { + device.setGeoCoordSys("WGS84"); + } + } + device.setServerId(userSetting.getServerId()); + device.setIp(remoteAddressInfo.getIp()); + device.setPort(remoteAddressInfo.getPort()); + device.setHostAddress(IpPortUtil.concatenateIpAndPort(remoteAddressInfo.getIp(), String.valueOf(remoteAddressInfo.getPort()))); + device.setLocalIp(request.getLocalAddress().getHostAddress()); + if (request.getExpires().getExpires() == 0) { + // 注销成功 + registerFlag = false; + } else { + // 注册成功 + device.setExpires(request.getExpires().getExpires()); + registerFlag = true; + // 判断TCP还是UDP + ViaHeader reqViaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME); + String transport = reqViaHeader.getTransport(); + device.setTransport("TCP".equalsIgnoreCase(transport) ? "TCP" : "UDP"); + } + + sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response); + // 注册成功 + // 保存到redis + if (registerFlag) { + log.info("[注册成功] deviceId: {}->{}", deviceId, requestAddress); + device.setRegisterTime(DateUtil.getNow()); + SipTransactionInfo sipTransactionInfo = new SipTransactionInfo((SIPResponse) response); + deviceService.online(device, sipTransactionInfo); + } else { + log.info("[注销成功] deviceId: {}->{}", deviceId, requestAddress); + deviceService.offline(deviceId, "主动注销"); + } + } catch (SipException | NoSuchAlgorithmException | ParseException e) { + log.error("未处理的异常 ", e); + } + } + + private Response getRegisterOkResponse(Request request) throws ParseException { + // 携带授权头并且密码正确 + Response response = getMessageFactory().createResponse(Response.OK, request); + // 如果主动禁用了Date头,则不添加 + if (!userSetting.isDisableDateHeader()) { + // 添加date头 + SIPDateHeader dateHeader = new SIPDateHeader(); + // 使用自己修改的 + GbSipDate gbSipDate = new GbSipDate(Calendar.getInstance(Locale.ENGLISH).getTimeInMillis()); + dateHeader.setDate(gbSipDate); + response.addHeader(dateHeader); + } + + // 添加Contact头 + response.addHeader(request.getHeader(ContactHeader.NAME)); + // 添加Expires头 + response.addHeader(request.getExpires()); + + return response; + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/SubscribeRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/SubscribeRequestProcessor.java new file mode 100755 index 0000000..12d84b7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/SubscribeRequestProcessor.java @@ -0,0 +1,212 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.EventHeader; +import javax.sip.header.ExpiresHeader; +import javax.sip.message.Response; +import java.text.ParseException; + +/** + * SIP命令类型: SUBSCRIBE请求 + * @author lin + */ +@Slf4j +@Component +public class SubscribeRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor { + + private final String method = "SUBSCRIBE"; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private SubscribeHolder subscribeHolder; + + @Autowired + private SIPSender sipSender; + + + @Autowired + private IPlatformService platformService; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addRequestProcessor(method, this); + } + + /** + * 处理SUBSCRIBE请求 + * + * @param evt 事件 + */ + @Override + public void process(RequestEvent evt) { + SIPRequest request = (SIPRequest) evt.getRequest(); + try { + Element rootElement = getRootElement(evt); + if (rootElement == null) { + log.error("处理SUBSCRIBE请求 未获取到消息体{}", evt.getRequest()); + responseAck(request, Response.BAD_REQUEST); + return; + } + ExpiresHeader expires = request.getExpires(); + if (expires == null) { + log.error("处理SUBSCRIBE请求 未获取到ExpiresHeader{}", evt.getRequest()); + responseAck(request, Response.BAD_REQUEST, "missing expires"); + return; + } + String platformId = SipUtils.getUserIdFromFromHeader(request); + String cmd = XmlUtil.getText(rootElement, "CmdType"); + log.info("[收到订阅请求] 类型: {}, 来自: {}", cmd, platformId); + if (CmdType.MOBILE_POSITION.equals(cmd)) { + processNotifyMobilePosition(request, rootElement); +// } else if (CmdType.ALARM.equals(cmd)) { +// logger.info("接收到Alarm订阅"); +// processNotifyAlarm(serverTransaction, rootElement); + } else if (CmdType.CATALOG.equals(cmd)) { + processNotifyCatalogList(request, rootElement); + } else { + log.info("接收到消息:" + cmd); + + Response response = getMessageFactory().createResponse(200, request); + if (response != null) { + ExpiresHeader expireHeader = getHeaderFactory().createExpiresHeader(30); + response.setExpires(expireHeader); + } + log.info("response : " + response); + sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response); + } + } catch (ParseException | SipException | InvalidArgumentException | DocumentException e) { + log.error("未处理的异常 ", e); + } + + } + + /** + * 处理移动位置订阅消息 + */ + private void processNotifyMobilePosition(SIPRequest request, Element rootElement) throws SipException { + if (request == null) { + return; + } + String platformId = SipUtils.getUserIdFromFromHeader(request); + String deviceId = XmlUtil.getText(rootElement, "DeviceID"); + Platform platform = platformService.queryPlatformByServerGBId(platformId); + if (platform == null) { + return; + } + + String sn = XmlUtil.getText(rootElement, "SN"); + log.info("[回复上级的移动位置订阅请求]: {}", platformId); + StringBuilder resultXml = new StringBuilder(200); + resultXml.append("\r\n") + .append("\r\n") + .append("MobilePosition\r\n") + .append("").append(sn).append("\r\n") + .append("").append(deviceId).append("\r\n") + .append("OK\r\n") + .append("\r\n"); + + + + try { + int expires = request.getExpires().getExpires(); + SIPResponse response = responseXmlAck(request, resultXml.toString(), platform, expires); + + SubscribeInfo subscribeInfo = SubscribeInfo.getInstance(response, platformId, expires, + (EventHeader)request.getHeader(EventHeader.NAME)); + if (subscribeInfo.getExpires() > 0) { + // GPS上报时间间隔 + String interval = XmlUtil.getText(rootElement, "Interval"); + if (interval == null) { + subscribeInfo.setGpsInterval(5); + }else { + subscribeInfo.setGpsInterval(Integer.parseInt(interval)); + } + subscribeInfo.setSn(sn); + } + if (subscribeInfo.getExpires() == 0) { + subscribeHolder.removeMobilePositionSubscribe(platformId); + }else { + subscribeInfo.setTransactionInfo(new SipTransactionInfo(response)); + subscribeHolder.putMobilePositionSubscribe(platformId, subscribeInfo, ()->{ + platformService.sendNotifyMobilePosition(platformId); + }); + } + + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("未处理的异常 ", e); + } + } + + private void processNotifyAlarm(RequestEvent evt, Element rootElement) { + + } + + private void processNotifyCatalogList(SIPRequest request, Element rootElement) throws SipException { + if (request == null) { + log.info("[处理目录订阅] 发现request为NUll。已忽略"); + return; + } + String platformId = SipUtils.getUserIdFromFromHeader(request); + String deviceId = XmlUtil.getText(rootElement, "DeviceID"); + Platform platform = platformService.queryPlatformByServerGBId(platformId); + if (platform == null){ + log.info("[处理目录订阅] 未找到平台 {}。已忽略", platformId); + return; + } + + String sn = XmlUtil.getText(rootElement, "SN"); + log.info("[回复上级的目录订阅请求]: {}/{}", platformId, deviceId); + StringBuilder resultXml = new StringBuilder(200); + resultXml.append("\r\n") + .append("\r\n") + .append("Catalog\r\n") + .append("").append(sn).append("\r\n") + .append("").append(deviceId).append("\r\n") + .append("OK\r\n") + .append("\r\n"); + + try { + int expires = request.getExpires().getExpires(); + Platform parentPlatform = platformService.queryPlatformByServerGBId(platformId); + SIPResponse response = responseXmlAck(request, resultXml.toString(), parentPlatform, expires); + + SubscribeInfo subscribeInfo = SubscribeInfo.getInstance(response, platformId, expires, + (EventHeader)request.getHeader(EventHeader.NAME)); + + if (subscribeInfo.getExpires() == 0) { + subscribeHolder.removeCatalogSubscribe(platformId); + }else { + subscribeInfo.setTransactionInfo(new SipTransactionInfo(response)); + subscribeHolder.putCatalogSubscribe(platformId, subscribeInfo); + } + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("未处理的异常 ", e); + } + if (subscribeHolder.getCatalogSubscribe(platformId) == null + && platform.getAutoPushChannel() != null && platform.getAutoPushChannel()) { + platformService.addSimulatedSubscribeInfo(platform); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/info/InfoRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/info/InfoRequestProcessor.java new file mode 100755 index 0000000..02408aa --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/info/InfoRequestProcessor.java @@ -0,0 +1,154 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.info; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.service.*; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.CallIdHeader; +import javax.sip.header.ContentTypeHeader; +import javax.sip.message.Response; +import java.text.ParseException; + +/** + * INFO 一般用于国标级联时的回放控制 + */ +@Slf4j +@Component +public class InfoRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor { + + private final String method = "INFO"; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private IPlatformService platformService; + + @Autowired + private SipSubscribe sipSubscribe; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private SIPCommander cmder; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addRequestProcessor(method, this); + } + + @Override + public void process(RequestEvent evt) { + SIPRequest request = (SIPRequest) evt.getRequest(); + CallIdHeader callIdHeader = request.getCallIdHeader(); + // 先从会话内查找 + try { + SendRtpInfo sendRtpInfo = sendRtpServerService.queryByCallId(callIdHeader.getCallId()); + if (sendRtpInfo == null || !sendRtpInfo.isSendToPlatform()) { + // 不存在则回复404 + log.warn("[INFO 消息] 事务未找到, callID: {}", callIdHeader.getCallId()); + responseAck(request, Response.NOT_FOUND, "transaction not found"); + return; + } + // 查询上级平台是否存在 + Platform platform = platformService.queryPlatformByServerGBId(sendRtpInfo.getTargetId()); + if (platform == null || !platform.isStatus()) { + // 不存在则回复404 + log.warn("[INFO 消息] 平台未找到或者已离线: 平台: {}", sendRtpInfo.getTargetId()); + responseAck(request, Response.NOT_FOUND, "platform "+ sendRtpInfo.getTargetId() +" not found or offline"); + return; + } + CommonGBChannel channel = channelService.getOne(sendRtpInfo.getChannelId()); + if (channel == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道不存在: 通道ID: {}", sendRtpInfo.getChannelId()); + responseAck(request, Response.NOT_FOUND, "channel not found or offline"); + return; + } + // 判断通道类型 + if (channel.getDataType() != ChannelDataType.GB28181) { + // 非国标通道不支持录像回放控制 + log.warn("[INFO 消息] 非国标通道不支持录像回放控制: 通道ID: {}", sendRtpInfo.getChannelId()); + responseAck(request, Response.FORBIDDEN, ""); + return; + } + + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道所属设备不存在, 通道ID: {}", sendRtpInfo.getChannelId()); + responseAck(request, Response.NOT_FOUND, "platform "+ sendRtpInfo.getChannelId() +" not found or offline"); + return; + } + // 获取通道的原始信息 + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(sendRtpInfo.getChannelId()); + // 向原始通道转发控制消息 + ContentTypeHeader header = (ContentTypeHeader)evt.getRequest().getHeader(ContentTypeHeader.NAME); + String contentType = header.getContentType(); + String contentSubType = header.getContentSubType(); + if ("Application".equalsIgnoreCase(contentType) && "MANSRTSP".equalsIgnoreCase(contentSubType)) { + log.info("[INFO 消息] 平台: {}->{}({})/{}", platform.getServerGBId(), device.getName(), + device.getDeviceId(), deviceChannel.getId()); + // 不解析协议, 直接转发给对应的设备 + cmder.playbackControlCmd(device, deviceChannel, sendRtpInfo.getStream(), new String(evt.getRequest().getRawContent()), eventResult -> { + // 失败的回复 + try { + responseAck(request, eventResult.statusCode, eventResult.msg); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 录像控制: {}", e.getMessage()); + } + }, eventResult -> { + // 成功的回复 + try { + responseAck(request, eventResult.statusCode); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 录像控制: {}", e.getMessage()); + } + }); + } + } catch (SipException e) { + log.warn("SIP 回复错误", e); + } catch (InvalidArgumentException e) { + log.warn("参数无效", e); + } catch (ParseException e) { + log.warn("SIP回复时解析异常", e); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/IMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/IMessageHandler.java new file mode 100755 index 0000000..085f921 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/IMessageHandler.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import org.dom4j.Element; + +import javax.sip.RequestEvent; + +public interface IMessageHandler { + /** + * 处理来自设备的信息 + * @param evt + * @param device + */ + void handForDevice(RequestEvent evt, Device device, Element element); + + /** + * 处理来自平台的信息 + * @param evt + * @param parentPlatform + */ + void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageHandlerAbstract.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageHandlerAbstract.java new file mode 100755 index 0000000..7a743d1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageHandlerAbstract.java @@ -0,0 +1,93 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.event.MessageSubscribe; +import com.genersoft.iot.vmp.gb28181.event.sip.MessageEvent; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd.CatalogQueryMessageHandler; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +@Slf4j +public abstract class MessageHandlerAbstract extends SIPRequestProcessorParent implements IMessageHandler{ + + public Map messageHandlerMap = new ConcurrentHashMap<>(); + + @Autowired + private IPlatformService platformService; + + @Autowired + private MessageSubscribe messageSubscribe; + + public void addHandler(String cmdType, IMessageHandler messageHandler) { + messageHandlerMap.put(cmdType, messageHandler); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + String cmd = getText(element, "CmdType"); + if (cmd == null) { + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 回复200 OK: {}", e.getMessage()); + } + return; + } + IMessageHandler messageHandler = messageHandlerMap.get(cmd); + + if (messageHandler != null) { + //两个国标平台互相级联时由于上一步判断导致本该在平台处理的消息 放到了设备的处理逻辑 + //所以对目录查询单独做了校验 + if(messageHandler instanceof CatalogQueryMessageHandler){ + Platform parentPlatform = platformService.queryPlatformByServerGBId(device.getDeviceId()); + messageHandler.handForPlatform(evt, parentPlatform, element); + return; + } + messageHandler.handForDevice(evt, device, element); + }else { + handMessageEvent(element, null); + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + String cmd = getText(element, "CmdType"); + IMessageHandler messageHandler = messageHandlerMap.get(cmd); + if (messageHandler != null) { + messageHandler.handForPlatform(evt, parentPlatform, element); + } + } + + + public void handMessageEvent(Element element, Object data) { + + String cmd = getText(element, "CmdType"); + String sn = getText(element, "SN"); + MessageEvent subscribe = (MessageEvent)messageSubscribe.getSubscribe(cmd + sn); + if (subscribe != null && subscribe.getCallback() != null) { + String result = getText(element, "Result"); + if (result == null || "OK".equalsIgnoreCase(result) || data != null) { + subscribe.getCallback().run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), data); + }else { + subscribe.getCallback().run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), result); + } + messageSubscribe.removeSubscribe(cmd + sn); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageRequestProcessor.java new file mode 100755 index 0000000..a8c1062 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageRequestProcessor.java @@ -0,0 +1,143 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceNotFoundEvent; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.event.sip.SipEvent; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.CSeqHeader; +import javax.sip.header.CallIdHeader; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Component +public class MessageRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor { + + private final String method = "MESSAGE"; + + private static final Map messageHandlerMap = new ConcurrentHashMap<>(); + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private IPlatformService platformService; + + @Autowired + private SipSubscribe sipSubscribe; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addRequestProcessor(method, this); + } + + public void addHandler(String name, IMessageHandler handler) { + messageHandlerMap.put(name, handler); + } + + @Override + public void process(RequestEvent evt) { + SIPRequest sipRequest = (SIPRequest)evt.getRequest(); +// logger.info("接收到消息:" + evt.getRequest()); + String deviceId = SipUtils.getUserIdFromFromHeader(evt.getRequest()); + CallIdHeader callIdHeader = sipRequest.getCallIdHeader(); + CSeqHeader cSeqHeader = sipRequest.getCSeqHeader(); + // 先从会话内查找 + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByCallId(callIdHeader.getCallId()); + // 兼容海康 媒体通知 消息from字段不是设备ID的问题 + if (ssrcTransaction != null) { + deviceId = ssrcTransaction.getDeviceId(); + } + SIPRequest request = (SIPRequest) evt.getRequest(); + // 查询设备是否存在 + Device device = redisCatchStorage.getDevice(deviceId); + // 查询上级平台是否存在 + Platform parentPlatform = platformService.queryPlatformByServerGBId(deviceId); + try { + if (device != null && parentPlatform != null) { + String hostAddress = request.getRemoteAddress().getHostAddress(); + int remotePort = request.getRemotePort(); + if (device.getHostAddress().equals(hostAddress + ":" + remotePort)) { + parentPlatform = null; + }else { + device = null; + } + } + if (device == null && parentPlatform == null) { + // 不存在则回复404 + responseAck(request, Response.NOT_FOUND, "device "+ deviceId +" not found"); + log.warn("[设备未找到 ]deviceId: {}, callId: {}", deviceId, callIdHeader.getCallId()); + SipEvent sipEvent = sipSubscribe.getSubscribe(callIdHeader.getCallId() + cSeqHeader.getSeqNumber()); + if (sipEvent != null && sipEvent.getErrorEvent() != null){ + DeviceNotFoundEvent deviceNotFoundEvent = new DeviceNotFoundEvent(callIdHeader.getCallId()); + SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(deviceNotFoundEvent); + sipEvent.getErrorEvent().response(eventResult); + } + }else { + Element rootElement; + try { + rootElement = getRootElement(evt); + if (rootElement == null) { + log.error("处理MESSAGE请求 未获取到消息体{}", evt.getRequest()); + responseAck(request, Response.BAD_REQUEST, "content is null"); + return; + } + String name = rootElement.getName(); + IMessageHandler messageHandler = messageHandlerMap.get(name); + if (messageHandler != null) { + if (device != null) { + messageHandler.handForDevice(evt, device, rootElement); + }else { // 由于上面已经判断都为null则直接返回,所以这里device和parentPlatform必有一个不为null + messageHandler.handForPlatform(evt, parentPlatform, rootElement); + } + }else { + // 不支持的message + // 不存在则回复415 + responseAck(request, Response.UNSUPPORTED_MEDIA_TYPE, "Unsupported message type, must Control/Notify/Query/Response"); + } + } catch (DocumentException e) { + log.warn("解析XML消息内容异常", e); + // 不存在则回复404 + responseAck(request, Response.BAD_REQUEST, e.getMessage()); + } + } + } catch (SipException e) { + log.warn("SIP 回复错误", e); + } catch (InvalidArgumentException e) { + log.warn("参数无效", e); + } catch (ParseException e) { + log.warn("SIP回复时解析异常", e); + } + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/control/ControlMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/control/ControlMessageHandler.java new file mode 100755 index 0000000..235a477 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/control/ControlMessageHandler.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.control; + +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageHandlerAbstract; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageRequestProcessor; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 命令类型: 控制命令 + * 命令类型: 设备控制: 远程启动, 录像控制(TODO), 报警布防/撤防命令(TODO), 报警复位命令(TODO), + * 强制关键帧命令(TODO), 拉框放大/缩小控制命令(TODO), 看守位控制(TODO), 报警复位(TODO) + * 命令类型: 设备配置: SVAC编码配置(TODO), 音频参数(TODO), SVAC解码配置(TODO) + */ +@Component +public class ControlMessageHandler extends MessageHandlerAbstract implements InitializingBean { + + private final String messageType = "Control"; + + @Autowired + private MessageRequestProcessor messageRequestProcessor; + + @Override + public void afterPropertiesSet() throws Exception { + messageRequestProcessor.addHandler(messageType, this); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/control/cmd/DeviceControlQueryMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/control/cmd/DeviceControlQueryMessageHandler.java new file mode 100755 index 0000000..e22e136 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/control/cmd/DeviceControlQueryMessageHandler.java @@ -0,0 +1,639 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.control.cmd; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.common.enums.DeviceControlType; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelControlService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.control.ControlMessageHandler; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.address.SipURI; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.List; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.loadElement; + +@Slf4j +@Component +public class DeviceControlQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "DeviceControl"; + + @Autowired + private ControlMessageHandler controlMessageHandler; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IGbChannelControlService channelControlService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private SIPCommander cmder; + + @Override + public void afterPropertiesSet() throws Exception { + controlMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform platform, Element rootElement) { + + SIPRequest request = (SIPRequest) evt.getRequest(); + + // 此处是上级发出的DeviceControl指令 + String targetGBId = ((SipURI) request.getToHeader().getAddress().getURI()).getUser(); + String channelId = getText(rootElement, "DeviceID"); + // 远程启动功能 + if (!ObjectUtils.isEmpty(getText(rootElement, "TeleBoot"))) { + // 拒绝远程启动命令 + log.warn("[deviceControl] 远程启动命令, 禁用,不允许上级平台随意重启下级平台"); + try { + responseAck(request, Response.FORBIDDEN); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + DeviceControlType deviceControlType = DeviceControlType.typeOf(rootElement); + + CommonGBChannel channel = channelService.queryOneWithPlatform(platform.getId(), channelId); + if (channel == null) { + log.warn("[deviceControl] 未找到通道, 平台: {}({}),通道编号:{}", platform.getName(), + platform.getServerGBId(), channelId); + try { + responseAck(request, Response.NOT_FOUND, "channel not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + log.info("[deviceControl] 命令: {}, 平台: {}({})->{}", deviceControlType, platform.getName(), + platform.getServerGBId(), channel.getGbId()); + + if (!ObjectUtils.isEmpty(deviceControlType)) { + switch (deviceControlType) { + case PTZ: + handlePtzCmd(channel, rootElement, request, DeviceControlType.PTZ); + break; + case ALARM: + handleAlarmCmd(channel, rootElement, request); + break; + case GUARD: + handleGuardCmd(channel, rootElement, request, DeviceControlType.GUARD); + break; + case RECORD: + handleRecordCmd(channel, rootElement, request, DeviceControlType.RECORD); + break; + case I_FRAME: + handleIFameCmd(channel, request); + break; + case TELE_BOOT: + handleTeleBootCmd(channel, request); + break; + case DRAG_ZOOM_IN: + handleDragZoom(channel, rootElement, request, DeviceControlType.DRAG_ZOOM_IN); + break; + case DRAG_ZOOM_OUT: + handleDragZoom(channel, rootElement, request, DeviceControlType.DRAG_ZOOM_OUT); + break; + case HOME_POSITION: + handleHomePositionCmd(channel, rootElement, request, DeviceControlType.HOME_POSITION); + break; + default: + break; + } + } + } + + /** + * 处理云台指令 + */ + private void handlePtzCmd(CommonGBChannel channel, Element rootElement, SIPRequest request, DeviceControlType type) { + if (channel.getDataType() == ChannelDataType.GB28181) { + + deviceChannelService.handlePtzCmd(channel.getDataDeviceId(), channel.getGbId(), rootElement, type, ((code, msg, data) -> { + try { + responseAck(request, code, msg); + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] 云台指令: {}", exception.getMessage()); + } + })); + }else { + // 解析云台控制参数 + String cmdString = getText(rootElement, type.getVal()); + IFrontEndControlCode frontEndControlCode = FrontEndCode.decode(cmdString); + if (frontEndControlCode == null) { + log.info("[INFO 消息] 不支持的控制方式"); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] 云台指令: {}", exception.getMessage()); + } + return; + } + switch (frontEndControlCode.getType()){ + case PTZ: + channelControlService.ptz(channel, (FrontEndControlCodeForPTZ)frontEndControlCode, ((code, msg, data) -> { + try { + if (code == ErrorCode.SUCCESS.getCode()) { + responseAck(request, Response.OK); + }else { + responseAck(request, Response.FORBIDDEN); + } + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] 云台指令: {}", exception.getMessage()); + } + })); + break; + case FI: + channelControlService.fi(channel, (FrontEndControlCodeForFI) frontEndControlCode, ((code, msg, data) -> { + try { + if (code == ErrorCode.SUCCESS.getCode()) { + responseAck(request, Response.OK); + }else { + responseAck(request, Response.FORBIDDEN); + } + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] FI指令: {}", exception.getMessage()); + } + })); + break; + case PRESET: + channelControlService.preset(channel, (FrontEndControlCodeForPreset) frontEndControlCode, ((code, msg, data) -> { + try { + if (code == ErrorCode.SUCCESS.getCode()) { + responseAck(request, Response.OK); + }else { + responseAck(request, Response.FORBIDDEN); + } + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] 预置位指令: {}", exception.getMessage()); + } + })); + break; + case TOUR: + channelControlService.tour(channel, (FrontEndControlCodeForTour) frontEndControlCode, ((code, msg, data) -> { + try { + if (code == ErrorCode.SUCCESS.getCode()) { + responseAck(request, Response.OK); + }else { + responseAck(request, Response.FORBIDDEN); + } + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] 巡航指令: {}", exception.getMessage()); + } + })); + break; + case SCAN: + channelControlService.scan(channel, (FrontEndControlCodeForScan) frontEndControlCode, ((code, msg, data) -> { + try { + if (code == ErrorCode.SUCCESS.getCode()) { + responseAck(request, Response.OK); + }else { + responseAck(request, Response.FORBIDDEN); + } + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] 扫描指令: {}", exception.getMessage()); + } + })); + break; + case AUXILIARY: + channelControlService.auxiliary(channel, (FrontEndControlCodeForAuxiliary) frontEndControlCode, ((code, msg, data) -> { + try { + if (code == ErrorCode.SUCCESS.getCode()) { + responseAck(request, Response.OK); + }else { + responseAck(request, Response.FORBIDDEN); + } + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] 辅助开关指令: {}", exception.getMessage()); + } + })); + break; + default: + log.info("[INFO 消息] 设备不支持的控制方式"); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] 云台指令: {}", exception.getMessage()); + } + } + } + } + + /** + * 处理强制关键帧 + */ + private void handleIFameCmd(CommonGBChannel channel, SIPRequest request) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只支持国标的云台控制 + log.warn("[INFO 消息] 只支持国标的处理强制关键帧, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道所属设备不存在, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "device not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + if (deviceChannel == null) { + log.warn("[deviceControl] 未找到设备原始通道, 设备: {}({}),通道编号:{}", device.getName(), + device.getDeviceId(), channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "channel not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + log.info("[deviceControl] 命令: 强制关键帧, 设备: {}({}), 通道{}({}", device.getName(), device.getDeviceId(), + deviceChannel.getName(), deviceChannel.getDeviceId()); + try { + cmder.iFrameCmd(device, deviceChannel.getDeviceId()); + responseAck(request, Response.OK); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 强制关键帧: {}", e.getMessage()); + } + } + + /** + * 处理重启命令 + */ + private void handleTeleBootCmd(CommonGBChannel channel, SIPRequest request) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只支持国标的云台控制 + log.warn("[INFO 消息] 只支持国标的重启命令, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道所属设备不存在, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "device not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + try { + cmder.teleBootCmd(device); + responseAck(request, Response.OK); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 重启: {}", e.getMessage()); + } + + } + + /** + * 处理拉框控制 + */ + private void handleDragZoom(CommonGBChannel channel, Element rootElement, SIPRequest request, DeviceControlType type) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只支持国标的云台控制 + log.warn("[deviceControl-DragZoom] 只支持国标的拉框控制, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[deviceControl-DragZoom] 通道所属设备不存在, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "device not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + if (deviceChannel == null) { + log.warn("[deviceControl-DragZoom] 未找到设备原始通道, 设备: {}({}),通道编号:{}", device.getName(), + device.getDeviceId(), channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "channel not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + log.info("[deviceControl] 命令: {}, 设备: {}({}), 通道{}({}", type, device.getName(), device.getDeviceId(), + deviceChannel.getName(), deviceChannel.getDeviceId()); + try { + DragZoomRequest dragZoomRequest = loadElement(rootElement, DragZoomRequest.class); + DragZoomParam dragZoom = dragZoomRequest.getDragZoomIn(); + if (dragZoom == null) { + dragZoom = dragZoomRequest.getDragZoomOut(); + } + StringBuffer cmdXml = new StringBuffer(200); + cmdXml.append("<" + type.getVal() + ">\r\n"); + cmdXml.append("" + dragZoom.getLength() + "\r\n"); + cmdXml.append("" + dragZoom.getWidth() + "\r\n"); + cmdXml.append("" + dragZoom.getMidPointX() + "\r\n"); + cmdXml.append("" + dragZoom.getMidPointY() + "\r\n"); + cmdXml.append("" + dragZoom.getLengthX() + "\r\n"); + cmdXml.append("" + dragZoom.getLengthY() + "\r\n"); + cmdXml.append("\r\n"); + cmder.dragZoomCmd(device, deviceChannel.getDeviceId(), cmdXml.toString(), (code, msg, data) -> { + + }); + responseAck(request, Response.OK); + } catch (Exception e) { + log.error("[命令发送失败] 拉框控制: {}", e.getMessage()); + } + + } + + /** + * 处理看守位命令 + */ + private void handleHomePositionCmd(CommonGBChannel channel, Element rootElement, SIPRequest request, DeviceControlType type) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只支持国标的云台控制 + log.warn("[INFO 消息] 只支持国标的看守位命令, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道所属设备不存在, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "device not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + if (deviceChannel == null) { + log.warn("[deviceControl] 未找到设备原始通道, 设备: {}({}),通道编号:{}", device.getName(), + device.getDeviceId(), channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "channel not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + log.info("[deviceControl] 命令: {}, 设备: {}({}), 通道{}({}", type, device.getName(), device.getDeviceId(), + deviceChannel.getName(), deviceChannel.getDeviceId()); + try { + HomePositionRequest homePosition = loadElement(rootElement, HomePositionRequest.class); + //获取整个消息主体,我们只需要修改请求头即可 + HomePositionRequest.HomePosition info = homePosition.getHomePosition(); + cmder.homePositionCmd(device, deviceChannel.getDeviceId(), !"0".equals(info.getEnabled()), Integer.parseInt(info.getResetTime()), Integer.parseInt(info.getPresetIndex()), (code, msg, data) -> { + if (code == ErrorCode.SUCCESS.getCode()) { + onOk(request); + }else { + onError(request, code, msg); + } + }); + } catch (Exception e) { + log.error("[命令发送失败] 看守位设置: {}", e.getMessage()); + } + } + + /** + * 处理告警消息 + */ + private void handleAlarmCmd(CommonGBChannel channel, Element rootElement, SIPRequest request) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只支持国标的云台控制 + log.warn("[INFO 消息] 只支持国标的告警消息, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道所属设备不存在, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "device not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + //告警方法 + String alarmMethod = ""; + //告警类型 + String alarmType = ""; + List info = rootElement.elements("Info"); + if (info != null) { + for (Element element : info) { + alarmMethod = getText(element, "AlarmMethod"); + alarmType = getText(element, "AlarmType"); + } + } + try { + cmder.alarmResetCmd(device, alarmMethod, alarmType, (code, msg, data) -> { + if (code == ErrorCode.SUCCESS.getCode()) { + onOk(request); + }else { + onError(request, code, msg); + } + }); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 告警消息: {}", e.getMessage()); + } + } + + /** + * 处理录像控制 + */ + private void handleRecordCmd(CommonGBChannel channel, Element rootElement, SIPRequest request, DeviceControlType type) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只支持国标的云台控制 + log.warn("[INFO 消息] 只支持国标的息录像控制, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道所属设备不存在, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "device not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + if (deviceChannel == null) { + // 拒绝远程启动命令 + log.warn("[deviceControl] 未找到设备原始通道, 设备: {}({}),通道编号:{}", device.getName(), + device.getDeviceId(), channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "channel not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + log.info("[deviceControl] 命令: {}, 设备: {}({}), 通道{}({}", type, device.getName(), device.getDeviceId(), + deviceChannel.getName(), deviceChannel.getDeviceId()); + //获取整个消息主体,我们只需要修改请求头即可 + String cmdString = getText(rootElement, type.getVal()); + try { + cmder.recordCmd(device, deviceChannel.getDeviceId(), cmdString, (code, msg, data) -> { + if (code == ErrorCode.SUCCESS.getCode()) { + onOk(request); + }else { + onError(request, code, msg); + } + }); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 录像控制: {}", e.getMessage()); + } + } + + /** + * 处理报警布防/撤防命令 + */ + private void handleGuardCmd(CommonGBChannel channel, Element rootElement, SIPRequest request, DeviceControlType type) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只支持国标的云台控制 + log.warn("[INFO 消息] 只支持国标的报警布防/撤防命令, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道所属设备不存在, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "device not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + //获取整个消息主体,我们只需要修改请求头即可 + String cmdString = getText(rootElement, type.getVal()); + try { + cmder.guardCmd(device, cmdString,(code, msg, data) -> { + if (code == ErrorCode.SUCCESS.getCode()) { + onOk(request); + }else { + onError(request, code, msg); + } + }); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 布防/撤防命令: {}", e.getMessage()); + } + } + + + + + /** + * 错误响应处理 + * + */ + private void onError(SIPRequest request, Integer code, String msg) { + // 失败的回复 + try { + responseAck(request, code, msg); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 回复: {}", e.getMessage()); + } + } + + private void onError(SIPRequest request, SipSubscribe.EventResult errorResult) { + onError(request, errorResult.statusCode, errorResult.msg); + } + + /** + * 成功响应处理 + * + * @param request 请求 + */ + private void onOk(SIPRequest request) { + // 成功的回复 + try { + responseAck(request, Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 回复: {}", e.getMessage()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/NotifyMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/NotifyMessageHandler.java new file mode 100755 index 0000000..bb34189 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/NotifyMessageHandler.java @@ -0,0 +1,26 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify; + +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageHandlerAbstract; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageRequestProcessor; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 命令类型: 通知命令, 参看 A.2.5 通知命令 + * 命令类型: 状态信息(心跳)报送, 报警通知, 媒体通知, 移动设备位置数据,语音广播通知(TODO), 设备预置位(TODO) + * @author lin + */ +@Component +public class NotifyMessageHandler extends MessageHandlerAbstract implements InitializingBean { + + private final String messageType = "Notify"; + + @Autowired + private MessageRequestProcessor messageRequestProcessor; + + @Override + public void afterPropertiesSet() throws Exception { + messageRequestProcessor.addHandler(messageType, this); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/AlarmNotifyMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/AlarmNotifyMessageHandler.java new file mode 100755 index 0000000..5cb5ac9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/AlarmNotifyMessageHandler.java @@ -0,0 +1,276 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.service.IDeviceAlarmService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler; +import com.genersoft.iot.vmp.gb28181.utils.NumericUtil; +import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.DateUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +/** + * 报警事件的处理,参考:9.4 + */ +@Slf4j +@Component +public class AlarmNotifyMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "Alarm"; + + @Autowired + private NotifyMessageHandler notifyMessageHandler; + + @Autowired + private EventPublisher publisher; + + @Autowired + private UserSetting userSetting; + + @Autowired + private SipConfig sipConfig; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IDeviceAlarmService deviceAlarmService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Override + public void afterPropertiesSet() throws Exception { + notifyMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + if (taskQueue.size() >= userSetting.getMaxNotifyCountQueue()) { + log.error("[Alarm] 待处理消息队列已满 {},返回486 BUSY_HERE,消息不做处理", userSetting.getMaxNotifyCountQueue()); + return; + } + taskQueue.offer(new SipMsgInfo(evt, device, rootElement)); + } + + @Scheduled(fixedDelay = 200) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List handlerCatchDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + SipMsgInfo poll = taskQueue.poll(); + if (poll != null) { + handlerCatchDataList.add(poll); + } + } + if (handlerCatchDataList.isEmpty()) { + return; + } + for (SipMsgInfo sipMsgInfo : handlerCatchDataList) { + if (sipMsgInfo == null) { + continue; + } + RequestEvent evt = sipMsgInfo.getEvt(); + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 报警通知回复: {}", e.getMessage()); + } + try { + Device device = sipMsgInfo.getDevice(); + Element deviceIdElement = sipMsgInfo.getRootElement().element("DeviceID"); + String channelId = deviceIdElement.getText(); + + DeviceAlarm deviceAlarm = new DeviceAlarm(); + deviceAlarm.setCreateTime(DateUtil.getNow()); + deviceAlarm.setDeviceId(sipMsgInfo.getDevice().getDeviceId()); + deviceAlarm.setDeviceName(sipMsgInfo.getDevice().getName()); + deviceAlarm.setChannelId(channelId); + deviceAlarm.setAlarmPriority(getText(sipMsgInfo.getRootElement(), "AlarmPriority")); + deviceAlarm.setAlarmMethod(getText(sipMsgInfo.getRootElement(), "AlarmMethod")); + String alarmTime = XmlUtil.getText(sipMsgInfo.getRootElement(), "AlarmTime"); + if (alarmTime == null) { + continue; + } + deviceAlarm.setAlarmTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(alarmTime)); + String alarmDescription = getText(sipMsgInfo.getRootElement(), "AlarmDescription"); + if (alarmDescription == null) { + deviceAlarm.setAlarmDescription(""); + } else { + deviceAlarm.setAlarmDescription(alarmDescription); + } + String longitude = getText(sipMsgInfo.getRootElement(), "Longitude"); + if (longitude != null && NumericUtil.isDouble(longitude)) { + deviceAlarm.setLongitude(Double.parseDouble(longitude)); + } else { + deviceAlarm.setLongitude(0.00); + } + String latitude = getText(sipMsgInfo.getRootElement(), "Latitude"); + if (latitude != null && NumericUtil.isDouble(latitude)) { + deviceAlarm.setLatitude(Double.parseDouble(latitude)); + } else { + deviceAlarm.setLatitude(0.00); + } + + if (!ObjectUtils.isEmpty(deviceAlarm.getAlarmMethod()) && deviceAlarm.getAlarmMethod().contains(DeviceAlarmMethod.GPS.getVal() + "")) { + DeviceChannel deviceChannel = deviceChannelService.getOne(device.getDeviceId(), channelId); + if (deviceChannel == null) { + log.warn("[解析报警消息] 未找到通道:{}/{}", device.getDeviceId(), channelId); + } else { + MobilePosition mobilePosition = new MobilePosition(); + mobilePosition.setCreateTime(DateUtil.getNow()); + mobilePosition.setDeviceId(deviceAlarm.getDeviceId()); + mobilePosition.setChannelId(deviceChannel.getId()); + mobilePosition.setChannelDeviceId(deviceChannel.getDeviceId()); + mobilePosition.setTime(deviceAlarm.getAlarmTime()); + mobilePosition.setLongitude(deviceAlarm.getLongitude()); + mobilePosition.setLatitude(deviceAlarm.getLatitude()); + mobilePosition.setReportSource("GPS Alarm"); + + // 更新device channel 的经纬度 + deviceChannel.setLongitude(mobilePosition.getLongitude()); + deviceChannel.setLatitude(mobilePosition.getLatitude()); + deviceChannel.setGpsTime(mobilePosition.getTime()); + + deviceChannelService.updateChannelGPS(device, deviceChannel, mobilePosition); + } + } + if (!ObjectUtils.isEmpty(deviceAlarm.getDeviceId())) { + if (deviceAlarm.getAlarmMethod().contains(DeviceAlarmMethod.Video.getVal() + "")) { + deviceAlarm.setAlarmType(getText(sipMsgInfo.getRootElement().element("Info"), "AlarmType")); + } + } + if (log.isDebugEnabled()) { + log.debug("[收到报警通知]设备:{}, 内容:{}", device.getDeviceId(), JSON.toJSONString(deviceAlarm)); + } + // 作者自用判断,其他小伙伴需要此消息可以自行修改,但是不要提在pr里 + if (DeviceAlarmMethod.Other.getVal() == Integer.parseInt(deviceAlarm.getAlarmMethod())) { + // 发送给平台的报警信息。 发送redis通知 + log.info("[发送给平台的报警信息]内容:{}", JSONObject.toJSONString(deviceAlarm)); + AlarmChannelMessage alarmChannelMessage = new AlarmChannelMessage(); + if (deviceAlarm.getAlarmMethod() != null) { + alarmChannelMessage.setAlarmSn(Integer.parseInt(deviceAlarm.getAlarmMethod())); + } + alarmChannelMessage.setAlarmDescription(deviceAlarm.getAlarmDescription()); + if (deviceAlarm.getAlarmType() != null) { + alarmChannelMessage.setAlarmType(Integer.parseInt(deviceAlarm.getAlarmType())); + } + alarmChannelMessage.setGbId(channelId); + redisCatchStorage.sendAlarmMsg(alarmChannelMessage); + continue; + } + + log.debug("存储报警信息、报警分类"); + // 存储报警信息、报警分类 + if (sipConfig.isAlarm()) { + deviceAlarmService.add(deviceAlarm); + } + + if (redisCatchStorage.deviceIsOnline(sipMsgInfo.getDevice().getDeviceId())) { + publisher.deviceAlarmEventPublish(deviceAlarm); + } + } catch (Exception e) { + log.error("未处理的异常 ", e); + log.warn("[收到报警通知] 发现未处理的异常, {}\r\n{}", e.getMessage(), evt.getRequest()); + } + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element rootElement) { + log.info("收到来自平台[{}]的报警通知", parentPlatform.getServerGBId()); + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 报警通知回复: {}", e.getMessage()); + } + Element deviceIdElement = rootElement.element("DeviceID"); + String channelId = deviceIdElement.getText(); + + + DeviceAlarm deviceAlarm = new DeviceAlarm(); + deviceAlarm.setCreateTime(DateUtil.getNow()); + deviceAlarm.setDeviceId(parentPlatform.getServerGBId()); + deviceAlarm.setDeviceName(parentPlatform.getName()); + deviceAlarm.setChannelId(channelId); + deviceAlarm.setAlarmPriority(getText(rootElement, "AlarmPriority")); + deviceAlarm.setAlarmMethod(getText(rootElement, "AlarmMethod")); + String alarmTime = XmlUtil.getText(rootElement, "AlarmTime"); + if (alarmTime == null) { + return; + } + deviceAlarm.setAlarmTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(alarmTime)); + String alarmDescription = getText(rootElement, "AlarmDescription"); + if (alarmDescription == null) { + deviceAlarm.setAlarmDescription(""); + } else { + deviceAlarm.setAlarmDescription(alarmDescription); + } + String longitude = getText(rootElement, "Longitude"); + if (longitude != null && NumericUtil.isDouble(longitude)) { + deviceAlarm.setLongitude(Double.parseDouble(longitude)); + } else { + deviceAlarm.setLongitude(0.00); + } + String latitude = getText(rootElement, "Latitude"); + if (latitude != null && NumericUtil.isDouble(latitude)) { + deviceAlarm.setLatitude(Double.parseDouble(latitude)); + } else { + deviceAlarm.setLatitude(0.00); + } + + if (!ObjectUtils.isEmpty(deviceAlarm.getAlarmMethod())) { + + if (deviceAlarm.getAlarmMethod().contains(DeviceAlarmMethod.Video.getVal() + "")) { + deviceAlarm.setAlarmType(getText(rootElement.element("Info"), "AlarmType")); + } + } + + if (channelId.equals(parentPlatform.getDeviceGBId())) { + // 发送给平台的报警信息。 发送redis通知 + AlarmChannelMessage alarmChannelMessage = new AlarmChannelMessage(); + if (deviceAlarm.getAlarmMethod() != null) { + alarmChannelMessage.setAlarmSn(Integer.parseInt(deviceAlarm.getAlarmMethod())); + } + alarmChannelMessage.setAlarmDescription(deviceAlarm.getAlarmDescription()); + alarmChannelMessage.setGbId(channelId); + if (deviceAlarm.getAlarmType() != null) { + alarmChannelMessage.setAlarmType(Integer.parseInt(deviceAlarm.getAlarmType())); + } + redisCatchStorage.sendAlarmMsg(alarmChannelMessage); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/BroadcastNotifyMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/BroadcastNotifyMessageHandler.java new file mode 100644 index 0000000..d185408 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/BroadcastNotifyMessageHandler.java @@ -0,0 +1,212 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.*; +import com.genersoft.iot.vmp.gb28181.session.AudioBroadcastManager; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; + +/** + * 语音喊话请求 + */ +@Slf4j +@Component +public class BroadcastNotifyMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final static String cmdType = "Broadcast"; + + @Autowired + private NotifyMessageHandler notifyMessageHandler; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private ISIPCommanderForPlatform commanderForPlatform; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private IPlayService playService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IPlatformService platformService; + + @Autowired + private AudioBroadcastManager audioBroadcastManager; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Override + public void afterPropertiesSet() throws Exception { + notifyMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform platform, Element rootElement) { + // 来自上级平台的语音喊话请求 + SIPRequest request = (SIPRequest) evt.getRequest(); + try { + Element snElement = rootElement.element("SN"); + if (snElement == null) { + responseAck(request, Response.BAD_REQUEST, "sn must not null"); + return; + } + String sn = snElement.getText(); + Element targetIDElement = rootElement.element("TargetID"); + if (targetIDElement == null) { + responseAck(request, Response.BAD_REQUEST, "TargetID must not null"); + return; + } + String targetId = targetIDElement.getText(); + + Element sourceIdElement = rootElement.element("SourceID"); + String sourceId; + if (sourceIdElement != null) { + sourceId = sourceIdElement.getText(); + }else { + sourceId = targetId; + } + log.info("[国标级联 语音喊话] platform: {}, channel: {}", platform.getServerGBId(), targetId); + + CommonGBChannel channel = channelService.queryOneWithPlatform(platform.getId(), targetId); + if (channel == null) { + log.warn("[国标级联 语音喊话] 未找到通道 platform: {}, channel: {}", platform.getServerGBId(), targetId); + responseAck(request, Response.NOT_FOUND, "TargetID not found"); + return; + } + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只支持国标的语音喊话 + log.warn("[INFO 消息] 只支持国标的语音喊话命令, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + // 向下级发送语音的喊话请求 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + responseAck(request, Response.NOT_FOUND, "device not found"); + return; + } + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + if (deviceChannel == null) { + responseAck(request, Response.NOT_FOUND, "channel not found"); + return; + } + responseAck(request, Response.OK); + + // 查看语音通道是否已经建立并且已经在使用 + if (playService.audioBroadcastInUse(device, deviceChannel)) { + commanderForPlatform.broadcastResultCmd(platform, channel, sn, false,null, null); + return; + } + + MediaServer mediaServerForMinimumLoad = mediaServerService.getMediaServerForMinimumLoad(null); + commanderForPlatform.broadcastResultCmd(platform, channel, sn, true, eventResult->{ + log.info("[国标级联] 语音喊话 回复失败 platform: {}, 错误:{}/{}", platform.getServerGBId(), eventResult.statusCode, eventResult.msg); + }, eventResult->{ + // 消息发送成功, 向上级发送invite,获取推流 + try { + platformService.broadcastInvite(platform, channel, sourceId, mediaServerForMinimumLoad, (hookData)->{ + // 上级平台推流成功 + AudioBroadcastCatch broadcastCatch = audioBroadcastManager.get(channel.getGbId()); + if (broadcastCatch != null ) { + + if (playService.audioBroadcastInUse(device, deviceChannel)) { + log.info("[国标级联] 语音喊话 设备正在使用中 platform: {}, channel: {}", + platform.getServerGBId(), channel.getGbDeviceId()); + // 查看语音通道已经建立且已经占用 回复BYE + platformService.stopBroadcast(platform, channel, hookData.getApp(), hookData.getStream(), true, hookData.getMediaServer()); + }else { + // 查看语音通道已经建立但是未占用 + broadcastCatch.setApp(hookData.getApp()); + broadcastCatch.setStream(hookData.getStream()); + broadcastCatch.setMediaServerItem(hookData.getMediaServer()); + audioBroadcastManager.update(broadcastCatch); + // 推流到设备 + SendRtpInfo sendRtpItem = sendRtpServerService.queryByStream(hookData.getStream(), targetId); + if (sendRtpItem == null) { + log.warn("[国标级联] 语音喊话 异常,未找到发流信息, channelId: {}, stream: {}", targetId, hookData.getStream()); + log.info("[国标级联] 语音喊话 重新开始,channelId: {}, stream: {}", targetId, hookData.getStream()); + try { + playService.audioBroadcastCmd(device, deviceChannel, hookData.getMediaServer(), hookData.getApp(), hookData.getStream(), 60, true, msg -> { + log.info("[语音喊话] 通道建立成功, device: {}, channel: {}", device.getDeviceId(), targetId); + }); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.info("[消息发送失败] 国标级联 语音喊话 platform: {}", platform.getServerGBId()); + } + }else { + // 发流 + try { + mediaServerService.startSendRtp(hookData.getMediaServer(), sendRtpItem); + }catch (ControllerException e) { + log.info("[语音喊话] 推流失败, 结果: {}", e.getMessage()); + return; + } + log.info("[语音喊话] 自动推流成功, device: {}, channel: {}", device.getDeviceId(), targetId); + } + } + }else { + try { + playService.audioBroadcastCmd(device, deviceChannel, hookData.getMediaServer(), hookData.getApp(), hookData.getStream(), 60, true, msg -> { + log.info("[语音喊话] 通道建立成功, device: {}, channel: {}", device.getDeviceId(), targetId); + }); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.info("[消息发送失败] 国标级联 语音喊话 platform: {}", platform.getServerGBId()); + } + } + + }, eventResultForBroadcastInvite -> { + // 收到错误 + log.info("[国标级联-语音喊话] 与下级通道建立失败 device: {}, channel: {}, 错误:{}/{}", device.getDeviceId(), + targetId, eventResultForBroadcastInvite.statusCode, eventResultForBroadcastInvite.msg); + }, (code, msg)->{ + // 超时 + log.info("[国标级联-语音喊话] 与下级通道建立超时 device: {}, channel: {}, 错误:{}/{}", device.getDeviceId(), + targetId, code, msg); + }); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.info("[消息发送失败] 国标级联 语音喊话 invite消息 platform: {}", platform.getServerGBId()); + } + }); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.info("[消息发送失败] 国标级联 语音喊话 platform: {}", platform.getServerGBId()); + } + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java new file mode 100755 index 0000000..e0cabc7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java @@ -0,0 +1,144 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd; + +import com.genersoft.iot.vmp.common.RemoteAddressInfo; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.SipMsgInfo; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.task.deviceStatus.DeviceStatusTaskRunner; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.IpPortUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 状态信息(心跳)报送 + */ +@Slf4j +@Component +public class KeepaliveNotifyMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + + private final static String cmdType = "Keepalive"; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Autowired + private NotifyMessageHandler notifyMessageHandler; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private DeviceStatusTaskRunner statusTaskRunner; + + @Autowired + private UserSetting userSetting; + + @Autowired + private DynamicTask dynamicTask; + + @Override + public void afterPropertiesSet() throws Exception { + notifyMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + if (taskQueue.size() >= userSetting.getMaxNotifyCountQueue()) { + log.error("[心跳] 待处理消息队列已满 {},返回486 BUSY_HERE,消息不做处理", userSetting.getMaxNotifyCountQueue()); + return; + } + taskQueue.offer(new SipMsgInfo(evt, device, rootElement)); + } + + @Scheduled(fixedDelay = 100) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List handlerCatchDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + SipMsgInfo poll = taskQueue.poll(); + if (poll != null) { + handlerCatchDataList.add(poll); + } + } + if (handlerCatchDataList.isEmpty()) { + return; + } + List deviceListForUpdate = new ArrayList<>(); + for (SipMsgInfo sipMsgInfo : handlerCatchDataList) { + if (sipMsgInfo == null) { + continue; + } + RequestEvent evt = sipMsgInfo.getEvt(); + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 心跳回复: {}", e.getMessage()); + } + Device device = sipMsgInfo.getDevice(); + SIPRequest request = (SIPRequest) evt.getRequest(); + + RemoteAddressInfo remoteAddressInfo = SipUtils.getRemoteAddressFromRequest(request, userSetting.getSipUseSourceIpAsRemoteAddress()); + if (device.getIp() == null || !device.getIp().equalsIgnoreCase(remoteAddressInfo.getIp()) || device.getPort() != remoteAddressInfo.getPort()) { + log.info("[收到心跳] 地址变化, {}({}), {}:{}->{}", device.getName(), device.getDeviceId(), remoteAddressInfo.getIp(), remoteAddressInfo.getPort(), request.getLocalAddress().getHostAddress()); + device.setPort(remoteAddressInfo.getPort()); + device.setHostAddress(IpPortUtil.concatenateIpAndPort(remoteAddressInfo.getIp(), String.valueOf(remoteAddressInfo.getPort()))); + device.setIp(remoteAddressInfo.getIp()); + device.setLocalIp(request.getLocalAddress().getHostAddress()); + } + + device.setKeepaliveTime(DateUtil.getNow()); + + if (device.isOnLine()) { + deviceListForUpdate.add(device); + long expiresTime = Math.min(device.getExpires(), device.getHeartBeatInterval() * device.getHeartBeatCount()) * 1000L; + if (statusTaskRunner.containsKey(device.getDeviceId())) { + statusTaskRunner.updateDelay(device.getDeviceId(), expiresTime + System.currentTimeMillis()); + } + } else { + if (userSetting.getGbDeviceOnline() == 1) { + // 对于已经离线的设备判断他的注册是否已经过期 + deviceService.online(device, null); + } + } + } + if (!deviceListForUpdate.isEmpty()) { + deviceService.updateDeviceList(deviceListForUpdate); + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + // 个别平台保活不回复200OK会判定离线 + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 心跳回复: {}", e.getMessage()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MediaStatusNotifyMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MediaStatusNotifyMessageHandler.java new file mode 100755 index 0000000..41fa636 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MediaStatusNotifyMessageHandler.java @@ -0,0 +1,134 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd; + +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.*; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.CallIdHeader; +import javax.sip.message.Response; +import java.text.ParseException; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +/** + * 媒体通知 + */ +@Slf4j +@Component +public class MediaStatusNotifyMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "MediaStatus"; + + @Autowired + private NotifyMessageHandler notifyMessageHandler; + + @Autowired + private SIPCommander cmder; + + @Autowired + private SIPCommanderForPlatform sipCommanderFroPlatform; + + @Autowired + private IPlatformChannelService platformChannelService; + + @Autowired + private IPlatformService platformService; + + @Autowired + private HookSubscribe subscribe; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IPlayService playService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Override + public void afterPropertiesSet() throws Exception { + notifyMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 录像流推送完毕,回复200OK: {}", e.getMessage()); + } + CallIdHeader callIdHeader = (CallIdHeader)evt.getRequest().getHeader(CallIdHeader.NAME); + String NotifyType =getText(rootElement, "NotifyType"); + if ("121".equals(NotifyType)){ + log.info("[录像流]推送完毕,收到关流通知"); + + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByCallId(callIdHeader.getCallId()); + if (ssrcTransaction != null) { + log.info("[录像流]推送完毕,关流通知, device: {}, channelId: {}", ssrcTransaction.getDeviceId(), ssrcTransaction.getChannelId()); + InviteInfo inviteInfo = inviteStreamService.getInviteInfo(InviteSessionType.DOWNLOAD, ssrcTransaction.getChannelId(), ssrcTransaction.getStream()); + if (inviteInfo != null) { + playService.stop(inviteInfo); + } + // 去除监听流注销自动停止下载的监听 + Hook hook = Hook.getInstance(HookType.on_media_arrival, "rtp", ssrcTransaction.getStream(), ssrcTransaction.getMediaServerId()); + subscribe.removeSubscribe(hook); + if (ssrcTransaction.getPlatformId() != null) { + // 如果级联播放,需要给上级发送此通知 TODO 多个上级同时观看一个下级 可能存在停错的问题,需要将点播CallId进行上下级绑定 + SendRtpInfo sendRtpInfo = sendRtpServerService.queryByChannelId(ssrcTransaction.getChannelId(), ssrcTransaction.getPlatformId()); + if (sendRtpInfo != null) { + Platform parentPlatform = platformService.queryPlatformByServerGBId(sendRtpInfo.getTargetId()); + if (parentPlatform == null) { + log.warn("[级联消息发送]:发送MediaStatus发现上级平台{}不存在", sendRtpInfo.getTargetId()); + return; + } + CommonGBChannel channel = platformChannelService.queryChannelByPlatformIdAndChannelId(parentPlatform.getId(), sendRtpInfo.getChannelId()); + if (channel == null) { + log.warn("[级联消息发送]:发送MediaStatus发现通道{}不存在", sendRtpInfo.getChannelId()); + return; + } + try { + sipCommanderFroPlatform.sendMediaStatusNotify(parentPlatform, sendRtpInfo, channel); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 录像播放完毕: {}", e.getMessage()); + } + } + } + }else { + log.info("[录像流]推送完毕,关流通知, 但是未找到对应的下载信息"); + } + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MobilePositionNotifyMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MobilePositionNotifyMessageHandler.java new file mode 100755 index 0000000..ec65dc8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MobilePositionNotifyMessageHandler.java @@ -0,0 +1,141 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler; +import com.genersoft.iot.vmp.gb28181.utils.NumericUtil; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.utils.DateUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.concurrent.ConcurrentLinkedQueue; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +/** + * 移动设备位置数据通知,设备主动发起,不需要上级订阅 + */ +@Slf4j +@Component +public class MobilePositionNotifyMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "MobilePosition"; + + @Autowired + private NotifyMessageHandler notifyMessageHandler; + + @Autowired + private IDeviceChannelService deviceChannelService; + + private ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Qualifier("taskExecutor") + @Autowired + private ThreadPoolTaskExecutor taskExecutor; + + @Override + public void afterPropertiesSet() throws Exception { + notifyMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + + boolean isEmpty = taskQueue.isEmpty(); + taskQueue.offer(new SipMsgInfo(evt, device, rootElement)); + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 移动位置通知回复: {}", e.getMessage()); + } + if (isEmpty) { + taskExecutor.execute(() -> { + while (!taskQueue.isEmpty()) { + SipMsgInfo sipMsgInfo = taskQueue.poll(); + try { + Element rootElementAfterCharset = getRootElement(sipMsgInfo.getEvt(), sipMsgInfo.getDevice().getCharset()); + if (rootElementAfterCharset == null) { + log.warn("[移动位置通知] {}处理失败,未识别到信息体", device.getDeviceId()); + continue; + } + String channelId = getText(rootElementAfterCharset, "DeviceID"); + DeviceChannel deviceChannel = deviceChannelService.getOne(device.getDeviceId(), channelId); + if (deviceChannel == null) { + log.warn("[解析移动位置通知] 未找到通道:{}/{}", device.getDeviceId(), channelId); + continue; + } + + MobilePosition mobilePosition = new MobilePosition(); + mobilePosition.setCreateTime(DateUtil.getNow()); + if (!ObjectUtils.isEmpty(sipMsgInfo.getDevice().getName())) { + mobilePosition.setDeviceName(sipMsgInfo.getDevice().getName()); + } + mobilePosition.setDeviceId(sipMsgInfo.getDevice().getDeviceId()); + + mobilePosition.setChannelId(deviceChannel.getId()); + mobilePosition.setChannelDeviceId(deviceChannel.getDeviceId()); + String time = getText(rootElementAfterCharset, "Time"); + if (ObjectUtils.isEmpty(time)){ + mobilePosition.setTime(DateUtil.getNow()); + }else { + mobilePosition.setTime(SipUtils.parseTime(time)); + } + mobilePosition.setLongitude(Double.parseDouble(getText(rootElementAfterCharset, "Longitude"))); + mobilePosition.setLatitude(Double.parseDouble(getText(rootElementAfterCharset, "Latitude"))); + if (NumericUtil.isDouble(getText(rootElementAfterCharset, "Speed"))) { + mobilePosition.setSpeed(Double.parseDouble(getText(rootElementAfterCharset, "Speed"))); + } else { + mobilePosition.setSpeed(0.0); + } + if (NumericUtil.isDouble(getText(rootElementAfterCharset, "Direction"))) { + mobilePosition.setDirection(Double.parseDouble(getText(rootElementAfterCharset, "Direction"))); + } else { + mobilePosition.setDirection(0.0); + } + if (NumericUtil.isDouble(getText(rootElementAfterCharset, "Altitude"))) { + mobilePosition.setAltitude(Double.parseDouble(getText(rootElementAfterCharset, "Altitude"))); + } else { + mobilePosition.setAltitude(0.0); + } + mobilePosition.setReportSource("Mobile Position"); + + // 更新device channel 的经纬度 + deviceChannel.setLongitude(mobilePosition.getLongitude()); + deviceChannel.setLatitude(mobilePosition.getLatitude()); + deviceChannel.setGpsTime(mobilePosition.getTime()); + + deviceChannelService.updateChannelGPS(device, deviceChannel, mobilePosition); + + } catch (DocumentException e) { + log.error("未处理的异常 ", e); + } catch (Exception e) { + log.warn("[移动位置通知] 发现未处理的异常, \r\n{}", evt.getRequest()); + log.error("[移动位置通知] 异常内容: ", e); + } + } + }); + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/QueryMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/QueryMessageHandler.java new file mode 100755 index 0000000..9a29955 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/QueryMessageHandler.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query; + +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageHandlerAbstract; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageRequestProcessor; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 命令类型: 查询指令 + * 命令类型: 设备状态, 设备目录信息, 设备信息, 文件目录检索(TODO), 报警(TODO), 设备配置(TODO), 设备预置位(TODO), 移动设备位置数据(TODO) + */ +@Component +public class QueryMessageHandler extends MessageHandlerAbstract implements InitializingBean { + + private final String messageType = "Query"; + + @Autowired + private MessageRequestProcessor messageRequestProcessor; + + @Override + public void afterPropertiesSet() throws Exception { + messageRequestProcessor.addHandler(messageType, this); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/AlarmQueryMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/AlarmQueryMessageHandler.java new file mode 100755 index 0000000..adcfb86 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/AlarmQueryMessageHandler.java @@ -0,0 +1,51 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.QueryMessageHandler; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; + +@Slf4j +@Component +public class AlarmQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "Alarm"; + + @Autowired + private QueryMessageHandler queryMessageHandler; + + @Override + public void afterPropertiesSet() throws Exception { + queryMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element rootElement) { + + log.info("不支持alarm查询"); + try { + responseAck((SIPRequest) evt.getRequest(), Response.NOT_FOUND, "not support alarm query"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 alarm查询回复200OK: {}", e.getMessage()); + } + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/CatalogQueryMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/CatalogQueryMessageHandler.java new file mode 100755 index 0000000..dbe2b5e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/CatalogQueryMessageHandler.java @@ -0,0 +1,85 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.QueryMessageHandler; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.FromHeader; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.Collections; +import java.util.List; + +@Slf4j +@Component +public class CatalogQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "Catalog"; + + @Autowired + private QueryMessageHandler queryMessageHandler; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IPlatformChannelService platformChannelService; + + @Autowired + private SIPCommanderForPlatform cmderFroPlatform; + + + @Override + public void afterPropertiesSet() throws Exception { + queryMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + try { + // 回复200 OK + responseAck((SIPRequest) evt.getRequest(), Response.FORBIDDEN); + } catch (SipException | InvalidArgumentException | ParseException ignored) {} + } + + @Override + public void handForPlatform(RequestEvent evt, Platform platform, Element rootElement) { + + FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME); + try { + // 回复200 OK + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 目录查询回复200OK: {}", e.getMessage()); + } + Element snElement = rootElement.element("SN"); + String sn = snElement.getText(); + List channelList = platformChannelService.queryByPlatform(platform); + + try { + if (!channelList.isEmpty()) { + cmderFroPlatform.catalogQuery(channelList, platform, sn, fromHeader.getTag()); + }else { + // 回复无通道 + cmderFroPlatform.catalogQuery(Collections.emptyList(), platform, sn, fromHeader.getTag()); + } + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 目录查询回复: {}", e.getMessage()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/DeviceInfoQueryMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/DeviceInfoQueryMessageHandler.java new file mode 100755 index 0000000..648ea67 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/DeviceInfoQueryMessageHandler.java @@ -0,0 +1,136 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.QueryMessageHandler; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.FromHeader; +import javax.sip.message.Response; +import java.text.ParseException; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +@Slf4j +@Component +public class DeviceInfoQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "DeviceInfo"; + + @Autowired + private QueryMessageHandler queryMessageHandler; + + @Autowired + private SIPCommanderForPlatform cmderFroPlatform; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Override + public void afterPropertiesSet() throws Exception { + queryMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform platform, Element rootElement) { + log.info("[DeviceInfo查询]消息"); + SIPRequest request = (SIPRequest) evt.getRequest(); + FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME); + + String sn = rootElement.element("SN").getText(); + + /*根据WVP原有的数据结构,设备和通道是分开放置,设备信息都是存放在设备表里,通道表里的设备信息不可作为真实信息处理 + 大部分NVR/IPC设备对他的通道信息实现都是返回默认的值没有什么参考价值。NVR/IPC通道我们统一使用设备表的设备信息来作为返回。 + 我们这里使用查询数据库的方式来实现这个设备信息查询的功能,在其他地方对设备信息更新达到正确的目的。*/ + + String channelId = getText(rootElement, "DeviceID"); + // 查询这是通道id还是设备id + if (platform.getDeviceGBId().equals(channelId)) { + // id指向平台的国标编号,那么就是查询平台的信息 + try { + cmderFroPlatform.deviceInfoResponse(platform, null, sn, fromHeader.getTag()); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 DeviceInfo查询回复: {}", e.getMessage()); + } + return; + } + CommonGBChannel channel = channelService.queryOneWithPlatform(platform.getId(), channelId); + if (channel == null) { + // 不存在则回复404 + log.warn("[DeviceInfo] 通道不存在: 通道编号: {}", channelId); + try { + responseAck(request, Response.NOT_FOUND, "channel not found or offline"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] DeviceInfo查询回复: {}", e.getMessage()); + return; + } + return; + } + // 判断通道类型 + if (channel.getDataType() != ChannelDataType.GB28181) { + // 非国标通道不支持录像回放控制 + log.warn("[DeviceInfo] 非国标通道不支持录像回放控制: 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] DeviceInfo查询回复: {}", e.getMessage()); + return; + } + return; + } + + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[DeviceInfo] 通道所属设备不存在, 通道ID: {}", channel.getDataDeviceId()); + + try { + responseAck(request, Response.NOT_FOUND, "device not found "); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] DeviceInfo查询回复: {}", e.getMessage()); + return; + } + return; + } + try { + // 回复200 OK + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] DeviceInfo查询回复: {}", e.getMessage()); + return; + } + try { + cmderFroPlatform.deviceInfoResponse(platform, device, sn, fromHeader.getTag()); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 DeviceInfo查询回复: {}", e.getMessage()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/DeviceStatusQueryMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/DeviceStatusQueryMessageHandler.java new file mode 100755 index 0000000..2b3b8fc --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/DeviceStatusQueryMessageHandler.java @@ -0,0 +1,76 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.QueryMessageHandler; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.FromHeader; +import javax.sip.message.Response; +import java.text.ParseException; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +@Slf4j +@Component +public class DeviceStatusQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "DeviceStatus"; + + @Autowired + private QueryMessageHandler queryMessageHandler; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private ISIPCommanderForPlatform cmderFroPlatform; + + @Override + public void afterPropertiesSet() throws Exception { + queryMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element rootElement) { + + log.info("接收到DeviceStatus查询消息"); + FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME); + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 DeviceStatus查询回复200OK: {}", e.getMessage()); + } + String sn = rootElement.element("SN").getText(); + String channelId = getText(rootElement, "DeviceID"); + CommonGBChannel channel= channelService.queryOneWithPlatform(parentPlatform.getId(), channelId); + if (channel ==null){ + log.error("[平台没有该通道的使用权限]:platformId"+parentPlatform.getServerGBId()+" deviceID:"+channelId); + return; + } + try { + cmderFroPlatform.deviceStatusResponse(parentPlatform, channelId, sn, fromHeader.getTag(), "ON".equalsIgnoreCase(channel.getGbStatus())); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 DeviceStatus查询回复: {}", e.getMessage()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java new file mode 100755 index 0000000..b0b82ef --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java @@ -0,0 +1,212 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEventListener; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.QueryMessageHandler; +import com.genersoft.iot.vmp.utils.DateUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Component +public class RecordInfoQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "RecordInfo"; + + @Autowired + private QueryMessageHandler queryMessageHandler; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IGbChannelPlayService playService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private SIPCommanderForPlatform cmderFroPlatform; + + @Autowired + private SIPCommander commander; + + @Autowired + private RecordInfoEventListener recordInfoEventListener; + + @Override + public void afterPropertiesSet() throws Exception { + queryMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform platform, Element rootElement) { + + SIPRequest request = (SIPRequest) evt.getRequest(); + Element snElement = rootElement.element("SN"); + int sn = Integer.parseInt(snElement.getText()); + Element deviceIDElement = rootElement.element("DeviceID"); + String channelId = deviceIDElement.getText(); + Element startTimeElement = rootElement.element("StartTime"); + String startTime = null; + if (startTimeElement != null) { + startTime = startTimeElement.getText(); + } + Element endTimeElement = rootElement.element("EndTime"); + String endTime = null; + if (endTimeElement != null) { + endTime = endTimeElement.getText(); + } + Element secrecyElement = rootElement.element("Secrecy"); + int secrecy = 0; + if (secrecyElement != null) { + secrecy = Integer.parseInt(secrecyElement.getText().trim()); + } + String type = "all"; + Element typeElement = rootElement.element("Type"); + if (typeElement != null) { + type = typeElement.getText(); + } + + // 向国标设备请求录像数据 + CommonGBChannel channel = channelService.queryOneWithPlatform(platform.getId(), channelId); + if (channel == null) { + log.info("[平台查询录像记录] 未找到通道 {}/{}", platform.getName(), channelId ); + try { + responseAck(request, Response.BAD_REQUEST); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] [平台查询录像记录] 未找到通道: {}", e.getMessage()); + } + return; + } + if (channel.getDataType() == ChannelDataType.GB28181) { + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + log.warn("[平台查询录像记录] 未找到通道对应的设备 {}/{}", platform.getName(), channelId ); + try { + responseAck(request, Response.BAD_REQUEST); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] [平台查询录像记录] 未找到通道对应的设备: {}", e.getMessage()); + } + return; + } + // 获取通道的原始信息 + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + // 接收录像数据 + recordInfoEventListener.addEndEventHandler(device.getDeviceId(), deviceChannel.getDeviceId(), (recordInfo)->{ + try { + log.info("[国标级联] 录像查询收到数据, 通道: {},准备转发===", channelId); + cmderFroPlatform.recordInfo(channel, platform, request.getFromTag(), recordInfo); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 回复录像数据: {}", e.getMessage()); + } + }); + try { + commander.recordInfoQuery(device, deviceChannel.getDeviceId(), DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(startTime), + DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(endTime), sn, secrecy, type, (eventResult -> { + // 回复200 OK + try { + responseAck(request, Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 录像查询回复: {}", e.getMessage()); + } + }),(eventResult -> { + // 查询失败 + try { + responseAck(request, eventResult.statusCode, eventResult.msg); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 录像查询回复: {}", e.getMessage()); + } + })); + } catch (InvalidArgumentException | ParseException | SipException e) { + log.error("[命令发送失败] 录像查询: {}", e.getMessage()); + } + }else { + // 回复200 OK + try { + responseAck(request, Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 录像查询回复: {}", e.getMessage()); + } + + playService.queryRecord(channel, DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(startTime), + DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(endTime), + (code, msg, commonRecordInfoList) -> { + RecordInfo recordInfo = new RecordInfo(); + recordInfo.setSumNum(commonRecordInfoList.size()); + recordInfo.setChannelId(channelId); + recordInfo.setSn(sn + ""); + List recordList = new ArrayList<>(commonRecordInfoList.size()); + for (int i = 0; i < commonRecordInfoList.size(); i++) { + CommonRecordInfo commonRecordInfo = commonRecordInfoList.get(i); + RecordItem recordItem = new RecordItem(); + recordItem.setDeviceId(channelId); + recordItem.setName(commonRecordInfo.getStartTime()); + recordItem.setFilePath("/" + commonRecordInfo.getStartTime()); + recordItem.setAddress("/" + commonRecordInfo.getStartTime()); + recordItem.setStartTime(commonRecordInfo.getStartTime()); + recordItem.setEndTime(commonRecordInfo.getEndTime()); + recordItem.setSecrecy(0); + recordItem.setRecorderId(""); + recordItem.setType(""); + recordItem.setFileSize(commonRecordInfo.getFileSize()); + recordList.add(recordItem); + } + recordInfo.setRecordList(recordList); + + try { + log.info("[国标级联] 录像查询收到数据, 通道: {},准备转发===", channelId); + cmderFroPlatform.recordInfo(channel, platform, request.getFromTag(), recordInfo); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 回复录像数据: {}", e.getMessage()); + } + }); + } +// +// +// +// +// +// +// +// if (channel.getDataType() != ChannelDataType.GB28181) { +// log.info("[平台查询录像记录] 只支持查询国标28181的录像数据 {}/{}", platform.getName(), channelId ); +// try { +// responseAck(request, Response.NOT_IMPLEMENTED); // 回复未实现 +// } catch (SipException | InvalidArgumentException | ParseException e) { +// log.error("[命令发送失败] 平台查询录像记录: {}", e.getMessage()); +// } +// return; +// } + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/ResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/ResponseMessageHandler.java new file mode 100755 index 0000000..163288e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/ResponseMessageHandler.java @@ -0,0 +1,36 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageHandlerAbstract; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageRequestProcessor; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.RequestEvent; + +/** + * 命令类型: 请求动作的应答 + * 命令类型: 设备控制, 报警通知, 设备目录信息查询, 目录信息查询, 目录收到, 设备信息查询, 设备状态信息查询 ...... + */ +@Component +public class ResponseMessageHandler extends MessageHandlerAbstract implements InitializingBean { + + private final String messageType = "Response"; + + @Autowired + private MessageRequestProcessor messageRequestProcessor; + + + + @Override + public void afterPropertiesSet() throws Exception { + messageRequestProcessor.addHandler(messageType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + super.handForDevice(evt, device, element); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/AlarmResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/AlarmResponseMessageHandler.java new file mode 100755 index 0000000..ffbe907 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/AlarmResponseMessageHandler.java @@ -0,0 +1,61 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; + +@Slf4j +@Component +public class AlarmResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "Alarm"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + @Autowired + private DeferredResultHolder deferredResultHolder; + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 目录查询回复: {}", e.getMessage()); + } + JSONObject json = new JSONObject(); + XmlUtil.node2Json(rootElement, json); + if (log.isDebugEnabled()) { + log.debug(json.toJSONString()); + } + responseMessageHandler.handMessageEvent(rootElement, null); + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/BroadcastResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/BroadcastResponseMessageHandler.java new file mode 100755 index 0000000..2567b76 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/BroadcastResponseMessageHandler.java @@ -0,0 +1,99 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.session.AudioBroadcastManager; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +@Slf4j +@Component +public class BroadcastResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "Broadcast"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private AudioBroadcastManager audioBroadcastManager; + + @Autowired + private IPlayService playService; + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + + SIPRequest request = (SIPRequest) evt.getRequest(); + try { + String channelId = getText(rootElement, "DeviceID"); + DeviceChannel channel = null; + if (!channelId.equals(device.getDeviceId())) { + channel = deviceChannelService.getOneBySourceId(device.getId(), channelId); + }else { + channel = deviceChannelService.getBroadcastChannel(device.getId()); + } + if (channel == null) { + log.info("[语音广播]回复: 未找到通道{}/{}", device.getDeviceId(), channelId ); + // 回复410 + responseAck((SIPRequest) evt.getRequest(), Response.NOT_FOUND); + return; + } + if (!audioBroadcastManager.exit(channel.getId())) { + // 回复410 + responseAck((SIPRequest) evt.getRequest(), Response.BUSY_HERE); + return; + } + String result = getText(rootElement, "Result"); + Element infoElement = rootElement.element("Info"); + String reason = null; + if (infoElement != null) { + reason = getText(infoElement, "Reason"); + } + log.info("[语音广播]回复:{}, {}/{}", reason == null? result : result + ": " + reason, device.getDeviceId(), channelId ); + + // 回复200 OK + responseAck(request, Response.OK); + if (result.equalsIgnoreCase("OK")) { + AudioBroadcastCatch audioBroadcastCatch = audioBroadcastManager.get(channel.getId()); + audioBroadcastCatch.setStatus(AudioBroadcastCatchStatus.WaiteInvite); + audioBroadcastManager.update(audioBroadcastCatch); + }else { + playService.stopAudioBroadcast(device, channel); + } + } catch (ParseException | SipException | InvalidArgumentException e) { + log.error("[命令发送失败] 国标级联 语音喊话: {}", e.getMessage()); + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/CatalogResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/CatalogResponseMessageHandler.java new file mode 100755 index 0000000..738430a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/CatalogResponseMessageHandler.java @@ -0,0 +1,245 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IGroupService; +import com.genersoft.iot.vmp.gb28181.service.IRegionService; +import com.genersoft.iot.vmp.gb28181.session.CatalogDataManager; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import com.genersoft.iot.vmp.utils.Coordtransform; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 目录查询的回复 + */ +@Slf4j +@Component +public class CatalogResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "Catalog"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IRegionService regionService; + + @Autowired + private IGroupService groupService; + + @Autowired + private CatalogDataManager catalogDataCatch; + + @Autowired + private SipConfig sipConfig; + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + taskQueue.offer(new HandlerCatchData(evt, device, element)); + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 目录查询回复: {}", e.getMessage()); + } + } + + @Scheduled(fixedDelay = 50) + @Transactional + public void executeTaskQueue(){ + if (taskQueue.isEmpty()) { + return; + } + List handlerCatchDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + HandlerCatchData poll = taskQueue.poll(); + if (poll != null) { + handlerCatchDataList.add(poll); + } + } + if (handlerCatchDataList.isEmpty()) { + return; + } + for (HandlerCatchData take : handlerCatchDataList) { + if (take == null) { + continue; + } + RequestEvent evt = take.getEvt(); + int sn = 0; + // 全局异常捕获,保证下一条可以得到处理 + try { + Element rootElement = null; + try { + rootElement = getRootElement(take.getEvt(), take.getDevice().getCharset()); + } catch (DocumentException e) { + log.error("[xml解析] 失败: ", e); + continue; + } + if (rootElement == null) { + log.warn("[ 收到通道 ] content cannot be null, {}", evt.getRequest()); + continue; + } + Element deviceListElement = rootElement.element("DeviceList"); + Element sumNumElement = rootElement.element("SumNum"); + Element snElement = rootElement.element("SN"); + + sn = Integer.parseInt(snElement.getText()); + int sumNum = Integer.parseInt(sumNumElement.getText()); + + if (sumNum == 0) { + log.info("[收到通道]设备:{}的: 0个", take.getDevice().getDeviceId()); + // 数据已经完整接收 + deviceChannelService.cleanChannelsForDevice(take.getDevice().getId()); + // 推送空数据,不然无法及时结束 + catalogDataCatch.put(take.getDevice().getDeviceId(), sn, 0, take.getDevice(), + Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + catalogDataCatch.setChannelSyncEnd(take.getDevice().getDeviceId(), sn, null); + return; + } else { + Iterator deviceListIterator = deviceListElement.elementIterator(); + if (deviceListIterator != null) { + List channelList = new ArrayList<>(); + List regionList = new ArrayList<>(); + List groupList = new ArrayList<>(); + // 遍历DeviceList + while (deviceListIterator.hasNext()) { + Element itemDevice = deviceListIterator.next(); + Element channelDeviceElement = itemDevice.element("DeviceID"); + if (channelDeviceElement == null) { + // 总数减一, 避免最后总数不对 无法确定问题 + continue; + } + // 从xml解析内容到 DeviceChannel 对象 + DeviceChannel channel = DeviceChannel.decode(itemDevice); + if (channel.getDeviceId() == null) { + log.info("[收到目录订阅]:但是解析失败 {}", new String(evt.getRequest().getRawContent())); + continue; + } + channel.setDataDeviceId(take.getDevice().getId()); + if (channel.getParentId() != null && channel.getParentId().equals(sipConfig.getId())) { + channel.setParentId(null); + } + // 解析通道类型 + if (channel.getDeviceId().length() <= 8) { + // 行政区划 + Region region = Region.getInstance(channel); + regionList.add(region); + channel.setChannelType(1); + }else if (channel.getDeviceId().length() == 20){ + // 业务分组/虚拟组织 + Group group = Group.getInstance(channel); + if (group != null) { + channel.setParental(1); + channel.setChannelType(2); + groupList.add(group); + } + if (channel.getLongitude() != null && channel.getLatitude() != null && channel.getLongitude() > 0 && channel.getLatitude() > 0) { + Double[] wgs84Position = Coordtransform.GCJ02ToWGS84(channel.getLongitude(), channel.getLatitude()); + channel.setGbLongitude(wgs84Position[0]); + channel.setGbLatitude(wgs84Position[1]); + } + } + channelList.add(channel); + } + + catalogDataCatch.put(take.getDevice().getDeviceId(), sn, sumNum, take.getDevice(), + channelList, regionList, groupList); + log.info("[收到通道]设备: {} -> {}个,{}/{}", take.getDevice().getDeviceId(), channelList.size(), catalogDataCatch.size(take.getDevice().getDeviceId(), sn), sumNum); + } + } + } catch (Exception e) { + log.warn("[收到通道] 发现未处理的异常, \r\n{}", evt.getRequest()); + log.error("[收到通道] 异常内容: ", e); + } finally { + String deviceId = take.getDevice().getDeviceId(); + if (catalogDataCatch.size(deviceId, sn) > 0 + && catalogDataCatch.size(deviceId, sn) == catalogDataCatch.sumNum(deviceId, sn)) { + // 数据已经完整接收, 此时可能存在某个设备离线变上线的情况,但是考虑到性能,此处不做处理, + // 目前支持设备通道上线通知时和设备上线时向上级通知 + boolean resetChannelsResult = saveData(take.getDevice(), sn); + if (!resetChannelsResult) { + String errorMsg = "接收成功,写入失败,共" + catalogDataCatch.sumNum(deviceId, sn) + "条,已接收" + catalogDataCatch.getDeviceChannelList(take.getDevice().getDeviceId(), sn).size() + "条"; + catalogDataCatch.setChannelSyncEnd(deviceId, sn, errorMsg); + } else { + catalogDataCatch.setChannelSyncEnd(deviceId, sn, null); + } + } + } + } + } + + @Transactional + public boolean saveData(Device device, int sn) { + + boolean result = true; + List deviceChannelList = catalogDataCatch.getDeviceChannelList(device.getDeviceId(), sn); + if (deviceChannelList != null && !deviceChannelList.isEmpty()) { + result &= deviceChannelService.resetChannels(device.getId(), deviceChannelList); + } + + List regionList = catalogDataCatch.getRegionList(device.getDeviceId(), sn); + if ( regionList!= null && !regionList.isEmpty()) { + result &= regionService.batchAdd(regionList); + } + + List groupList = catalogDataCatch.getGroupList(device.getDeviceId(), sn); + if (groupList != null && !groupList.isEmpty()) { + result &= groupService.batchAdd(groupList); + } + return result; + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element rootElement) { + + } + + public SyncStatus getChannelSyncProgress(String deviceId) { + return catalogDataCatch.getSyncStatus(deviceId); + } + + public boolean isSyncRunning(String deviceId) { + return catalogDataCatch.isSyncRunning(deviceId); + } + + public void setChannelSyncReady(Device device, int sn) { + catalogDataCatch.addReady(device, sn); + } + + public void setChannelSyncEnd(String deviceId, int sn, String errorMsg) { + catalogDataCatch.setChannelSyncEnd(deviceId, sn, errorMsg); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/ConfigDownloadResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/ConfigDownloadResponseMessageHandler.java new file mode 100755 index 0000000..6aa5cfa --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/ConfigDownloadResponseMessageHandler.java @@ -0,0 +1,95 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; + +@Slf4j +@Component +public class ConfigDownloadResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "ConfigDownload"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + @Autowired + private DeferredResultHolder deferredResultHolder; + + @Autowired + private IDeviceService deviceService; + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + try { + // 回复200 OK + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 设备配置查询: {}", e.getMessage()); + } + // 此处是对本平台发出DeviceControl指令的应答 + JSONObject json = new JSONObject(); + XmlUtil.node2Json(element, json); + if (log.isDebugEnabled()) { + log.debug(json.toJSONString()); + } + JSONObject jsonObject = new JSONObject(); + if (json.get("BasicParam") != null) { + jsonObject.put("BasicParam", json.getJSONObject("BasicParam")); + } + if (json.get("VideoParamOpt") != null) { + jsonObject.put("VideoParamOpt", json.getJSONObject("VideoParamOpt")); + } + if (json.get("SVACEncodeConfig") != null) { + jsonObject.put("SVACEncodeConfig", json.getJSONObject("SVACEncodeConfig")); + } + if (json.get("SVACDecodeConfig") != null) { + jsonObject.put("SVACDecodeConfig", json.getJSONObject("SVACDecodeConfig")); + } + + responseMessageHandler.handMessageEvent(element, jsonObject); + + JSONObject basicParam = json.getJSONObject("BasicParam"); + if (basicParam != null) { + Integer heartBeatInterval = basicParam.getInteger("HeartBeatInterval"); + Integer heartBeatCount = basicParam.getInteger("HeartBeatCount"); + Integer positionCapability = basicParam.getInteger("PositionCapability"); + device.setHeartBeatInterval(heartBeatInterval); + device.setHeartBeatCount(heartBeatCount); + device.setPositionCapability(positionCapability); + + deviceService.updateDeviceHeartInfo(device); + } + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + // 不会收到上级平台的心跳信息 + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceInfoResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceInfoResponseMessageHandler.java new file mode 100755 index 0000000..24e7f58 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceInfoResponseMessageHandler.java @@ -0,0 +1,95 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +/** + * @author lin + */ +@Slf4j +@Component +public class DeviceInfoResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "DeviceInfo"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + + @Autowired + private IDeviceService deviceService; + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + log.debug("接收到DeviceInfo应答消息"); + // 检查设备是否存在, 不存在则不回复 + if (device == null || !device.isOnLine()) { + log.warn("[接收到DeviceInfo应答消息,但是设备已经离线]:" + (device != null ? device.getDeviceId():"" )); + return; + } + SIPRequest request = (SIPRequest) evt.getRequest(); + try { + rootElement = getRootElement(evt, device.getCharset()); + + if (rootElement == null) { + log.warn("[ 接收到DeviceInfo应答消息 ] content cannot be null, {}", evt.getRequest()); + try { + responseAck((SIPRequest) evt.getRequest(), Response.BAD_REQUEST); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] DeviceInfo应答消息 BAD_REQUEST: {}", e.getMessage()); + } + return; + } + device.setName(getText(rootElement, "DeviceName")); + + device.setManufacturer(getText(rootElement, "Manufacturer")); + device.setModel(getText(rootElement, "Model")); + device.setFirmware(getText(rootElement, "Firmware")); + if (ObjectUtils.isEmpty(device.getStreamMode())) { + device.setStreamMode("TCP-PASSIVE"); + } + deviceService.updateDevice(device); + responseMessageHandler.handMessageEvent(rootElement, device); + + } catch (DocumentException e) { + throw new RuntimeException(e); + } + try { + // 回复200 OK + responseAck(request, Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] DeviceInfo应答消息 200: {}", e.getMessage()); + } + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element rootElement) { + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceStatusResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceStatusResponseMessageHandler.java new file mode 100755 index 0000000..affd22f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceStatusResponseMessageHandler.java @@ -0,0 +1,70 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; + +@Slf4j +@Component +public class DeviceStatusResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "DeviceStatus"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + @Autowired + private IDeviceService deviceService; + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + log.info("接收到DeviceStatus应答消息"); + // 检查设备是否存在, 不存在则不回复 + if (device == null) { + return; + } + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 设备状态应答回复200OK: {}", e.getMessage()); + } + Element onlineElement = element.element("Online"); + JSONObject json = new JSONObject(); + XmlUtil.node2Json(element, json); + if (log.isDebugEnabled()) { + log.debug(json.toJSONString()); + } + String text = onlineElement.getText(); + responseMessageHandler.handMessageEvent(element, text); + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element rootElement) { + + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/MobilePositionResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/MobilePositionResponseMessageHandler.java new file mode 100755 index 0000000..cc72c02 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/MobilePositionResponseMessageHandler.java @@ -0,0 +1,141 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import com.genersoft.iot.vmp.gb28181.utils.NumericUtil; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.utils.DateUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +/** + * 移动设备位置数据查询回复 + * @author lin + */ +@Slf4j +@Component +public class MobilePositionResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "MobilePosition"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private DeferredResultHolder resultHolder; + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + SIPRequest request = (SIPRequest) evt.getRequest(); + + try { + rootElement = getRootElement(evt, device.getCharset()); + if (rootElement == null) { + log.warn("[ 移动设备位置数据查询回复 ] content cannot be null, {}", evt.getRequest()); + try { + responseAck(request, Response.BAD_REQUEST); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 移动设备位置数据查询 BAD_REQUEST: {}", e.getMessage()); + } + return; + } + String channelId = getText(rootElement, "DeviceID"); + DeviceChannel deviceChannel = deviceChannelService.getOne(device.getDeviceId(), channelId); + if (deviceChannel == null) { + log.warn("[解析报警消息] 未找到通道:{}/{}", device.getDeviceId(), channelId); + }else { + MobilePosition mobilePosition = new MobilePosition(); + mobilePosition.setCreateTime(DateUtil.getNow()); + if (!ObjectUtils.isEmpty(device.getName())) { + mobilePosition.setDeviceName(device.getName()); + } + mobilePosition.setDeviceId(device.getDeviceId()); + mobilePosition.setChannelId(deviceChannel.getId()); + mobilePosition.setChannelDeviceId(deviceChannel.getDeviceId()); + //兼容ISO 8601格式时间 + String time = getText(rootElement, "Time"); + if (ObjectUtils.isEmpty(time)){ + mobilePosition.setTime(DateUtil.getNow()); + }else { + mobilePosition.setTime(SipUtils.parseTime(time)); + } + mobilePosition.setLongitude(Double.parseDouble(getText(rootElement, "Longitude"))); + mobilePosition.setLatitude(Double.parseDouble(getText(rootElement, "Latitude"))); + if (NumericUtil.isDouble(getText(rootElement, "Speed"))) { + mobilePosition.setSpeed(Double.parseDouble(getText(rootElement, "Speed"))); + } else { + mobilePosition.setSpeed(0.0); + } + if (NumericUtil.isDouble(getText(rootElement, "Direction"))) { + mobilePosition.setDirection(Double.parseDouble(getText(rootElement, "Direction"))); + } else { + mobilePosition.setDirection(0.0); + } + if (NumericUtil.isDouble(getText(rootElement, "Altitude"))) { + mobilePosition.setAltitude(Double.parseDouble(getText(rootElement, "Altitude"))); + } else { + mobilePosition.setAltitude(0.0); + } + mobilePosition.setReportSource("Mobile Position"); + + // 更新device channel 的经纬度 + deviceChannel.setLongitude(mobilePosition.getLongitude()); + deviceChannel.setLatitude(mobilePosition.getLatitude()); + deviceChannel.setGpsTime(mobilePosition.getTime()); + + deviceChannelService.updateChannelGPS(device, deviceChannel, mobilePosition); + + String key = DeferredResultHolder.CALLBACK_CMD_MOBILE_POSITION + device.getDeviceId(); + RequestMessage msg = new RequestMessage(); + msg.setKey(key); + msg.setData(mobilePosition); + resultHolder.invokeAllResult(msg); + } + + //回复 200 OK + try { + responseAck(request, Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 移动设备位置数据查询 200: {}", e.getMessage()); + } + + } catch (DocumentException e) { + log.error("未处理的异常 ", e); + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/PresetQueryResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/PresetQueryResponseMessageHandler.java new file mode 100755 index 0000000..7984e69 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/PresetQueryResponseMessageHandler.java @@ -0,0 +1,178 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.MessageResponseTask; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.Preset; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.TimeUnit; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +/** + * 设备预置位查询应答 + */ +@Slf4j +@Component +public class PresetQueryResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "PresetQuery"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + private final Map> mesageMap = new ConcurrentHashMap<>(); + + private final DelayQueue> delayQueue = new DelayQueue<>(); + + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + + SIPRequest request = (SIPRequest) evt.getRequest(); + + try { + Element rootElement = getRootElement(evt, device.getCharset()); + + if (rootElement == null) { + log.warn("[ 设备预置位查询应答 ] content cannot be null, {}", evt.getRequest()); + try { + responseAck(request, Response.BAD_REQUEST); + } catch (InvalidArgumentException | ParseException | SipException e) { + log.error("[命令发送失败] 设备预置位查询应答处理: {}", e.getMessage()); + } + return; + } + Element presetListNumElement = rootElement.element("PresetList"); + Element snElement = rootElement.element("SN"); + //该字段可能为通道或则设备的id + if (snElement == null || presetListNumElement == null) { + try { + responseAck(request, Response.BAD_REQUEST, "xml error"); + } catch (InvalidArgumentException | ParseException | SipException e) { + log.error("[命令发送失败] 设备预置位查询应答处理: {}", e.getMessage()); + } + return; + } + int num = Integer.parseInt(presetListNumElement.attributeValue("Num")); + List presetQuerySipReqList = new ArrayList<>(); + if (num > 0) { + for (Iterator presetIterator = presetListNumElement.elementIterator(); presetIterator.hasNext(); ) { + Element itemListElement = presetIterator.next(); + Preset presetQuerySipReq = new Preset(); + for (Iterator itemListIterator = itemListElement.elementIterator(); itemListIterator.hasNext(); ) { + // 遍历item + Element itemOne = itemListIterator.next(); + String name = itemOne.getName(); + String textTrim = itemOne.getTextTrim(); + if ("PresetID".equalsIgnoreCase(name)) { + presetQuerySipReq.setPresetId(textTrim); + } else { + presetQuerySipReq.setPresetName(textTrim); + } + } + presetQuerySipReqList.add(presetQuerySipReq); + } + } + String sn = getText(element, "SN"); + addCatch(cmdType + "_" + sn, num, rootElement, presetQuerySipReqList); + try { + responseAck(request, Response.OK); + } catch (InvalidArgumentException | ParseException | SipException e) { + log.error("[命令发送失败] 设备预置位查询应答处理: {}", e.getMessage()); + } + } catch (DocumentException e) { + log.error("[解析xml]失败: ", e); + } + } + + private void addCatch(String key, int sumNum, Element rootElement, List presetQuerySipReqList) { + if (presetQuerySipReqList.size() == sumNum) { + responseMessageHandler.handMessageEvent(rootElement, presetQuerySipReqList); + if (mesageMap.containsKey(key)) { + MessageResponseTask messageResponseTask = mesageMap.get(key); + mesageMap.remove(key); + boolean remove = delayQueue.remove(messageResponseTask); + if (!remove) { + log.info("[移除预置位查询任务] 从延时队列内移除失败: {}", key); + } + } + }else { + if (mesageMap.containsKey(key)) { + MessageResponseTask messageResponseTask = mesageMap.get(key); + List data = messageResponseTask.getData(); + data.addAll(presetQuerySipReqList); + if (data.size() == sumNum) { + responseMessageHandler.handMessageEvent(rootElement, data); + mesageMap.remove(key); + boolean remove = delayQueue.remove(messageResponseTask); + if (!remove) { + log.info("[移除预置位查询任务] 从延时队列内移除失败: {}", key); + } + return; + } + messageResponseTask.setDelayTime(System.currentTimeMillis() + 1000); + }else { + MessageResponseTask messageResponseTask = new MessageResponseTask<>(); + messageResponseTask.setElement(rootElement); + messageResponseTask.setData(presetQuerySipReqList); + messageResponseTask.setDelayTime(System.currentTimeMillis() + 1000); + messageResponseTask.setKey(key); + mesageMap.put(key, messageResponseTask); + delayQueue.offer(messageResponseTask); + } + } + } + + // 处理过期的缓存 + @Scheduled(fixedDelay = 500, timeUnit = TimeUnit.MILLISECONDS) + public void expirationCheck(){ + while (!delayQueue.isEmpty()) { + MessageResponseTask take = null; + try { + take = delayQueue.take(); + try { + responseMessageHandler.handMessageEvent(take.getElement(), take.getData()); + mesageMap.remove(take.getKey()); + }catch (Exception e) { + log.error("[预置位查询到期] {} 到期处理时出现异常", take.getKey()); + } + } catch (InterruptedException e) { + log.error("[设备订阅任务] ", e); + } + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element rootElement) { + + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/RecordInfoResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/RecordInfoResponseMessageHandler.java new file mode 100755 index 0000000..0ab97e8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/RecordInfoResponseMessageHandler.java @@ -0,0 +1,191 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.RecordInfo; +import com.genersoft.iot.vmp.gb28181.bean.RecordItem; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEndEvent; +import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEvent; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.UJson; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +/** + * @author lin + */ +@Slf4j +@Component +public class RecordInfoResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "RecordInfo"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + private RedisTemplate redisTemplate; + + private Long recordInfoTtl = 1800L; + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + try { + // 回复200 OK + responseAck((SIPRequest) evt.getRequest(), Response.OK); + }catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 国标录像: {}", e.getMessage()); + } + try { + String sn = getText(rootElement, "SN"); + String channelId = getText(rootElement, "DeviceID"); + RecordInfo recordInfo = new RecordInfo(); + recordInfo.setChannelId(channelId); + recordInfo.setDeviceId(device.getDeviceId()); + recordInfo.setSn(sn); + recordInfo.setName(getText(rootElement, "Name")); + String sumNumStr = getText(rootElement, "SumNum"); + int sumNum = 0; + if (!ObjectUtils.isEmpty(sumNumStr)) { + sumNum = Integer.parseInt(sumNumStr); + } + recordInfo.setSumNum(sumNum); + Element recordListElement = rootElement.element("RecordList"); + if (recordListElement == null || sumNum == 0) { + log.info("无录像数据"); + recordInfo.setCount(sumNum); + recordInfoEventPush(recordInfo); + recordInfoEndEventPush(recordInfo); + } else { + Iterator recordListIterator = recordListElement.elementIterator(); + if (recordListIterator != null) { + List recordList = new ArrayList<>(); + // 遍历DeviceList + while (recordListIterator.hasNext()) { + Element itemRecord = recordListIterator.next(); + Element recordElement = itemRecord.element("DeviceID"); + if (recordElement == null) { + log.info("记录为空,下一个..."); + continue; + } + RecordItem record = new RecordItem(); + record.setDeviceId(getText(itemRecord, "DeviceID")); + record.setName(getText(itemRecord, "Name")); + record.setFilePath(getText(itemRecord, "FilePath")); + record.setFileSize(getText(itemRecord, "FileSize")); + record.setAddress(getText(itemRecord, "Address")); + + String startTimeStr = getText(itemRecord, "StartTime"); + record.setStartTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(startTimeStr)); + + String endTimeStr = getText(itemRecord, "EndTime"); + record.setEndTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(endTimeStr)); + + record.setSecrecy(itemRecord.element("Secrecy") == null ? 0 + : Integer.parseInt(getText(itemRecord, "Secrecy"))); + record.setType(getText(itemRecord, "Type")); + record.setRecorderId(getText(itemRecord, "RecorderID")); + recordList.add(record); + } + Map map = recordList.stream() + .filter(record -> record.getDeviceId() != null) + .collect(Collectors.toMap(record -> record.getStartTime()+ record.getEndTime(), UJson::writeJson)); + // 获取任务结果数据 + String resKey = VideoManagerConstants.REDIS_RECORD_INFO_RES_PRE + channelId + sn; + redisTemplate.opsForHash().putAll(resKey, map); + redisTemplate.expire(resKey, recordInfoTtl, TimeUnit.SECONDS); + String resCountKey = VideoManagerConstants.REDIS_RECORD_INFO_RES_COUNT_PRE + channelId + sn; + Long incr = redisTemplate.opsForValue().increment(resCountKey, map.size()); + if (incr == null) { + incr = 0L; + } + redisTemplate.expire(resCountKey, recordInfoTtl, TimeUnit.SECONDS); + recordInfo.setRecordList(recordList); + recordInfo.setCount(Math.toIntExact(incr)); + recordInfoEventPush(recordInfo); + if (incr < sumNum) { + return; + } + // 已接收完成 + List resList = redisTemplate.opsForHash().entries(resKey).values().stream().map(e -> UJson.readJson(e.toString(), RecordItem.class)).collect(Collectors.toList()); + if (resList.size() < sumNum) { + return; + } + recordInfo.setRecordList(resList); + recordInfoEndEventPush(recordInfo); + } + } + } catch (Exception e) { + log.error("[国标录像] 发现未处理的异常, \r\n{}", evt.getRequest()); + log.error("[国标录像] 异常内容: ", e); + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + + } + + private void recordInfoEventPush(RecordInfo recordInfo) { + if (recordInfo == null) { + return; + } + if(recordInfo.getRecordList() != null) { + Collections.sort(recordInfo.getRecordList()); + }else{ + recordInfo.setRecordList(new ArrayList<>()); + } + RecordInfoEvent outEvent = new RecordInfoEvent(this); + outEvent.setRecordInfo(recordInfo); + applicationEventPublisher.publishEvent(outEvent); + } + + private void recordInfoEndEventPush(RecordInfo recordInfo) { + if (recordInfo == null) { + return; + } + if(recordInfo.getRecordList() != null) { + Collections.sort(recordInfo.getRecordList()); + }else{ + recordInfo.setRecordList(new ArrayList<>()); + } + RecordInfoEndEvent outEvent = new RecordInfoEndEvent(this); + outEvent.setRecordInfo(recordInfo); + applicationEventPublisher.publishEvent(outEvent); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/ISIPResponseProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/ISIPResponseProcessor.java new file mode 100755 index 0000000..ffa93f2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/ISIPResponseProcessor.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.response; + +import org.springframework.scheduling.annotation.Async; + +import javax.sip.ResponseEvent; + +/** + * @description:处理接收IPCamera发来的SIP协议响应消息 + * @author: swwheihei + * @date: 2020年5月3日 下午4:42:22 + */ +public interface ISIPResponseProcessor { + + + void process(ResponseEvent evt); + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/SIPResponseProcessorAbstract.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/SIPResponseProcessorAbstract.java new file mode 100755 index 0000000..9f2a10b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/SIPResponseProcessorAbstract.java @@ -0,0 +1,8 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.response; + +import org.springframework.beans.factory.InitializingBean; + +public abstract class SIPResponseProcessorAbstract implements InitializingBean, ISIPResponseProcessor { + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/ByeResponseProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/ByeResponseProcessor.java new file mode 100755 index 0000000..17c928d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/ByeResponseProcessor.java @@ -0,0 +1,39 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.response.impl; + +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.event.response.SIPResponseProcessorAbstract; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.ResponseEvent; + +/** + * @description: BYE请求响应器 + * @author: swwheihei + * @date: 2020年5月3日 下午5:32:05 + */ +@Component +public class ByeResponseProcessor extends SIPResponseProcessorAbstract { + + private final String method = "BYE"; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addResponseProcessor(method, this); + } + /** + * 处理BYE响应 + * + * @param evt + */ + @Override + public void process(ResponseEvent evt) { + // TODO Auto-generated method stub + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/CancelResponseProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/CancelResponseProcessor.java new file mode 100755 index 0000000..a5f80d8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/CancelResponseProcessor.java @@ -0,0 +1,39 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.response.impl; + +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.event.response.SIPResponseProcessorAbstract; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.ResponseEvent; + +/** + * @description: CANCEL响应处理器 + * @author: panlinlin + * @date: 2021年11月5日 16:35 + */ +@Component +public class CancelResponseProcessor extends SIPResponseProcessorAbstract { + + private final String method = "CANCEL"; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addResponseProcessor(method, this); + } + /** + * 处理CANCEL响应 + * + * @param evt + */ + @Override + public void process(ResponseEvent evt) { + // TODO Auto-generated method stub + + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/InviteResponseProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/InviteResponseProcessor.java new file mode 100755 index 0000000..82c5375 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/InviteResponseProcessor.java @@ -0,0 +1,90 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.response.impl; + +import com.genersoft.iot.vmp.gb28181.bean.Gb28181Sdp; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.SIPRequestHeaderProvider; +import com.genersoft.iot.vmp.gb28181.transmit.event.response.SIPResponseProcessorAbstract; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.utils.IpPortUtil; +import gov.nist.javax.sip.ResponseEventExt; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sdp.SdpParseException; +import javax.sdp.SessionDescription; +import javax.sip.InvalidArgumentException; +import javax.sip.ResponseEvent; +import javax.sip.SipException; +import javax.sip.SipFactory; +import javax.sip.address.SipURI; +import javax.sip.message.Request; +import javax.sip.message.Response; +import java.text.ParseException; + + +/** + * @description: 处理INVITE响应 + * @author: panlinlin + * @date: 2021年11月5日 16:40 + */ +@Slf4j +@Component +public class InviteResponseProcessor extends SIPResponseProcessorAbstract { + + private final String method = "INVITE"; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private SIPSender sipSender; + + @Autowired + private SIPRequestHeaderProvider headerProvider; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addResponseProcessor(method, this); + } + + + + /** + * 处理invite响应 + * + * @param evt 响应消息 + * @throws ParseException + */ + @Override + public void process(ResponseEvent evt ){ + log.debug("接收到消息:" + evt.getResponse()); + try { + SIPResponse response = (SIPResponse)evt.getResponse(); + int statusCode = response.getStatusCode(); + // trying不会回复 + if (statusCode == Response.TRYING) { + } + // 成功响应 + // 下发ack + if (statusCode == Response.OK) { + ResponseEventExt event = (ResponseEventExt)evt; + + String contentString = new String(response.getRawContent()); + Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString); + SessionDescription sdp = gb28181Sdp.getBaseSdb(); + SipURI requestUri = SipFactory.getInstance().createAddressFactory().createSipURI(sdp.getOrigin().getUsername(), IpPortUtil.concatenateIpAndPort(event.getRemoteIpAddress(), String.valueOf(event.getRemotePort()))); + Request reqAck = headerProvider.createAckRequest(response.getLocalAddress().getHostAddress(), requestUri, response); + + log.info("[回复ack] {}-> {}:{} ", sdp.getOrigin().getUsername(), event.getRemoteIpAddress(), event.getRemotePort()); + sipSender.transmitRequest( response.getLocalAddress().getHostAddress(), reqAck); + } + } catch (InvalidArgumentException | ParseException | SipException | SdpParseException e) { + log.info("[点播回复ACK],异常:", e ); + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/RegisterResponseProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/RegisterResponseProcessor.java new file mode 100755 index 0000000..1f73370 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/RegisterResponseProcessor.java @@ -0,0 +1,99 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.response.impl; + +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.event.sip.SipEvent; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.event.response.SIPResponseProcessorAbstract; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.ResponseEvent; +import javax.sip.SipException; +import javax.sip.header.WWWAuthenticateHeader; +import javax.sip.message.Response; +import java.text.ParseException; + +/** + * @description:Register响应处理器 + * @author: swwheihei + * @date: 2020年5月3日 下午5:32:23 + */ +@Slf4j +@Component +public class RegisterResponseProcessor extends SIPResponseProcessorAbstract { + + private final String method = "REGISTER"; + + @Autowired + private ISIPCommanderForPlatform sipCommanderForPlatform; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private IPlatformService platformService; + + @Autowired + private SipSubscribe sipSubscribe; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addResponseProcessor(method, this); + } + + /** + * 处理Register响应 + * + * @param evt 事件 + */ + @Override + public void process(ResponseEvent evt) { + SIPResponse response = (SIPResponse)evt.getResponse(); + String callId = response.getCallIdHeader().getCallId(); + long seqNumber = response.getCSeqHeader().getSeqNumber(); + SipEvent subscribe = sipSubscribe.getSubscribe(callId + seqNumber); + if (subscribe == null || subscribe.getSipTransactionInfo() == null || subscribe.getSipTransactionInfo().getUser() == null) { + return; + } + + String action = subscribe.getSipTransactionInfo().getExpires() > 0 ? "注册" : "注销"; + String platFormServerGbId = subscribe.getSipTransactionInfo().getUser(); + + log.info("[国标级联]{} {}响应 {} ", action, response.getStatusCode(), platFormServerGbId); + Platform platform = platformService.queryPlatformByServerGBId(platFormServerGbId); + if (platform == null) { + log.warn("[国标级联]收到 来自{}的 {} 回复 {}, 但是平台信息未查询到!!!", platFormServerGbId, action, response.getStatusCode()); + return; + } + + if (response.getStatusCode() == Response.UNAUTHORIZED) { + WWWAuthenticateHeader www = (WWWAuthenticateHeader)response.getHeader(WWWAuthenticateHeader.NAME); + SipTransactionInfo sipTransactionInfo = new SipTransactionInfo(response); + try { + sipCommanderForPlatform.register(platform, sipTransactionInfo, www, null, null, subscribe.getSipTransactionInfo().getExpires() > 0); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 再次注册: {}", e.getMessage()); + } + }else if (response.getStatusCode() == Response.OK){ + if (subscribe.getSipTransactionInfo().getExpires() > 0) { + SipTransactionInfo sipTransactionInfo = new SipTransactionInfo(response); + platformService.online(platform, sipTransactionInfo); + }else { + platformService.offline(platform); + } + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/Coordtransform.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/Coordtransform.java new file mode 100644 index 0000000..5c12ff6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/Coordtransform.java @@ -0,0 +1,126 @@ +package com.genersoft.iot.vmp.gb28181.utils; + +/** + * 坐标转换 + * 一个提供了百度坐标(BD09)、国测局坐标(火星坐标,GCJ02)、和WGS84坐标系之间的转换的工具类 + * 参考https://github.com/wandergis/coordtransform 写的Java版本 + * @author Xinconan + * @date 2016-03-18 + * @url https://github.com/xinconan/coordtransform + */ +public class Coordtransform { + + private static double x_PI = 3.14159265358979324 * 3000.0 / 180.0; + private static double PI = 3.1415926535897932384626; + private static double a = 6378245.0; + private static double ee = 0.00669342162296594323; + + /** + * 百度坐标系 (BD-09) 与 火星坐标系 (GCJ-02)的转换 + * 即 百度 转 谷歌、高德 + * @param bd_lon + * @param bd_lat + * @return Double[lon,lat] + */ + public static Double[] BD09ToGCJ02(Double bd_lon,Double bd_lat){ + double x = bd_lon - 0.0065; + double y = bd_lat - 0.006; + double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_PI); + double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_PI); + Double[] arr = new Double[2]; + arr[0] = z * Math.cos(theta); + arr[1] = z * Math.sin(theta); + return arr; + } + + /** + * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换 + * 即谷歌、高德 转 百度 + * @param gcj_lon + * @param gcj_lat + * @return Double[lon,lat] + */ + public static Double[] GCJ02ToBD09(Double gcj_lon,Double gcj_lat){ + double z = Math.sqrt(gcj_lon * gcj_lon + gcj_lat * gcj_lat) + 0.00002 * Math.sin(gcj_lat * x_PI); + double theta = Math.atan2(gcj_lat, gcj_lon) + 0.000003 * Math.cos(gcj_lon * x_PI); + Double[] arr = new Double[2]; + arr[0] = z * Math.cos(theta) + 0.0065; + arr[1] = z * Math.sin(theta) + 0.006; + return arr; + } + + /** + * WGS84转GCJ02 + * @param wgs_lon + * @param wgs_lat + * @return Double[lon,lat] + */ + public static Double[] WGS84ToGCJ02(Double wgs_lon,Double wgs_lat){ + if(outOfChina(wgs_lon, wgs_lat)){ + return new Double[]{wgs_lon,wgs_lat}; + } + double dlat = transformlat(wgs_lon - 105.0, wgs_lat - 35.0); + double dlng = transformlng(wgs_lon - 105.0, wgs_lat - 35.0); + double radlat = wgs_lat / 180.0 * PI; + double magic = Math.sin(radlat); + magic = 1 - ee * magic * magic; + double sqrtmagic = Math.sqrt(magic); + dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI); + dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI); + Double[] arr = new Double[2]; + arr[0] = wgs_lon + dlng; + arr[1] = wgs_lat + dlat; + return arr; + } + + /** + * GCJ02转WGS84 + * @param gcj_lon + * @param gcj_lat + * @return Double[lon,lat] + */ + public static Double[] GCJ02ToWGS84(Double gcj_lon,Double gcj_lat){ + if(outOfChina(gcj_lon, gcj_lat)){ + return new Double[]{gcj_lon,gcj_lat}; + } + double dlat = transformlat(gcj_lon - 105.0, gcj_lat - 35.0); + double dlng = transformlng(gcj_lon - 105.0, gcj_lat - 35.0); + double radlat = gcj_lat / 180.0 * PI; + double magic = Math.sin(radlat); + magic = 1 - ee * magic * magic; + double sqrtmagic = Math.sqrt(magic); + dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI); + dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI); + double mglat = gcj_lat + dlat; + double mglng = gcj_lon + dlng; + return new Double[]{gcj_lon * 2 - mglng, gcj_lat * 2 - mglat}; + } + + private static Double transformlat(double lng, double lat) { + double ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng)); + ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0; + ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0; + return ret; + } + + private static Double transformlng(double lng,double lat) { + double ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng)); + ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0; + ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0; + return ret; + } + + /** + * outOfChina + * @描述: 判断是否在国内,不在国内则不做偏移 + * @param lng + * @param lat + * @return {boolean} + */ + private static boolean outOfChina(Double lng,Double lat) { + return (lng < 72.004 || lng > 137.8347) || ((lat < 0.8293 || lat > 55.8271) || false); + }; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/MessageElement.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/MessageElement.java new file mode 100644 index 0000000..f94237c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/MessageElement.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.gb28181.utils; + +import java.lang.annotation.*; + +/** + * @author gaofuwang + * @version 1.0 + * @date 2022/6/28 14:58 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MessageElement { + String value(); + + String subVal() default ""; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/MessageElementForCatalog.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/MessageElementForCatalog.java new file mode 100644 index 0000000..7abd7b3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/MessageElementForCatalog.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.gb28181.utils; + +import java.lang.annotation.*; + +/** + * @author gaofuwang + * @version 1.0 + * @date 2022/6/28 14:58 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MessageElementForCatalog { + String[] value(); + + String subVal() default ""; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/NumericUtil.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/NumericUtil.java new file mode 100644 index 0000000..f2b84b2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/NumericUtil.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.gb28181.utils; + +/** + * 数值格式判断和处理 + * @author lawrencehj + * @date 2021年1月27日 + */ +public class NumericUtil { + /** + * 判断是否Double格式 + * @param str + * @return true/false + */ + public static boolean isDouble(String str) { + try { + Double num2 = Double.valueOf(str); +// logger.debug(num2 + " is a valid numeric string!"); + return true; + } catch (Exception e) { +// logger.debug(str + " is an invalid numeric string!"); + return false; + } + } + + /** + * 判断是否Double格式 + * @param str + * @return true/false + */ + public static boolean isInteger(String str) { + try { + int num2 = Integer.valueOf(str); +// logger.debug(num2 + " is an integer!"); + return true; + } catch (Exception e) { +// logger.debug(str + " is not an integer!"); + return false; + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java new file mode 100644 index 0000000..90a7e2a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java @@ -0,0 +1,267 @@ +package com.genersoft.iot.vmp.gb28181.utils; + +import com.genersoft.iot.vmp.gb28181.bean.Gb28181Sdp; +import com.genersoft.iot.vmp.common.RemoteAddressInfo; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.GitUtil; +import gov.nist.javax.sip.address.AddressImpl; +import gov.nist.javax.sip.address.SipUri; +import gov.nist.javax.sip.header.Subject; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.util.ObjectUtils; + +import javax.sdp.SdpFactory; +import javax.sdp.SdpParseException; +import javax.sdp.SessionDescription; +import javax.sip.PeerUnavailableException; +import javax.sip.SipFactory; +import javax.sip.header.FromHeader; +import javax.sip.header.SubjectHeader; +import javax.sip.header.UserAgentHeader; +import javax.sip.message.Request; +import java.text.ParseException; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * @author panlinlin + * @version 1.0.0 + * @description JAIN SIP的工具类 + * @createTime 2021年09月27日 15:12:00 + */ +@Slf4j +public class SipUtils { + + public static String getUserIdFromFromHeader(Request request) { + FromHeader fromHeader = (FromHeader)request.getHeader(FromHeader.NAME); + return getUserIdFromFromHeader(fromHeader); + } + /** + * 从subject读取channelId + * */ + public static String[] getChannelIdFromRequest(Request request) { + SubjectHeader subject = (Subject)request.getHeader("subject"); + if (subject == null) { + // 如果缺失subject + return null; + } + String[] result = new String[2]; + String subjectStr = subject.getSubject(); + if (subjectStr.indexOf(",") > 0) { + String[] subjectSplit = subjectStr.split(","); + result[0] = subjectSplit[0].split(":")[0]; + result[1] = subjectSplit[1].split(":")[0]; + }else { + result[0] = subjectStr.split(":")[0]; + } + return result; + } + + public static String getUserIdFromFromHeader(FromHeader fromHeader) { + AddressImpl address = (AddressImpl)fromHeader.getAddress(); + SipUri uri = (SipUri) address.getURI(); + return uri.getUser(); + } + + public static String getNewViaTag() { + return "z9hG4bK" + RandomStringUtils.randomNumeric(10); + } + + public static UserAgentHeader createUserAgentHeader(GitUtil gitUtil) throws PeerUnavailableException, ParseException { + List agentParam = new ArrayList<>(); + agentParam.add("WVP-Pro "); + if (gitUtil != null ) { + if (!ObjectUtils.isEmpty(gitUtil.getBuildVersion())) { + agentParam.add("v"); + agentParam.add(gitUtil.getBuildVersion() + "."); + } + if (!ObjectUtils.isEmpty(gitUtil.getCommitTime())) { + agentParam.add(gitUtil.getCommitTime()); + } + } + return SipFactory.getInstance().createHeaderFactory().createUserAgentHeader(agentParam); + } + + public static String getNewFromTag(){ + return UUID.randomUUID().toString().replace("-", ""); + +// return getNewTag(); + } + + public static String getNewTag(){ + return String.valueOf(System.currentTimeMillis()); + } + + + /** + * 云台指令码计算 + * + * @param leftRight 镜头左移右移 0:停止 1:左移 2:右移 + * @param upDown 镜头上移下移 0:停止 1:上移 2:下移 + * @param inOut 镜头放大缩小 0:停止 1:缩小 2:放大 + * @param moveSpeed 镜头移动速度 默认 0XFF (0-255) + * @param zoomSpeed 镜头缩放速度 默认 0X1 (0-255) + */ + public static String cmdString(int leftRight, int upDown, int inOut, int moveSpeed, int zoomSpeed) { + int cmdCode = 0; + if (leftRight == 2) { + cmdCode|=0x01; // 右移 + } else if(leftRight == 1) { + cmdCode|=0x02; // 左移 + } + if (upDown == 2) { + cmdCode|=0x04; // 下移 + } else if(upDown == 1) { + cmdCode|=0x08; // 上移 + } + if (inOut == 2) { + cmdCode |= 0x10; // 放大 + } else if(inOut == 1) { + cmdCode |= 0x20; // 缩小 + } + StringBuilder builder = new StringBuilder("A50F01"); + String strTmp; + strTmp = String.format("%02X", cmdCode); + builder.append(strTmp, 0, 2); + strTmp = String.format("%02X", moveSpeed); + builder.append(strTmp, 0, 2); + builder.append(strTmp, 0, 2); + + //优化zoom低倍速下的变倍速率 + if ((zoomSpeed > 0) && (zoomSpeed <16)) + { + zoomSpeed = 16; + } + strTmp = String.format("%X", zoomSpeed); + builder.append(strTmp, 0, 1).append("0"); + //计算校验码 + int checkCode = (0XA5 + 0X0F + 0X01 + cmdCode + moveSpeed + moveSpeed + (zoomSpeed /*<< 4*/ & 0XF0)) % 0X100; + strTmp = String.format("%02X", checkCode); + builder.append(strTmp, 0, 2); + return builder.toString(); + } + + public static String getNewCallId() { + return (int) Math.floor(Math.random() * 1000000000) + ""; + } + + public static int getTypeCodeFromGbCode(String deviceId) { + if (ObjectUtils.isEmpty(deviceId)) { + return 0; + } + return Integer.parseInt(deviceId.substring(10, 13)); + } + + /** + * 判断是否是前端外围设备 + * @param deviceId + * @return + */ + public static boolean isFrontEnd(String deviceId) { + int typeCodeFromGbCode = getTypeCodeFromGbCode(deviceId); + return typeCodeFromGbCode > 130 && typeCodeFromGbCode < 199; + } + /** + * 从请求中获取设备ip地址和端口号 + * @param request 请求 + * @param sipUseSourceIpAsRemoteAddress false 从via中获取地址, true 直接获取远程地址 + * @return 地址信息 + */ + public static RemoteAddressInfo getRemoteAddressFromRequest(SIPRequest request, boolean sipUseSourceIpAsRemoteAddress) { + + String remoteAddress; + int remotePort; + if (sipUseSourceIpAsRemoteAddress) { + remoteAddress = request.getPeerPacketSourceAddress().getHostAddress(); + remotePort = request.getPeerPacketSourcePort(); + + }else { + // 判断RPort是否改变,改变则说明路由nat信息变化,修改设备信息 + // 获取到通信地址等信息 + remoteAddress = request.getTopmostViaHeader().getReceived(); + remotePort = request.getTopmostViaHeader().getRPort(); + // 解析本地地址替代 + if (ObjectUtils.isEmpty(remoteAddress) || remotePort == -1) { + if (request.getPeerPacketSourceAddress() != null) { + remoteAddress = request.getPeerPacketSourceAddress().getHostAddress(); + }else { + remoteAddress = request.getRemoteAddress().getHostAddress(); + } + if( request.getPeerPacketSourcePort() > 0) { + remotePort = request.getPeerPacketSourcePort(); + }else { + remotePort = request.getRemotePort(); + } + } + } + + return new RemoteAddressInfo(remoteAddress, remotePort); + } + + public static Gb28181Sdp parseSDP(String sdpStr) throws SdpParseException { + + // jainSip不支持y= f=字段, 移除以解析。 + int ssrcIndex = sdpStr.indexOf("y="); + int mediaDescriptionIndex = sdpStr.indexOf("f="); + // 检查是否有y字段 + SessionDescription sdp; + String ssrc = null; + String mediaDescription = null; + if (mediaDescriptionIndex == 0 && ssrcIndex == 0) { + sdp = SdpFactory.getInstance().createSessionDescription(sdpStr); + }else { + String lines[] = sdpStr.split("\\r?\\n"); + StringBuilder sdpBuffer = new StringBuilder(); + for (String line : lines) { + if (line.trim().startsWith("y=")) { + ssrc = line.substring(2); + }else if (line.trim().startsWith("f=")) { + mediaDescription = line.substring(2); + }else { + sdpBuffer.append(line.trim()).append("\r\n"); + } + } + sdp = SdpFactory.getInstance().createSessionDescription(sdpBuffer.toString()); + } + return Gb28181Sdp.getInstance(sdp, ssrc, mediaDescription); + } + + public static String getSsrcFromSdp(String sdpStr) { + + // jainSip不支持y= f=字段, 移除以解析。 + int ssrcIndex = sdpStr.indexOf("y="); + if (ssrcIndex == 0) { + return null; + } + String lines[] = sdpStr.split("\\r?\\n"); + for (String line : lines) { + if (line.trim().startsWith("y=")) { + return line.substring(2); + } + } + return null; + } + + public static String parseTime(String timeStr) { + if (ObjectUtils.isEmpty(timeStr)){ + return null; + } + LocalDateTime localDateTime; + try { + localDateTime = LocalDateTime.parse(timeStr); + }catch (DateTimeParseException e) { + try { + localDateTime = LocalDateTime.parse(timeStr, DateUtil.formatterISO8601); + }catch (DateTimeParseException e2) { + log.error("[格式化时间] 无法格式化时间: {}", timeStr); + return null; + } + } + return localDateTime.format(DateUtil.formatter); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/VectorTileCatch.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/VectorTileCatch.java new file mode 100644 index 0000000..88c27de --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/VectorTileCatch.java @@ -0,0 +1,95 @@ +package com.genersoft.iot.vmp.gb28181.utils; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.VectorTileSource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ConcurrentReferenceHashMap; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class VectorTileCatch { + + private final Map vectorTileMap = new ConcurrentReferenceHashMap<>(); + private final DelayQueue delayQueue = new DelayQueue<>(); + + public void addVectorTile(String id, String key, byte[] content) { + VectorTileSource vectorTileSource = vectorTileMap.get(id); + if (vectorTileSource == null) { + vectorTileSource = new VectorTileSource(); + vectorTileSource.setId(id); + vectorTileMap.put(id, vectorTileSource); + delayQueue.offer(vectorTileSource); + } + + vectorTileSource.getVectorTileMap().put(key, content); + } + + public byte[] getVectorTile(String id, String key) { + if (!vectorTileMap.containsKey(id)) { + return null; + } + return vectorTileMap.get(id).getVectorTileMap().get(key); + } + + public void addSource(String id, List channelList) { + VectorTileSource vectorTileSource = vectorTileMap.get(id); + if (vectorTileSource == null) { + vectorTileSource = new VectorTileSource(); + vectorTileSource.setId(id); + vectorTileMap.put(id, vectorTileSource); + delayQueue.offer(vectorTileSource); + } + vectorTileMap.get(id).getChannelList().addAll(channelList); + } + + + public void remove(String id) { + VectorTileSource vectorTileSource = vectorTileMap.get(id); + if (vectorTileSource != null) { + delayQueue.remove(vectorTileSource); + } + vectorTileMap.remove(id); + } + + public List getChannelList(String id) { + if (!vectorTileMap.containsKey(id)) { + return null; + } + return vectorTileMap.get(id).getChannelList(); + } + + public void save(String id) { + if (!vectorTileMap.containsKey(id)) { + return; + } + VectorTileSource vectorTileSource = vectorTileMap.get(id); + if (vectorTileSource == null) { + return; + } + vectorTileMap.remove(id); + delayQueue.remove(vectorTileSource); + vectorTileMap.put("DEFAULT", vectorTileSource); + } + + + // 缓存数据过期检查 + @Scheduled(fixedDelay = 30, timeUnit = TimeUnit.MINUTES) + public void expirationCheck(){ + while (!delayQueue.isEmpty()) { + try { + VectorTileSource vectorTileSource = delayQueue.take(); + vectorTileMap.remove(vectorTileSource.getId()); + } catch (InterruptedException e) { + log.error("[清理过期的抽稀数据] ", e); + } + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/XmlUtil.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/XmlUtil.java new file mode 100644 index 0000000..4656670 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/XmlUtil.java @@ -0,0 +1,731 @@ +package com.genersoft.iot.vmp.gb28181.utils; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.math.NumberUtils; +import org.dom4j.Attribute; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +import javax.sip.RequestEvent; +import javax.sip.message.Request; +import java.io.ByteArrayInputStream; +import java.io.StringReader; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.*; + +/** + * 基于dom4j的工具包 + */ +@Slf4j +public class XmlUtil { + + /** + * 解析XML为Document对象 + */ + public static Element parseXml(String xml) { + Document document = null; + // + StringReader sr = new StringReader(xml); + SAXReader saxReader = new SAXReader(); + try { + document = saxReader.read(sr); + } catch (DocumentException e) { + log.error("解析失败", e); + } + return null == document ? null : document.getRootElement(); + } + + /** + * 获取element对象的text的值 + * + * @param em 节点的对象 + * @param tag 节点的tag + * @return 节点 + */ + public static String getText(Element em, String tag) { + if (null == em) { + return null; + } + Element e = em.element(tag); + // + return null == e ? null : e.getText().trim(); + } + + /** + * 获取element对象的text的值 + * + * @param em 节点的对象 + * @param tag 节点的tag + * @return 节点 + */ + public static Double getDouble(Element em, String tag) { + if (null == em) { + return null; + } + Element e = em.element(tag); + if (null == e) { + return null; + } + String text = e.getText().trim(); + if (ObjectUtils.isEmpty(text) || !NumberUtils.isParsable(text)) { + return null; + } + return Double.parseDouble(text); + } + + /** + * 获取element对象的text的值 + * + * @param em 节点的对象 + * @param tag 节点的tag + * @return 节点 + */ + public static Integer getInteger(Element em, String tag) { + if (null == em) { + return null; + } + Element e = em.element(tag); + if (null == e) { + return null; + } + String text = e.getText().trim(); + if (ObjectUtils.isEmpty(text) || !NumberUtils.isParsable(text)) { + return null; + } + return Integer.parseInt(text); + } + + /** + * 递归解析xml节点,适用于 多节点数据 + * + * @param node node + * @param nodeName nodeName + * @return List> + */ + public static List> listNodes(Element node, String nodeName) { + if (null == node) { + return null; + } + // 初始化返回 + List> listMap = new ArrayList>(); + // 首先获取当前节点的所有属性节点 + List list = node.attributes(); + + Map map = null; + // 遍历属性节点 + for (Attribute attribute : list) { + if (nodeName.equals(node.getName())) { + if (null == map) { + map = new HashMap(); + listMap.add(map); + } + // 取到的节点属性放到map中 + map.put(attribute.getName(), attribute.getValue()); + } + + } + // 遍历当前节点下的所有节点 ,nodeName 要解析的节点名称 + // 使用递归 + Iterator iterator = node.elementIterator(); + while (iterator.hasNext()) { + Element e = iterator.next(); + listMap.addAll(listNodes(e, nodeName)); + } + return listMap; + } + + /** + * xml转json + * + * @param element + * @param json + */ + public static void node2Json(Element element, JSONObject json) { + // 如果是属性 + for (Object o : element.attributes()) { + Attribute attr = (Attribute) o; + if (!ObjectUtils.isEmpty(attr.getValue())) { + json.put("@" + attr.getName(), attr.getValue()); + } + } + List chdEl = element.elements(); + if (chdEl.isEmpty() && !ObjectUtils.isEmpty(element.getText())) {// 如果没有子元素,只有一个值 + json.put(element.getName(), element.getText()); + } + + for (Element e : chdEl) { // 有子元素 + if (!e.elements().isEmpty()) { // 子元素也有子元素 + JSONObject chdjson = new JSONObject(); + node2Json(e, chdjson); + Object o = json.get(e.getName()); + if (o != null) { + JSONArray jsona = null; + if (o instanceof JSONObject) { // 如果此元素已存在,则转为jsonArray + JSONObject jsono = (JSONObject) o; + json.remove(e.getName()); + jsona = new JSONArray(); + jsona.add(jsono); + jsona.add(chdjson); + } + if (o instanceof JSONArray) { + jsona = (JSONArray) o; + jsona.add(chdjson); + } + json.put(e.getName(), jsona); + } else { + if (!chdjson.isEmpty()) { + json.put(e.getName(), chdjson); + } + } + } else { // 子元素没有子元素 + for (Object o : element.attributes()) { + Attribute attr = (Attribute) o; + if (!ObjectUtils.isEmpty(attr.getValue())) { + json.put("@" + attr.getName(), attr.getValue()); + } + } + if (!e.getText().isEmpty()) { + json.put(e.getName(), e.getText()); + } + } + } + } + public static Element getRootElement(RequestEvent evt) throws DocumentException { + + return getRootElement(evt, "gb2312"); + } + + public static Element getRootElement(RequestEvent evt, String charset) throws DocumentException { + Request request = evt.getRequest(); + return getRootElement(request.getRawContent(), charset); + } + + public static Element getRootElement(byte[] content, String charset) throws DocumentException { + if (charset == null) { + charset = "gb2312"; + } + SAXReader reader = new SAXReader(); + reader.setEncoding(charset); + Document xml = reader.read(new ByteArrayInputStream(content)); + return xml.getRootElement(); + } + + private enum ChannelType{ + CivilCode, BusinessGroup,VirtualOrganization,Other + } + +// public static DeviceChannel channelContentHandler(Element itemDevice, Device device, String event){ +// DeviceChannel deviceChannel = new DeviceChannel(); +// deviceChannel.setDeviceId(device.getDeviceId()); +// Element channdelIdElement = itemDevice.element("DeviceID"); +// if (channdelIdElement == null) { +// logger.warn("解析Catalog消息时发现缺少 DeviceID"); +// return null; +// } +// String channelId = channdelIdElement.getTextTrim(); +// if (ObjectUtils.isEmpty(channelId)) { +// logger.warn("解析Catalog消息时发现缺少 DeviceID"); +// return null; +// } +// deviceChannel.setDeviceId(channelId); +// if (event != null && !event.equals(CatalogEvent.ADD) && !event.equals(CatalogEvent.UPDATE)) { +// // 除了ADD和update情况下需要识别全部内容, +// return deviceChannel; +// } +// Element nameElement = itemDevice.element("Name"); +// // 当通道名称为空时,设置通道名称为通道编码,避免级联时因通道名称为空导致上级接收通道失败 +// if (nameElement != null && StringUtils.isNotBlank(nameElement.getText())) { +// deviceChannel.setName(nameElement.getText()); +// } else { +// deviceChannel.setName(channelId); +// } +// if(channelId.length() <= 8) { +// deviceChannel.setHasAudio(false); +// CivilCodePo parentCode = CivilCodeUtil.INSTANCE.getParentCode(channelId); +// if (parentCode != null) { +// deviceChannel.setParentId(parentCode.getCode()); +// deviceChannel.setCivilCode(parentCode.getCode()); +// }else { +// logger.warn("[xml解析] 无法确定行政区划{}的上级行政区划", channelId); +// } +// deviceChannel.setStatus("ON"); +// return deviceChannel; +// }else { +// if(channelId.length() != 20) { +// logger.warn("[xml解析] 失败,编号不符合国标28181定义: {}", channelId); +// return null; +// } +// +// int code = Integer.parseInt(channelId.substring(10, 13)); +// if (code == 136 || code == 137 || code == 138) { +// deviceChannel.setHasAudio(true); +// }else { +// deviceChannel.setHasAudio(false); +// } +// // 设备厂商 +// String manufacturer = getText(itemDevice, "Manufacturer"); +// // 设备型号 +// String model = getText(itemDevice, "Model"); +// // 设备归属 +// String owner = getText(itemDevice, "Owner"); +// // 行政区域 +// String civilCode = getText(itemDevice, "CivilCode"); +// // 虚拟组织所属的业务分组ID,业务分组根据特定的业务需求制定,一个业务分组包含一组特定的虚拟组织 +// String businessGroupID = getText(itemDevice, "BusinessGroupID"); +// // 父设备/区域/系统ID +// String parentID = getText(itemDevice, "ParentID"); +// if (parentID != null && parentID.equalsIgnoreCase("null")) { +// parentID = null; +// } +// // 注册方式(必选)缺省为1;1:符合IETFRFC3261标准的认证注册模式;2:基于口令的双向认证注册模式;3:基于数字证书的双向认证注册模式 +// String registerWay = getText(itemDevice, "RegisterWay"); +// // 保密属性(必选)缺省为0;0:不涉密,1:涉密 +// String secrecy = getText(itemDevice, "Secrecy"); +// // 安装地址 +// String address = getText(itemDevice, "Address"); +// +// switch (code){ +// case 200: +// // 系统目录 +// if (!ObjectUtils.isEmpty(manufacturer)) { +// deviceChannel.setManufacture(manufacturer); +// } +// if (!ObjectUtils.isEmpty(model)) { +// deviceChannel.setModel(model); +// } +// if (!ObjectUtils.isEmpty(owner)) { +// deviceChannel.setOwner(owner); +// } +// if (!ObjectUtils.isEmpty(civilCode)) { +// deviceChannel.setCivilCode(civilCode); +// deviceChannel.setParentId(civilCode); +// }else { +// if (!ObjectUtils.isEmpty(parentID)) { +// deviceChannel.setParentId(parentID); +// } +// } +// if (!ObjectUtils.isEmpty(address)) { +// deviceChannel.setAddress(address); +// } +// deviceChannel.setStatus(true); +// if (!ObjectUtils.isEmpty(registerWay)) { +// try { +// deviceChannel.setRegisterWay(Integer.parseInt(registerWay)); +// }catch (NumberFormatException exception) { +// logger.warn("[xml解析] 从通道数据获取registerWay失败: {}", registerWay); +// } +// } +// if (!ObjectUtils.isEmpty(secrecy)) { +// deviceChannel.setSecrecy(secrecy); +// } +// return deviceChannel; +// case 215: +// // 业务分组 +// deviceChannel.setStatus(true); +// if (!ObjectUtils.isEmpty(parentID)) { +// if (!parentID.trim().equalsIgnoreCase(device.getDeviceId())) { +// deviceChannel.setParentId(parentID); +// } +// }else { +// logger.warn("[xml解析] 业务分组数据中缺少关键信息->ParentId"); +// if (!ObjectUtils.isEmpty(civilCode)) { +// deviceChannel.setCivilCode(civilCode); +// } +// } +// break; +// case 216: +// // 虚拟组织 +// deviceChannel.setStatus(true); +// if (!ObjectUtils.isEmpty(businessGroupID)) { +// deviceChannel.setBusinessGroupId(businessGroupID); +// } +// +// if (!ObjectUtils.isEmpty(parentID)) { +// if (parentID.contains("/")) { +// String[] parentIdArray = parentID.split("/"); +// parentID = parentIdArray[parentIdArray.length - 1]; +// } +// deviceChannel.setParentId(parentID); +// }else { +// if (!ObjectUtils.isEmpty(businessGroupID)) { +// deviceChannel.setParentId(businessGroupID); +// } +// } +// break; +// default: +// // 设备目录 +// if (!ObjectUtils.isEmpty(manufacturer)) { +// deviceChannel.setManufacture(manufacturer); +// } +// if (!ObjectUtils.isEmpty(model)) { +// deviceChannel.setModel(model); +// } +// if (!ObjectUtils.isEmpty(owner)) { +// deviceChannel.setOwner(owner); +// } +// if (!ObjectUtils.isEmpty(civilCode) +// && civilCode.length() <= 8 +// && NumberUtils.isParsable(civilCode) +// && civilCode.length()%2 == 0 +// ) { +// deviceChannel.setCivilCode(civilCode); +// } +// if (!ObjectUtils.isEmpty(businessGroupID)) { +// deviceChannel.setBusinessGroupId(businessGroupID); +// } +// +// // 警区 +// String block = getText(itemDevice, "Block"); +// if (!ObjectUtils.isEmpty(block)) { +// deviceChannel.setBlock(block); +// } +// if (!ObjectUtils.isEmpty(address)) { +// deviceChannel.setAddress(address); +// } +// +// if (!ObjectUtils.isEmpty(secrecy)) { +// deviceChannel.setSecrecy(secrecy); +// } +// +// // 当为设备时,是否有子设备(必选)1有,0没有 +// String parental = getText(itemDevice, "Parental"); +// if (!ObjectUtils.isEmpty(parental)) { +// try { +// // 由于海康会错误的发送65535作为这里的取值,所以这里除非是0否则认为是1 +// if (!ObjectUtils.isEmpty(parental) && parental.length() == 1 && Integer.parseInt(parental) == 0) { +// deviceChannel.setParental(0); +// }else { +// deviceChannel.setParental(1); +// } +// }catch (NumberFormatException e) { +// logger.warn("[xml解析] 从通道数据获取 parental失败: {}", parental); +// } +// } +// // 父设备/区域/系统ID +// +// if (!ObjectUtils.isEmpty(parentID) ) { +// if (parentID.contains("/")) { +// String[] parentIdArray = parentID.split("/"); +// deviceChannel.setParentId(parentIdArray[parentIdArray.length - 1]); +// }else { +// if (parentID.length()%2 == 0) { +// deviceChannel.setParentId(parentID); +// }else { +// logger.warn("[xml解析] 不规范的parentID:{}, 已舍弃", parentID); +// } +// } +// }else { +// if (!ObjectUtils.isEmpty(businessGroupID)) { +// deviceChannel.setParentId(businessGroupID); +// }else { +// if (!ObjectUtils.isEmpty(deviceChannel.getCivilCode())) { +// deviceChannel.setParentId(deviceChannel.getCivilCode()); +// } +// } +// } +// // 注册方式 +// if (!ObjectUtils.isEmpty(registerWay)) { +// try { +// int registerWayInt = Integer.parseInt(registerWay); +// deviceChannel.setRegisterWay(registerWayInt); +// }catch (NumberFormatException exception) { +// logger.warn("[xml解析] 从通道数据获取registerWay失败: {}", registerWay); +// deviceChannel.setRegisterWay(1); +// } +// }else { +// deviceChannel.setRegisterWay(1); +// } +// +// // 信令安全模式(可选)缺省为0; 0:不采用;2:S/MIME 签名方式;3:S/MIME加密签名同时采用方式;4:数字摘要方式 +// String safetyWay = getText(itemDevice, "SafetyWay"); +// if (!ObjectUtils.isEmpty(safetyWay)) { +// try { +// deviceChannel.setSafetyWay(Integer.parseInt(safetyWay)); +// }catch (NumberFormatException e) { +// logger.warn("[xml解析] 从通道数据获取 safetyWay失败: {}", safetyWay); +// } +// } +// +// // 证书序列号(有证书的设备必选) +// String certNum = getText(itemDevice, "CertNum"); +// if (!ObjectUtils.isEmpty(certNum)) { +// deviceChannel.setCertNum(certNum); +// } +// +// // 证书有效标识(有证书的设备必选)缺省为0;证书有效标识:0:无效 1:有效 +// String certifiable = getText(itemDevice, "Certifiable"); +// if (!ObjectUtils.isEmpty(certifiable)) { +// try { +// deviceChannel.setCertifiable(Integer.parseInt(certifiable)); +// }catch (NumberFormatException e) { +// logger.warn("[xml解析] 从通道数据获取 Certifiable失败: {}", certifiable); +// } +// } +// +// // 无效原因码(有证书且证书无效的设备必选) +// String errCode = getText(itemDevice, "ErrCode"); +// if (!ObjectUtils.isEmpty(errCode)) { +// try { +// deviceChannel.setErrCode(Integer.parseInt(errCode)); +// }catch (NumberFormatException e) { +// logger.warn("[xml解析] 从通道数据获取 ErrCode失败: {}", errCode); +// } +// } +// +// // 证书终止有效期(有证书的设备必选) +// String endTime = getText(itemDevice, "EndTime"); +// if (!ObjectUtils.isEmpty(endTime)) { +// deviceChannel.setEndTime(endTime); +// } +// +// +// // 设备/区域/系统IP地址 +// String ipAddress = getText(itemDevice, "IPAddress"); +// if (!ObjectUtils.isEmpty(ipAddress)) { +// deviceChannel.setIpAddress(ipAddress); +// } +// +// // 设备/区域/系统端口 +// String port = getText(itemDevice, "Port"); +// if (!ObjectUtils.isEmpty(port)) { +// try { +// deviceChannel.setPort(Integer.parseInt(port)); +// }catch (NumberFormatException e) { +// logger.warn("[xml解析] 从通道数据获取 Port失败: {}", port); +// } +// } +// +// // 设备口令 +// String password = getText(itemDevice, "Password"); +// if (!ObjectUtils.isEmpty(password)) { +// deviceChannel.setPassword(password); +// } +// +// +// // 设备状态 +// String status = getText(itemDevice, "Status"); +// if (status != null) { +// // ONLINE OFFLINE HIKVISION DS-7716N-E4 NVR的兼容性处理 +// if (status.equalsIgnoreCase("ON") || status.equalsIgnoreCase("On") || status.equalsIgnoreCase("ONLINE") || status.equalsIgnoreCase("OK")) { +// deviceChannel.setStatus(true); +// } +// if (status.equalsIgnoreCase("OFF") || status.equalsIgnoreCase("Off") || status.equalsIgnoreCase("OFFLINE")) { +// deviceChannel.setStatus(false); +// } +// }else { +// deviceChannel.setStatus(true); +// } +//// logger.info("状态字符串: {}", status); +//// logger.info("状态结果: {}", deviceChannel.isStatus()); +// // 经度 +// String longitude = getText(itemDevice, "Longitude"); +// if (NumericUtil.isDouble(longitude)) { +// deviceChannel.setLongitude(Double.parseDouble(longitude)); +// } else { +// deviceChannel.setLongitude(0.00); +// } +// +// // 纬度 +// String latitude = getText(itemDevice, "Latitude"); +// if (NumericUtil.isDouble(latitude)) { +// deviceChannel.setLatitude(Double.parseDouble(latitude)); +// } else { +// deviceChannel.setLatitude(0.00); +// } +// +// deviceChannel.setGpsTime(DateUtil.getNow()); +// +// // -摄像机类型扩展,标识摄像机类型:1-球机;2-半球;3-固定枪机;4-遥控枪机。当目录项为摄像机时可选 +// String ptzType = getText(itemDevice, "PTZType"); +// if (ObjectUtils.isEmpty(ptzType)) { +// //兼容INFO中的信息 +// Element info = itemDevice.element("Info"); +// String ptzTypeFromInfo = XmlUtil.getText(info, "PTZType"); +// if(!ObjectUtils.isEmpty(ptzTypeFromInfo)){ +// try { +// deviceChannel.setPtzType(Integer.parseInt(ptzTypeFromInfo)); +// }catch (NumberFormatException e){ +// logger.warn("[xml解析] 从通道数据info中获取PTZType失败: {}", ptzTypeFromInfo); +// } +// } +// } else { +// try { +// deviceChannel.setPtzType(Integer.parseInt(ptzType)); +// }catch (NumberFormatException e){ +// logger.warn("[xml解析] 从通道数据中获取PTZType失败: {}", ptzType); +// } +// } +// +// // TODO 摄像机位置类型扩展。 +// // 1-省际检查站、 +// // 2-党政机关、 +// // 3-车站码头、 +// // 4-中心广场、 +// // 5-体育场馆、 +// // 6-商业中心、 +// // 7-宗教场所、 +// // 8-校园周边、 +// // 9-治安复杂区域、 +// // 10-交通干线。 +// // String positionType = getText(itemDevice, "PositionType"); +// +// // TODO 摄像机安装位置室外、室内属性。1-室外、2-室内。 +// // String roomType = getText(itemDevice, "RoomType"); +// // TODO 摄像机用途属性 +// // String useType = getText(itemDevice, "UseType"); +// // TODO 摄像机补光属性。1-无补光、2-红外补光、3-白光补光 +// // String supplyLightType = getText(itemDevice, "SupplyLightType"); +// // TODO 摄像机监视方位属性。1-东、2-西、3-南、4-北、5-东南、6-东北、7-西南、8-西北。 +// // String directionType = getText(itemDevice, "DirectionType"); +// // TODO 摄像机支持的分辨率,可有多个分辨率值,各个取值间以“/”分隔。分辨率取值参见附录 F中SDPf字段规定 +// // String resolution = getText(itemDevice, "Resolution"); +// +// // TODO 下载倍速范围(可选),各可选参数以“/”分隔,如设备支持1,2,4倍速下载则应写为“1/2/4 +// // String downloadSpeed = getText(itemDevice, "DownloadSpeed"); +// // TODO 空域编码能力,取值0:不支持;1:1级增强(1个增强层);2:2级增强(2个增强层);3:3级增强(3个增强层) +// // String svcSpaceSupportMode = getText(itemDevice, "SVCSpaceSupportMode"); +// // TODO 时域编码能力,取值0:不支持;1:1级增强;2:2级增强;3:3级增强 +// // String svcTimeSupportMode = getText(itemDevice, "SVCTimeSupportMode"); +// +// +// deviceChannel.setSecrecy(secrecy); +// break; +// } +// } +// +// return deviceChannel; +// } + + /** + * 新增方法支持内部嵌套 + * + * @param element xmlElement + * @param clazz 结果类 + * @param 泛型 + * @return 结果对象 + * @throws NoSuchMethodException + * @throws InvocationTargetException + * @throws InstantiationException + * @throws IllegalAccessException + */ + public static T loadElement(Element element, Class clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + Field[] fields = clazz.getDeclaredFields(); + T t = clazz.getDeclaredConstructor().newInstance(); + for (Field field : fields) { + ReflectionUtils.makeAccessible(field); + MessageElement annotation = field.getAnnotation(MessageElement.class); + if (annotation == null) { + continue; + } + String value = annotation.value(); + String subVal = annotation.subVal(); + Element element1 = element.element(value); + if (element1 == null) { + continue; + } + if ("".equals(subVal)) { + // 无下级数据 + Object fieldVal = element1.isTextOnly() ? element1.getText() : loadElement(element1, field.getType()); + Object o = simpleTypeDeal(field.getType(), fieldVal); + ReflectionUtils.setField(field, t, o); + } else { + // 存在下级数据 + ArrayList list = new ArrayList<>(); + Type genericType = field.getGenericType(); + if (!(genericType instanceof ParameterizedType)) { + continue; + } + Class aClass = (Class) ((ParameterizedType) genericType).getActualTypeArguments()[0]; + for (Element element2 : element1.elements(subVal)) { + list.add(loadElement(element2, aClass)); + } + ReflectionUtils.setField(field, t, list); + } + } + return t; + } + + public static T elementDecode(Element element, Class clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + Field[] fields = clazz.getDeclaredFields(); + T t = clazz.getDeclaredConstructor().newInstance(); + for (Field field : fields) { + ReflectionUtils.makeAccessible(field); + MessageElementForCatalog annotation = field.getAnnotation(MessageElementForCatalog.class); + if (annotation == null) { + continue; + } + String[] values = annotation.value(); + for (String value : values) { + boolean subVal = value.contains("."); + if (!subVal) { + Element element1 = element.element(value); + if (element1 == null) { + continue; + } + // 无下级数据 + Object fieldVal = element1.isTextOnly() ? element1.getText() : loadElement(element1, field.getType()); + Object o = simpleTypeDeal(field.getType(), fieldVal); + ReflectionUtils.setField(field, t, o); + break; + } else { + String[] pathArray = value.split("\\."); + Element subElement = element; + for (String path : pathArray) { + subElement = subElement.element(path); + if (subElement == null) { + break; + } + } + if (subElement == null) { + continue; + } + Object fieldVal = subElement.isTextOnly() ? subElement.getText() : loadElement(subElement, field.getType()); + Object o = simpleTypeDeal(field.getType(), fieldVal); + ReflectionUtils.setField(field, t, o); + } + } + + } + return t; + } + + /** + * 简单类型处理 + * + * @param tClass + * @param val + * @return + */ + private static Object simpleTypeDeal(Class tClass, Object val) { + try { + if (val == null || val.toString().equalsIgnoreCase("null")) { + return null; + } + if (tClass.equals(String.class)) { + return val.toString(); + } + if (tClass.equals(Integer.class)) { + return Integer.valueOf(val.toString()); + } + if (tClass.equals(Double.class)) { + return Double.valueOf(val.toString()); + + } + if (tClass.equals(Long.class)) { + return Long.valueOf(val.toString()); + } + return val; + }catch (Exception e) { + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/annotation/MsgId.java b/src/main/java/com/genersoft/iot/vmp/jt1078/annotation/MsgId.java new file mode 100644 index 0000000..d5c2de4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/annotation/MsgId.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.jt1078.annotation; + +import java.lang.annotation.*; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:31 + * @email qingtaij@163.com + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MsgId { + String id(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAlarmSign.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAlarmSign.java new file mode 100644 index 0000000..82bb06e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAlarmSign.java @@ -0,0 +1,264 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.bean.config.JTDeviceSubConfig; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 报警标志 + */ +@Data +@Schema(description = "报警标志") +public class JTAlarmSign implements JTDeviceSubConfig { + + @Schema(description = "紧急报警,触动报警开关后触发") + private boolean urgent; + @Schema(description = "超速报警") + private boolean alarmSpeeding; + @Schema(description = "疲劳驾驶报警") + private boolean alarmTired; + @Schema(description = "危险驾驶行为报警") + private boolean alarmDangerous; + @Schema(description = "GNSS 模块发生故障报警") + private boolean alarmGnssFault; + @Schema(description = "GNSS 天线未接或被剪断报警") + private boolean alarmGnssBreak; + @Schema(description = "GNSS 天线短路报警") + private boolean alarmGnssShortCircuited; + @Schema(description = "终端主电源欠压报警") + private boolean alarmUnderVoltage; + @Schema(description = "终端主电源掉电报警") + private boolean alarmPowerOff; + @Schema(description = "终端 LCD或显示器故障报警") + private boolean alarmLCD; + @Schema(description = "TTS 模块故障报警") + private boolean alarmTtsFault; + @Schema(description = "摄像头故障报警") + private boolean alarmCameraFault; + @Schema(description = "道路运输证 IC卡模块故障报警") + private boolean alarmIcFault; + @Schema(description = "超速预警") + private boolean warningSpeeding; + @Schema(description = "疲劳驾驶预警") + private boolean warningTired; + @Schema(description = "违规行驶报警") + private boolean alarmWrong; + @Schema(description = "胎压预警") + private boolean warningTirePressure; + @Schema(description = "右转盲区异常报警") + private boolean alarmBlindZone; + @Schema(description = "当天累计驾驶超时报警") + private boolean alarmDrivingTimeout; + @Schema(description = "超时停车报警") + private boolean alarmParkingTimeout; + @Schema(description = "进出区域报警") + private boolean alarmRegion; + @Schema(description = "进出路线报警") + private boolean alarmRoute; + @Schema(description = "路段行驶时间不足/过长报警") + private boolean alarmTravelTime; + @Schema(description = "路线偏离报警") + private boolean alarmRouteDeviation; + @Schema(description = "车辆 VSS 故障") + private boolean alarmVSS; + @Schema(description = "车辆油量异常报警") + private boolean alarmOil; + @Schema(description = "车辆被盗报警(通过车辆防盗器)") + private boolean alarmStolen; + @Schema(description = "车辆非法点火报警") + private boolean alarmIllegalIgnition; + @Schema(description = "车辆非法位移报警") + private boolean alarmIllegalDisplacement; + @Schema(description = "碰撞侧翻报警") + private boolean alarmRollover; + @Schema(description = "侧翻预警") + private boolean warningRollover; + + public JTAlarmSign() { + } + + public JTAlarmSign(long alarmSignInt) { + if (alarmSignInt == 0) { + return; + } + // 解析alarm参数 + this.urgent = (alarmSignInt & 1) == 1; + this.alarmSpeeding = (alarmSignInt >>> 1 & 1) == 1; + this.alarmTired = (alarmSignInt >>> 2 & 1) == 1; + this.alarmDangerous = (alarmSignInt >>> 3 & 1) == 1; + this.alarmGnssFault = (alarmSignInt >>> 4 & 1) == 1; + this.alarmGnssBreak = (alarmSignInt >>> 5 & 1) == 1; + this.alarmGnssShortCircuited = (alarmSignInt >>> 6 & 1) == 1; + this.alarmUnderVoltage = (alarmSignInt >>> 7 & 1) == 1; + this.alarmPowerOff = (alarmSignInt >>> 8 & 1) == 1; + this.alarmLCD = (alarmSignInt >>> 9 & 1) == 1; + this.alarmTtsFault = (alarmSignInt >>> 10 & 1) == 1; + this.alarmCameraFault = (alarmSignInt >>> 11 & 1) == 1; + this.alarmIcFault = (alarmSignInt >>> 12 & 1) == 1; + this.warningSpeeding = (alarmSignInt >>> 13 & 1) == 1; + this.warningTired = (alarmSignInt >>> 14 & 1) == 1; + this.alarmWrong = (alarmSignInt >>> 15 & 1) == 1; + this.warningTirePressure = (alarmSignInt >>> 16 & 1) == 1; + this.alarmBlindZone = (alarmSignInt >>> 17 & 1) == 1; + this.alarmDrivingTimeout = (alarmSignInt >>> 18 & 1) == 1; + this.alarmParkingTimeout = (alarmSignInt >>> 19 & 1) == 1; + this.alarmRegion = (alarmSignInt >>> 20 & 1) == 1; + this.alarmRoute = (alarmSignInt >>> 21 & 1) == 1; + this.alarmTravelTime = (alarmSignInt >>> 22 & 1) == 1; + this.alarmRouteDeviation = (alarmSignInt >>> 23 & 1) == 1; + this.alarmVSS = (alarmSignInt >>> 24 & 1) == 1; + this.alarmOil = (alarmSignInt >>> 25 & 1) == 1; + this.alarmStolen = (alarmSignInt >>> 26 & 1) == 1; + this.alarmIllegalIgnition = (alarmSignInt >>> 27 & 1) == 1; + this.alarmIllegalDisplacement = (alarmSignInt >>> 28 & 1) == 1; + this.alarmRollover = (alarmSignInt >>> 29 & 1) == 1; + this.warningRollover = (alarmSignInt >>> 30 & 1) == 1; + } + + public static JTAlarmSign decode(ByteBuf byteBuf) { + long alarmSignByte = byteBuf.readUnsignedInt(); + return new JTAlarmSign(alarmSignByte); + } + + @Override + public ByteBuf encode() { + // 限定容量 避免影响后续占位 + ByteBuf byteBuf = Unpooled.buffer(); + int alarmSignValue = 0; + if (urgent) { + alarmSignValue = alarmSignValue | 1; + } + if (alarmSpeeding) { + alarmSignValue = alarmSignValue | 1 << 1; + } + if (alarmTired) { + alarmSignValue = alarmSignValue | 1 << 2; + } + if (alarmDangerous) { + alarmSignValue = alarmSignValue | 1 << 3; + } + if (alarmGnssFault) { + alarmSignValue = alarmSignValue | 1 << 4; + } + if (alarmGnssBreak) { + alarmSignValue = alarmSignValue | 1 << 5; + } + if (alarmGnssShortCircuited) { + alarmSignValue = alarmSignValue | 1 << 6; + } + if (alarmUnderVoltage) { + alarmSignValue = alarmSignValue | 1 << 7; + } + if (alarmPowerOff) { + alarmSignValue = alarmSignValue | 1 << 8; + } + if (alarmLCD) { + alarmSignValue = alarmSignValue | 1 << 9; + } + if (alarmTtsFault) { + alarmSignValue = alarmSignValue | 1 << 10; + } + if (alarmCameraFault) { + alarmSignValue = alarmSignValue | 1 << 11; + } + if (alarmIcFault) { + alarmSignValue = alarmSignValue | 1 << 12; + } + if (warningSpeeding) { + alarmSignValue = alarmSignValue | 1 << 13; + } + if (warningTired) { + alarmSignValue = alarmSignValue | 1 << 14; + } + if (alarmWrong) { + alarmSignValue = alarmSignValue | 1 << 15; + } + if (warningTirePressure) { + alarmSignValue = alarmSignValue | 1 << 16; + } + if (alarmBlindZone) { + alarmSignValue = alarmSignValue | 1 << 17; + } + if (alarmDrivingTimeout) { + alarmSignValue = alarmSignValue | 1 << 18; + } + if (alarmParkingTimeout) { + alarmSignValue = alarmSignValue | 1 << 19; + } + if (alarmRegion) { + alarmSignValue = alarmSignValue | 1 << 20; + } + if (alarmRoute) { + alarmSignValue = alarmSignValue | 1 << 21; + } + if (alarmTravelTime) { + alarmSignValue = alarmSignValue | 1 << 22; + } + if (alarmRouteDeviation) { + alarmSignValue = alarmSignValue | 1 << 23; + } + if (alarmVSS) { + alarmSignValue = alarmSignValue | 1 << 24; + } + if (alarmOil) { + alarmSignValue = alarmSignValue | 1 << 25; + } + if (alarmStolen) { + alarmSignValue = alarmSignValue | 1 << 26; + } + if (alarmIllegalIgnition) { + alarmSignValue = alarmSignValue | 1 << 27; + } + if (alarmIllegalDisplacement) { + alarmSignValue = alarmSignValue | 1 << 28; + } + if (alarmRollover) { + alarmSignValue = alarmSignValue | 1 << 29; + } + if (warningRollover) { + alarmSignValue = alarmSignValue | 1 << 30; + } + byteBuf.writeInt(alarmSignValue); + return byteBuf; + } + + @Override + public String toString() { + return "状态报警标志位:" + + "\n 紧急报警:" + (urgent?"开":"关") + + "\n 超速报警:" + (alarmSpeeding?"开":"关") + + "\n 疲劳驾驶报警:" + (alarmTired?"开":"关") + + "\n 危险驾驶行为报警:" + (alarmDangerous?"开":"关") + + "\n GNSS 模块发生故障报警:" + (alarmGnssFault?"开":"关") + + "\n GNSS 天线未接或被剪断报警:" + (alarmGnssBreak?"开":"关") + + "\n GNSS 天线短路报警:" + (alarmGnssShortCircuited?"开":"关") + + "\n 终端主电源欠压报警:" + (alarmUnderVoltage?"开":"关") + + "\n 终端主电源掉电报警:" + (alarmPowerOff?"开":"关") + + "\n 终端LCD或显示器故障报警:" + (alarmLCD?"开":"关") + + "\n TTS 模块故障报警:" + (alarmTtsFault?"开":"关") + + "\n 摄像头故障报警:" + (alarmCameraFault?"开":"关") + + "\n 道路运输证IC卡模块故障报警:" + (alarmIcFault?"开":"关") + + "\n 超速预警:" + (warningSpeeding?"开":"关") + + "\n 疲劳驾驶预警:" + (warningTired?"开":"关") + + "\n 违规行驶报警:" + (alarmWrong ?"开":"关") + + "\n 胎压预警:" + (warningTirePressure?"开":"关") + + "\n 右转盲区异常报警:" + (alarmBlindZone?"开":"关") + + "\n 当天累计驾驶超时报警:" + (alarmDrivingTimeout?"开":"关") + + "\n 超时停车报警:" + (alarmParkingTimeout?"开":"关") + + "\n 进出区域报警:" + (alarmRegion?"开":"关") + + "\n 进出路线报警:" + (alarmRoute?"开":"关") + + "\n 路段行驶时间不足/过长报警:" + (alarmTravelTime?"开":"关") + + "\n 路线偏离报警:" + (alarmRouteDeviation?"开":"关") + + "\n 车辆 VSS 故障:" + (alarmVSS?"开":"关") + + "\n 车辆油量异常报警:" + (alarmOil?"开":"关") + + "\n 车辆被盗报警(通过车辆防盗器):" + (alarmStolen?"开":"关") + + "\n 车辆非法点火报警:" + (alarmIllegalIgnition?"开":"关") + + "\n 车辆非法位移报警:" + (alarmIllegalDisplacement?"开":"关") + + "\n 碰撞侧翻报警:" + (alarmRollover?"开":"关") + + "\n 侧翻预警:" + (warningRollover?"开":"关") + + "\n "; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAreaAttribute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAreaAttribute.java new file mode 100644 index 0000000..b21e2d2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAreaAttribute.java @@ -0,0 +1,103 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "区域属性") +public class JTAreaAttribute { + + @Schema(description = "是否启用起始时间与结束时间的判断规则 ,false:否;true:是") + private boolean ruleForTimeLimit; + + @Schema(description = "是否启用最高速度、超速持续时间和夜间最高速度的判断规则 ,false:否;true:是") + private boolean ruleForSpeedLimit; + + @Schema(description = "进区域是否报警给驾驶员,false:否;true:是") + private boolean ruleForAlarmToDriverWhenEnter; + + @Schema(description = "进区域是否报警给平台 ,false:否;true:是") + private boolean ruleForAlarmToPlatformWhenEnter; + + @Schema(description = "出区域是否报警给驾驶员,false:否;true:是") + private boolean ruleForAlarmToDriverWhenExit; + + @Schema(description = "出区域是否报警给平台 ,false:否;true:是") + private boolean ruleForAlarmToPlatformWhenExit; + + @Schema(description = "false:北纬;true:南纬") + private boolean southLatitude; + + @Schema(description = "false:东经;true:西经") + private boolean westLongitude; + + @Schema(description = "false:允许开门;true:禁止开门") + private boolean prohibitOpeningDoors; + + @Schema(description = "false:进区域开启通信模块;true:进区域关闭通信模块") + private boolean ruleForTurnOffCommunicationWhenEnter; + + @Schema(description = "false:进区域不采集 GNSS 详细定位数据;true:进区域采集 GNSS 详细定位数据") + private boolean ruleForGnssWhenEnter; + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + short content = 0 ; + if (ruleForTimeLimit) { + content |= 1; + } + if (ruleForSpeedLimit) { + content |= (1 << 1); + } + if (ruleForAlarmToDriverWhenEnter) { + content |= (1 << 2); + } + if (ruleForAlarmToPlatformWhenEnter) { + content |= (1 << 3); + } + if (ruleForAlarmToDriverWhenExit) { + content |= (1 << 4); + } + if (ruleForAlarmToPlatformWhenExit) { + content |= (1 << 5); + } + if (southLatitude) { + content |= (1 << 6); + } + if (westLongitude) { + content |= (byte) (1 << 7); + } + if (prohibitOpeningDoors) { + content |= (1 << (0 + 8)); + } + if (ruleForTurnOffCommunicationWhenEnter) { + content |= (1 << (1 + 8)); + } + if (ruleForGnssWhenEnter) { + content |= (1 << (2 + 8)); + } + byteBuf.writeShort((short)(content & 0xffff)); + return byteBuf; + } + + public static JTAreaAttribute decode(int attributeInt) { + JTAreaAttribute attribute = new JTAreaAttribute(); + attribute.setRuleForTimeLimit((attributeInt & 1) == 1); + attribute.setRuleForSpeedLimit((attributeInt >> 1 & 1) == 1); + attribute.setRuleForAlarmToDriverWhenEnter((attributeInt >> 2 & 1) == 1); + attribute.setRuleForAlarmToPlatformWhenEnter((attributeInt >> 3 & 1) == 1); + attribute.setRuleForAlarmToDriverWhenExit((attributeInt >> 4 & 1) == 1); + attribute.setRuleForAlarmToPlatformWhenExit((attributeInt >> 5 & 1) == 1); + attribute.setSouthLatitude((attributeInt >> 6 & 1) == 1); + attribute.setWestLongitude((attributeInt >> 7 & 1) == 1); + attribute.setProhibitOpeningDoors((attributeInt >> 8 & 1) == 1); + attribute.setRuleForTurnOffCommunicationWhenEnter((attributeInt >> 9 & 1) == 1); + attribute.setRuleForGnssWhenEnter((attributeInt >> 10 & 1) == 1); + return attribute; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAreaOrRoute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAreaOrRoute.java new file mode 100644 index 0000000..0ad0218 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAreaOrRoute.java @@ -0,0 +1,4 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +public interface JTAreaOrRoute { +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTChannel.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTChannel.java new file mode 100644 index 0000000..2d4e022 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTChannel.java @@ -0,0 +1,67 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.util.ObjectUtils; + +/** + * JT 通道 + */ +@Data +@Schema(description = "jt808通道") +@EqualsAndHashCode(callSuper = true) +public class JTChannel extends CommonGBChannel { + + @Schema(description = "数据库自增ID") + private int id; + + @Schema(description = "名称") + private String name; + + @Schema(description = "设备的数据库ID") + private int terminalDbId; + + @Schema(description = "通道ID") + private Integer channelId; + + @Schema(description = "是否含有音频") + private boolean hasAudio; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "更新时间") + private String updateTime; + + @Schema(description = "流信息") + private String stream; + + private Integer dataType = ChannelDataType.JT_1078; + + @Override + public String toString() { + return "JTChannel{" + + "id=" + id + + ", name='" + name + '\'' + + ", terminalDbId=" + terminalDbId + + ", channelId=" + channelId + + ", createTime='" + createTime + '\'' + + ", updateTime='" + updateTime + '\'' + + ", hasAudio='" + hasAudio + '\'' + + '}'; + } + + public CommonGBChannel buildCommonGBChannel() { + if (ObjectUtils.isEmpty(this.getGbDeviceId())) { + return null; + } + if (ObjectUtils.isEmpty(this.getGbName())) { + this.setGbName(this.getName()); + } + return this; + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTCircleArea.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTCircleArea.java new file mode 100644 index 0000000..331ce68 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTCircleArea.java @@ -0,0 +1,94 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; +import java.util.Date; + +@Setter +@Getter +@Schema(description = "圆形区域") +public class JTCircleArea implements JTAreaOrRoute{ + + @Schema(description = "区域 ID") + private long id; + + @Schema(description = "") + private JTAreaAttribute attribute; + + @Schema(description = "中心点纬度") + private Double latitude; + + @Schema(description = "中心点经度") + private Double longitude; + + @Schema(description = "半径,单位为米(m)") + private long radius; + + @Schema(description = "起始时间, yyyy-MM-dd HH:mm:ss") + private String startTime; + + @Schema(description = "结束时间, yyyy-MM-dd HH:mm:ss") + private String endTime; + + @Schema(description = "最高速度, 单位为千米每小时(km/h)") + private int maxSpeed; + + @Schema(description = "超速持续时间, 单位为秒(s)") + private int overSpeedDuration; + + @Schema(description = "夜间最高速度, 单位为千米每小时(km/h)") + private int nighttimeMaxSpeed; + + @Schema(description = "区域的名称") + private String name; + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeInt((int) (id & 0xffffffffL)); + byteBuf.writeBytes(attribute.encode()); + byteBuf.writeInt((int) (Math.round((latitude * 1000000)) & 0xffffffffL)); + byteBuf.writeInt((int) (Math.round((longitude * 1000000)) & 0xffffffffL)); + byteBuf.writeInt((int) (radius & 0xffffffffL)); + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime))); + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime))); + byteBuf.writeShort((short)(maxSpeed & 0xffff)); + byteBuf.writeByte(overSpeedDuration); + byteBuf.writeShort((short)(nighttimeMaxSpeed & 0xffff)); + byteBuf.writeShort((short)(name.getBytes(Charset.forName("GBK")).length & 0xffff)); + byteBuf.writeCharSequence(name, Charset.forName("GBK")); + return byteBuf; + } + + public static JTCircleArea decode(ByteBuf buf) { + + JTCircleArea area = new JTCircleArea(); + area.setId(buf.readUnsignedInt()); + int attributeInt = buf.readUnsignedShort(); + JTAreaAttribute areaAttribute = JTAreaAttribute.decode(attributeInt); + area.setAttribute(areaAttribute); + + area.setLatitude(buf.readUnsignedInt()/1000000D); + area.setLongitude(buf.readUnsignedInt()/1000000D); + area.setRadius(buf.readUnsignedInt()); + byte[] startTimeBytes = new byte[6]; + buf.readBytes(startTimeBytes); + area.setStartTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(startTimeBytes))); + byte[] endTimeBytes = new byte[6]; + buf.readBytes(endTimeBytes); + area.setEndTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(endTimeBytes))); + area.setMaxSpeed(buf.readUnsignedShort()); + area.setOverSpeedDuration(buf.readUnsignedByte()); + area.setNighttimeMaxSpeed(buf.readUnsignedShort()); + int nameLength = buf.readUnsignedShort(); + area.setName(buf.readCharSequence(nameLength, Charset.forName("GBK")).toString().trim()); + return area; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTCommunicationModuleAttribute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTCommunicationModuleAttribute.java new file mode 100644 index 0000000..795da1a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTCommunicationModuleAttribute.java @@ -0,0 +1,57 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * JT 通信模块属性 + */ +@Setter +@Getter +@Schema(description = "JT通信模块属性") +public class JTCommunicationModuleAttribute { + + private boolean gprs ; + private boolean cdma ; + private boolean tdScdma ; + private boolean wcdma ; + private boolean cdma2000 ; + private boolean tdLte ; + private boolean other ; + + + public static JTCommunicationModuleAttribute getInstance(short content) { + boolean gprs = (content & 1) == 1; + boolean cdma = (content >>> 1 & 1) == 1; + boolean tdScdma = (content >>> 2 & 1) == 1; + boolean wcdma = (content >>> 3 & 1) == 1; + boolean cdma2000 = (content >>> 4 & 1) == 1; + boolean tdLte = (content >>> 5 & 1) == 1; + boolean other = (content >>> 7 & 1) == 1; + return new JTCommunicationModuleAttribute(gprs, cdma, tdScdma, wcdma, cdma2000, tdLte, other); + } + + public JTCommunicationModuleAttribute(boolean gprs, boolean cdma, boolean tdScdma, boolean wcdma, boolean cdma2000, boolean tdLte, boolean other) { + this.gprs = gprs; + this.cdma = cdma; + this.tdScdma = tdScdma; + this.wcdma = wcdma; + this.cdma2000 = cdma2000; + this.tdLte = tdLte; + this.other = other; + } + + @Override + public String toString() { + return "JCommunicationModuleAttribute{" + + "gprs=" + gprs + + ", cdma=" + cdma + + ", tdScdma=" + tdScdma + + ", wcdma=" + wcdma + + ", cdma2000=" + cdma2000 + + ", tdLte=" + tdLte + + ", other=" + other + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTConfirmationAlarmMessageType.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTConfirmationAlarmMessageType.java new file mode 100644 index 0000000..f96cc87 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTConfirmationAlarmMessageType.java @@ -0,0 +1,65 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "人工确认报警类型") +public class JTConfirmationAlarmMessageType { + @Schema(description = "确认紧急报警") + private boolean urgent; + @Schema(description = "确认危险预警") + private boolean alarmDangerous; + @Schema(description = "确认进出区域报警") + private boolean alarmRegion; + @Schema(description = "确认进出路线报警") + private boolean alarmRoute; + @Schema(description = "确认路段行驶时间不足/过长报警") + private boolean alarmTravelTime; + @Schema(description = "确认车辆非法点火报警") + private boolean alarmIllegalIgnition; + @Schema(description = "确认车辆非法位移报警") + private boolean alarmIllegalDisplacement; + + public long encode(){ + long result = 0L; + if (urgent) { + result |= 0x01; + } + if (alarmDangerous) { + result |= (0x01 << 3); + } + if (alarmRegion) { + result |= (0x01 << 20); + } + if (alarmRoute) { + result |= (0x01 << 21); + } + if (alarmTravelTime) { + result |= (0x01 << 22); + } + if (alarmIllegalIgnition) { + result |= (0x01 << 27); + } + if (alarmIllegalDisplacement) { + result |= (0x01 << 28); + } + return result; + } + + + @Override + public String toString() { + return "JConfirmationAlarmMessageType{" + + "urgent=" + urgent + + ", alarmDangerous=" + alarmDangerous + + ", alarmRegion=" + alarmRegion + + ", alarmRoute=" + alarmRoute + + ", alarmTravelTime=" + alarmTravelTime + + ", alarmIllegalIgnition=" + alarmIllegalIgnition + + ", alarmIllegalDisplacement=" + alarmIllegalDisplacement + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDevice.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDevice.java new file mode 100644 index 0000000..a64c549 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDevice.java @@ -0,0 +1,89 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * JT 设备 + */ +@Data +@Schema(description = "jt808设备") +public class JTDevice { + + private int id; + + @Schema(description = "省域ID") + private String provinceId; + + @Schema(description = "省域文字描述") + private String provinceText; + + @Schema(description = "市县域ID") + private String cityId; + + @Schema(description = "市县域文字描述") + private String cityText; + + @Schema(description = "制造商ID") + private String makerId; + + @Schema(description = "终端型号") + private String model; + + @Schema(description = "终端手机号") + private String phoneNumber; + + @Schema(description = "终端ID") + private String terminalId; + + @Schema(description = "车牌颜色") + private int plateColor; + + @Schema(description = "车牌") + private String plateNo; + + @Schema(description = "经度") + private Double longitude; + + @Schema(description = "纬度") + private Double latitude; + + @Schema(description = "注册时间") + private String registerTime; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "更新时间") + private String updateTime; + + @Schema(description = "状态") + private boolean status; + + @Schema(description = "设备使用的媒体id, 默认为null") + private String mediaServerId; + + @Schema(description = "地理坐标系, 目前支持 WGS84,GCJ02") + private String geoCoordSys; + + @Schema(description = "收流IP") + private String sdpIp; + + @Override + public String toString() { + return "JTDevice{" + + " 终端手机号='" + phoneNumber + '\'' + + ", 省域ID='" + provinceId + '\'' + + ", 省域文字描述='" + provinceText + '\'' + + ", 市县域ID='" + cityId + '\'' + + ", 市县域文字描述='" + cityText + '\'' + + ", 制造商ID='" + makerId + '\'' + + ", 终端型号='" + model + '\'' + + ", 设备ID='" + terminalId + '\'' + + ", 车牌颜色=" + plateColor + + ", 车牌='" + plateNo + '\'' + + ", 注册时间='" + registerTime + '\'' + + ", status=" + status + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceAttribute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceAttribute.java new file mode 100644 index 0000000..a932f53 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceAttribute.java @@ -0,0 +1,56 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * JT 终端属性 + */ +@Setter +@Getter +@Schema(description = "JT终端属性") +public class JTDeviceAttribute { + + @Schema(description = "终端类型") + private JTDeviceType type; + + @Schema(description = "制造商 ID") + private String makerId; + + @Schema(description = "终端型号") + private String deviceModel; + + @Schema(description = "终端 ID") + private String terminalId; + + @Schema(description = "终端 SIM卡 ICCID") + private String iccId; + + @Schema(description = "终端硬件版本号") + private String hardwareVersion; + + @Schema(description = "固件版本号") + private String firmwareVersion ; + + @Schema(description = "GNSS 模块属性") + private JTGnssAttribute gnssAttribute ; + + @Schema(description = "通信模块属性") + private JTCommunicationModuleAttribute communicationModuleAttribute ; + + @Override + public String toString() { + return "JTDeviceAttribute{" + + "type=" + type + + ", makerId='" + makerId + '\'' + + ", deviceModel='" + deviceModel + '\'' + + ", terminalId='" + terminalId + '\'' + + ", iccId='" + iccId + '\'' + + ", hardwareVersion='" + hardwareVersion + '\'' + + ", firmwareVersion='" + firmwareVersion + '\'' + + ", gnssAttribute=" + gnssAttribute + + ", communicationModuleAttribute=" + communicationModuleAttribute + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceConfig.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceConfig.java new file mode 100644 index 0000000..fef0d67 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceConfig.java @@ -0,0 +1,380 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.bean.common.ConfigAttribute; +import com.genersoft.iot.vmp.jt1078.bean.config.*; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * JT 终端参数设置 + */ +@Schema(description = "JT终端参数设置") +@Data +public class JTDeviceConfig { + + @ConfigAttribute(id = 0x1, type="Long", description = "终端心跳发送间隔,单位为秒(s)") + private Long keepaliveInterval; + + @ConfigAttribute(id = 0x2, type="Long", description = "TCP消息应答超时时间,单位为秒(s)") + private Long tcpResponseTimeout; + + @ConfigAttribute(id = 0x3, type="Long", description = "TCP消息重传次数") + private Long tcpRetransmissionCount; + + @ConfigAttribute(id = 0x4, type="Long", description = "UDP消息应答超时时间,单位为秒(s)") + private Long udpResponseTimeout; + + @ConfigAttribute(id = 0x5, type="Long", description = "UDP消息重传次数") + private Long udpRetransmissionCount; + + @ConfigAttribute(id = 0x6, type="Long", description = "SMS 消息应答超时时间,单位为秒(s)") + private Long smsResponseTimeout; + + @ConfigAttribute(id = 0x7, type="Long", description = "SMS 消息重传次数") + private Long smsRetransmissionCount; + + @ConfigAttribute(id = 0x10, type="String", description = "主服务器APN无线通信拨号访问点,若网络制式为 CDMA,则该处 为 PPP拨号号码") + private String apnMaster; + + @ConfigAttribute(id = 0x11, type="String", description = "主服务器无线通信拨号用户名") + private String dialingUsernameMaster; + + @ConfigAttribute(id = 0x12, type="String", description = "主服务器无线通信拨号密码") + private String dialingPasswordMaster; + + @ConfigAttribute(id = 0x13, type="String", description = "主服务器地址IP或域名,以冒号分割主机和端口 多个服务器使用分号分割") + private String addressMaster; + + @ConfigAttribute(id = 0x14, type="String", description = "备份服务器APN") + private String apnBackup; + + @ConfigAttribute(id = 0x15, type="String", description = "备份服务器无线通信拨号用户名") + private String dialingUsernameBackup; + + @ConfigAttribute(id = 0x16, type="String", description = "备份服务器无线通信拨号密码") + private String dialingPasswordBackup; + + @ConfigAttribute(id = 0x17, type="String", description = "备用服务器备份地址IP或域名,以冒号分割主机和端口 多个服务器使用分号分割") + private String addressBackup; + + @ConfigAttribute(id = 0x1a, type="String", description = "道路运输证IC卡认证主服务器IP地址或域名") + private String addressIcMaster; + + @ConfigAttribute(id = 0x1b, type="Long", description = "道路运输证IC卡认证主服务器TCP端口") + private Long tcpPortIcMaster; + + @ConfigAttribute(id = 0x1c, type="Long", description = "道路运输证IC卡认证主服务器UDP端口") + private Long udpPortIcMaster; + + @ConfigAttribute(id = 0x1d, type="String", description = "道路运输证IC卡认证备份服务器IP地址或域名,端口同主服务器") + private String addressIcBackup; + + @ConfigAttribute(id = 0x20, type="Long", description = "位置汇报策略, 0定时汇报 1定距汇报 2定时和定距汇报") + private Long locationReportingStrategy; + + @ConfigAttribute(id = 0x21, type="Long", description = "位置汇报方案,0根据ACC状态 1根据登录状态和ACC状态,先判断登录状态,若登录再根据ACC状态") + private Long locationReportingPlan; + + @ConfigAttribute(id = 0x22, type="Long", description = "驾驶员未登录汇报时间间隔,单位为秒,值大于零") + private Long reportingIntervalOffline; + + @ConfigAttribute(id = 0x23, type="String", description = "从服务器 APN# 该值为空时 !终端应使用主服务器相同配置") + private String apnSlave; + + @ConfigAttribute(id = 0x24, type="String", description = "从服务器无线通信拨号用户名 # 该值为空时 !终端应使用主服务器 相同配置") + private String dialingUsernameSlave; + + @ConfigAttribute(id = 0x25, type="String", description = "从服务器无线通信拨号密码 # 该值为空时 !终端应使用主服务器相 同配置") + private String dialingPasswordSlave; + + @ConfigAttribute(id = 0x26, type="String", description = "从服务器备份地址 IP或域名 !主机和端口用冒号分割 !多个服务器 使用分号分割") + private String addressSlave; + + @ConfigAttribute(id = 0x27, type="Long", description = "休眠时汇报时间间隔 单位为秒 值大于0") + private Long reportingIntervalDormancy; + + @ConfigAttribute(id = 0x28, type="Long", description = "紧急报警时汇报时间间隔 单位为秒 值大于0") + private Long reportingIntervalEmergencyAlarm; + + @ConfigAttribute(id = 0x29, type="Long", description = "缺省时间汇报间隔 单位为秒 值大于0") + private Long reportingIntervalDefault; + + @ConfigAttribute(id = 0x2c, type="Long", description = "缺省距离汇报间隔 单位为米 值大于0") + private Long reportingDistanceDefault; + + @ConfigAttribute(id = 0x2d, type="Long", description = "驾驶员未登录汇报距离间隔 单位为米 值大于0") + private Long reportingDistanceOffline; + + @ConfigAttribute(id = 0x2e, type="Long", description = "休眠时汇报距离间隔 单位为米 值大于0") + private Long reportingDistanceDormancy; + + @ConfigAttribute(id = 0x2f, type="Long", description = "紧急报警时汇报距离间隔 单位为米 值大于0") + private Long reportingDistanceEmergencyAlarm; + + @ConfigAttribute(id = 0x30, type="Long", description = "拐点补传角度 ,值小于180") + private Long inflectionPointAngle; + + @ConfigAttribute(id = 0x31, type="Integer", description = "电子围栏半径(非法位移國值) ,单位为米(m)") + private Integer fenceRadius; + + @ConfigAttribute(id = 0x32, type="IllegalDrivingPeriods", description = "违规行驶时段范围 ,精确到分") + private JTIllegalDrivingPeriods illegalDrivingPeriods; + + @ConfigAttribute(id = 0x40, type="String", description = "监控平台电话号码") + private String platformPhoneNumber; + + @ConfigAttribute(id = 0x41, type="String", description = "复位电话号码 ,可采用此电话号码拨打终端电话让终端复位") + private String phoneNumberForReset; + + @ConfigAttribute(id = 0x42, type="String", description = "恢复出厂设置电话号码 ,可采用此电话号码拨打终端电话让终端恢 复出厂设置") + private String phoneNumberForFactoryReset; + + @ConfigAttribute(id = 0x42, type="String", description = "监控平台 SMS 电话号码") + private String phoneNumberForSms; + + @ConfigAttribute(id = 0x44, type="String", description = "接收终端 SMS 文本报警号码") + private String phoneNumberForReceiveTextAlarm; + + @ConfigAttribute(id = 0x45, type="Long", description = "终端电话接听策略 。0:自动接听;1:ACC ON时自动接听 ,OFF时手动接听") + private Long phoneAnsweringPolicy; + + @ConfigAttribute(id = 0x46, type="Long", description = "每次最长通话时间 ,单位为秒(s) ,0 为不允许通话 ,0xFFFFFFFF为不限制") + private Long longestCallTimeForPerSession; + + @ConfigAttribute(id = 0x47, type="Long", description = "当月最长通话时间 ,单位为秒(s) ,0 为不允许通话 ,0xFFFFFFFF为 不限制") + private Long longestCallTimeInMonth; + + @ConfigAttribute(id = 0x48, type="String", description = "监听电话号码") + private String phoneNumbersForListen; + + @ConfigAttribute(id = 0x49, type="String", description = "监管平台特权短信号码") + private String privilegedSMSNumber; + + @ConfigAttribute(id = 0x50, type="AlarmSign", description = "报警屏蔽字 ,与位置信息汇报消息中的报警标志相对应 ,相应位为 1 则相应报警被屏蔽") + private JTAlarmSign alarmMaskingWord; + + @ConfigAttribute(id = 0x51, type="AlarmSign", description = "报警发送文本 SMS 开关 , 与位置信息汇报消息中的报警标志相对 应 ,相应位为1 则相应报警时发送文本 SMS") + private JTAlarmSign alarmSendsTextSmsSwitch; + + @ConfigAttribute(id = 0x52, type="AlarmSign", description = "报警拍摄开关 ,与位置信息汇报消息中的报警标志相对应 ,相应位为 1 则相应报警时摄像头拍摄") + private JTAlarmSign alarmShootingSwitch; + + @ConfigAttribute(id = 0x53, type="AlarmSign", description = "报警拍摄存储标志 ,与位置信息汇报消息中的报警标志相对应 ,相应 位为1 则对相应报警时拍的照片进行存储 ,否则实时上传") + private JTAlarmSign alarmShootingStorageFlags; + + @ConfigAttribute(id = 0x54, type="AlarmSign", description = "关键标志 ,与位置信息汇报消息中的报警标志相对应 ,相应位为 1 则 对相应报警为关键报警") + private JTAlarmSign KeySign; + + @ConfigAttribute(id = 0x55, type="Long", description = "最高速度 ,单位为千米每小时(km/h)") + private Long maxSpeed; + + @ConfigAttribute(id = 0x56, type="Long", description = "超速持续时间 ,单位为秒(s)") + private Long overSpeedDuration; + + @ConfigAttribute(id = 0x57, type="Long", description = "连续驾驶时间门限 单位为秒(s)") + private Long continuousDrivingTimeThreshold; + + @ConfigAttribute(id = 0x58, type="Long", description = "当天累计驾驶时间门限 单位为秒(s)") + private Long cumulativeDrivingTimeThresholdForTheDay; + + @ConfigAttribute(id = 0x59, type="Long", description = "最小休息时间 单位为秒(s)") + private Long minimumBreakTime; + + @ConfigAttribute(id = 0x5a, type="Long", description = "最长停车时间 单位为秒(s)") + private Long maximumParkingTime; + + @ConfigAttribute(id = 0x5b, type="Integer", description = "超速预警差值 单位为1/10 千米每小时(1/10km/h)") + private Integer overSpeedWarningDifference; + + @ConfigAttribute(id = 0x5c, type="Integer", description = "疲劳驾驶预警差值 单位为秒 值大于零") + private Integer drowsyDrivingWarningDifference; + + @ConfigAttribute(id = 0x5d, type="CollisionAlarmParams", description = "碰撞报警参数设置") + private JTCollisionAlarmParams collisionAlarmParams; + + @ConfigAttribute(id = 0x5e, type="Integer", description = "侧翻报警参数设置:侧翻角度,单位为度,默认为30") + private Integer rolloverAlarm; + + @ConfigAttribute(id = 0x64, type="CameraTimer", description = "定时拍照控制") + private JTCameraTimer cameraTimer; + + @ConfigAttribute(id = 0x70, type="Long", description = "图像/视频质量 设置范围为1~10 1表示最优质量") + private Long qualityForVideo; + + @ConfigAttribute(id = 0x71, type="Long", description = "亮度,设置范围为0 ~ 255") + private Long brightness; + + @ConfigAttribute(id = 0x72, type="Long", description = "对比度,设置范围为0 ~ 127") + private Long contrastRatio; + + @ConfigAttribute(id = 0x73, type="Long", description = "饱和度,设置范围为0 ~ 127") + private Long saturation; + + @ConfigAttribute(id = 0x74, type="Long", description = "色度,设置范围为0 ~ 255") + private Long chroma; + + @ConfigAttribute(id = 0x75, type="VideoParam", description = "音视频参数设置") + private JTVideoParam videoParam; + + @ConfigAttribute(id = 0x76, type="ChannelListParam", description = "音视频通道列表设置") + private JTChannelListParam channelListParam; + + @ConfigAttribute(id = 0x77, type="ChannelParam", description = "单独视频通道参数设置") + private JTChannelParam channelParam; + + @ConfigAttribute(id = 0x79, type="AlarmRecordingParam", description = "特殊报警录像参数设置") + private JTAlarmRecordingParam alarmRecordingParam; + + @ConfigAttribute(id = 0x7a, type="VideoAlarmBit", description = "视频相关报警屏蔽字") + private JTVideoAlarmBit videoAlarmBit; + + @ConfigAttribute(id = 0x7b, type="AnalyzeAlarmParam", description = "图像分析报警参数设置") + private JTAnalyzeAlarmParam analyzeAlarmParam; + + @ConfigAttribute(id = 0x7c, type="AwakenParam", description = "终端休眠唤醒模式设置") + private JTAwakenParam awakenParam; + + @ConfigAttribute(id = 0x80, type="Long", description = "车辆里程表读数,单位'1/10km") + private Long mileage; + + @ConfigAttribute(id = 0x81, type="Integer", description = "车辆所在的省域ID") + private Integer provincialId; + + @ConfigAttribute(id = 0x82, type="Integer", description = "车辆所在的市域ID") + private Integer cityId; + + @ConfigAttribute(id = 0x83, type="String", description = "公安交通管理部门颁发的机动车号牌") + private String licensePlate; + + @ConfigAttribute(id = 0x84, type="Short", description = "车牌颜色,值按照JT/T697-7.2014中的规定,未上牌车辆填0") + private Short licensePlateColor; + + @ConfigAttribute(id = 0x90, type="GnssPositioningMode", description = "GNSS定位模式") + private JTGnssPositioningMode gnssPositioningMode; + + @ConfigAttribute(id = 0x91, type="Short", description = "GNSS 波特率,定义如下: 0: 4800, 1:9600, 2:19200, 3:38400, 4:57600, 5:115200") + private Short gnssBaudRate; + + @ConfigAttribute(id = 0x92, type="Short", description = "GNSS 模块详细定位数据输出频率,定义如下: 0: 500ms, 1:1000ms(默认值), 2:2000ms, 3:3000ms, 4:4000ms") + private Short gnssOutputFrequency; + + @ConfigAttribute(id = 0x93, type="Long", description = "GNSS 模块详细定位数据采集频率 ,单位为秒(s) ,默认为1") + private Long gnssCollectionFrequency; + + @ConfigAttribute(id = 0x94, type="Short", description = "GNSS 模块详细定位数据上传方式:,定义如下: " + + "0: 本地存储 ,不上传(默认值) , " + + "1:按时间间隔上传, " + + "2:按距离间隔上传, " + + "11:按累计时间上传 ,达到传输时间后自动停止上传, " + + "12:按累计距离上传 ,达到距离后自动停止上传, " + + "13:按累计条数上传 ,达到上传条数后自动停止上传") + private Short gnssDataUploadMethod; + + @ConfigAttribute(id = 0x95, type="Long", description = "GNSS 模块详细定位数据上传设置:,定义如下: " + + "1:单位为秒(s), " + + "2:单位为米(m) , " + + "11:单位为 秒(s), " + + "12:单位为米(m), " + + "13:单位 为条") + private Long gnssDataUploadMethodUnit; + + @ConfigAttribute(id = 0x100, type="Long", description = "CAN总线通道1 采集时间间隔 ,单位为毫秒(ms) ,0 表示不采集") + private Long canCollectionTimeForChannel1; + + @ConfigAttribute(id = 0x101, type="Integer", description = "CAN总线通道1 上传时间间隔 ,单位为秒(s) ,0 表示不上传") + private Integer canUploadIntervalForChannel1; + + @ConfigAttribute(id = 0x102, type="Long", description = "CAN总线通道2 采集时间间隔 ,单位为毫秒(ms) ,0 表示不采集") + private Long canCollectionTimeForChannel2; + + @ConfigAttribute(id = 0x103, type="Integer", description = "CAN总线通道2 上传时间间隔 ,单位为秒(s) ,0 表示不上传") + private Integer canUploadIntervalForChannel2; + + @Override + public String toString() { + return "JTDeviceConfig{" + + "终端心跳发送间隔: " + keepaliveInterval + "秒" + + ", TCP消息应答超时时间:" + tcpResponseTimeout + "秒" + + ", TCP消息重传次数: " + tcpRetransmissionCount + "秒" + + ", UDP消息应答超时时间: " + udpResponseTimeout + + ", UDP消息重传次数: " + udpRetransmissionCount + + ", SMS 消息应答超时时间: " + smsResponseTimeout + "秒" + + ", SMS 消息重传次数: " + smsRetransmissionCount + + ", 主服务器APN无线通信拨号访问点: " + apnMaster + '\'' + + ", 主服务器无线通信拨号用户名: " + dialingUsernameMaster + + ", 主服务器无线通信拨号密码: " + dialingPasswordMaster + + ", 主服务器地址IP或域名: " + addressMaster + + ", 备份服务器APN: " + apnBackup + + ", 备份服务器无线通信拨号用户名: " + dialingUsernameBackup + + ", 备份服务器无线通信拨号密码: " + dialingPasswordBackup + + ", 备用服务器备份地址IP或域名: " + addressBackup + + ", 道路运输证IC卡认证主服务器IP地址或域名: " + addressIcMaster + + ", 道路运输证IC卡认证主服务器TCP端口: " + tcpPortIcMaster + + ", 道路运输证IC卡认证主服务器UDP端口: " + udpPortIcMaster + + ", 道路运输证IC卡认证备份服务器IP地址或域名: " + addressIcBackup + + ", 位置汇报策略: " + locationReportingStrategy + + ", 位置汇报方案: " + locationReportingPlan + + ", 驾驶员未登录汇报时间间隔: " + reportingIntervalOffline + "秒" + + ", 从服务器 APN: " + apnSlave + + ", 从服务器无线通信拨号密码: " + dialingUsernameSlave + + ", 从服务器备份地址 IP或域名: " + dialingPasswordSlave + + ", 从服务器备份地址 IP或域名: " + addressSlave + + ", reportingIntervalDormancy: " + reportingIntervalDormancy + + ", reportingIntervalEmergencyAlarm: " + reportingIntervalEmergencyAlarm + + ", reportingIntervalDefault: " + reportingIntervalDefault + + ", reportingDistanceDefault: " + reportingDistanceDefault + + ", reportingDistanceOffline: " + reportingDistanceOffline + + ", reportingDistanceDormancy: " + reportingDistanceDormancy + + ", reportingDistanceEmergencyAlarm: " + reportingDistanceEmergencyAlarm + + ", inflectionPointAngle: " + inflectionPointAngle + + ", fenceRadius: " + fenceRadius + + ", illegalDrivingPeriods: " + illegalDrivingPeriods + + ", platformPhoneNumber: " + platformPhoneNumber + + ", phoneNumberForReset: " + phoneNumberForReset + + ", phoneNumberForFactoryReset: " + phoneNumberForFactoryReset + + ", phoneNumberForSms: " + phoneNumberForSms + + ", phoneNumberForReceiveTextAlarm: " + phoneNumberForReceiveTextAlarm + + ", phoneAnsweringPolicy: " + phoneAnsweringPolicy + + ", longestCallTimeForPerSession: " + longestCallTimeForPerSession + + ", longestCallTimeInMonth: " + longestCallTimeInMonth + + ", phoneNumbersForListen: " + phoneNumbersForListen + + ", privilegedSMSNumber: " + privilegedSMSNumber + + ", alarmMaskingWord: " + alarmMaskingWord + + ", alarmSendsTextSmsSwitch: " + alarmSendsTextSmsSwitch + + ", alarmShootingSwitch: " + alarmShootingSwitch + + ", alarmShootingStorageFlags: " + alarmShootingStorageFlags + + ", KeySign: " + KeySign + + ", topSpeed: " + maxSpeed + + ", overSpeedDuration: " + overSpeedDuration + + ", continuousDrivingTimeThreshold: " + continuousDrivingTimeThreshold + + ", cumulativeDrivingTimeThresholdForTheDay: " + cumulativeDrivingTimeThresholdForTheDay + + ", minimumBreakTime: " + minimumBreakTime + + ", maximumParkingTime: " + maximumParkingTime + + ", overSpeedWarningDifference: " + overSpeedWarningDifference + + ", drowsyDrivingWarningDifference: " + drowsyDrivingWarningDifference + + ", collisionAlarmParams: " + collisionAlarmParams + + ", rolloverAlarm: " + rolloverAlarm + + ", cameraTimer: " + cameraTimer + + ", qualityForVideo: " + qualityForVideo + + ", brightness: " + brightness + + ", contrastRatio: " + contrastRatio + + ", saturation: " + saturation + + ", chroma: " + chroma + + ", mileage: " + mileage + + ", provincialId: " + provincialId + + ", cityId: " + cityId + + ", licensePlate: " + licensePlate + + ", licensePlateColor: " + licensePlateColor + + ", gnssPositioningMode: " + gnssPositioningMode + + ", gnssBaudRate: " + gnssBaudRate + + ", gnssOutputFrequency: " + gnssOutputFrequency + + ", gnssCollectionFrequency: " + gnssCollectionFrequency + + ", gnssDataUploadMethod: " + gnssDataUploadMethod + + ", gnssDataUploadMethodUnit: " + gnssDataUploadMethodUnit + + ", canCollectionTimeForChannel1: " + canCollectionTimeForChannel1 + + ", canUploadIntervalForChannel1: " + canUploadIntervalForChannel1 + + ", canCollectionTimeForChannel2: " + canCollectionTimeForChannel2 + + ", canUploadIntervalForChannel2: " + canUploadIntervalForChannel2 + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceConnectionControl.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceConnectionControl.java new file mode 100644 index 0000000..af4cf27 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceConnectionControl.java @@ -0,0 +1,71 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * JT 终端控制 + */ +@Data +@Schema(description = "终端控制") +public class JTDeviceConnectionControl { + + /** + * false 表示切换到指定监管平台服务器 ,true 表示切换回原 缺省监控平台服务器 + */ + private Boolean switchOn; + /** + * 监管平台鉴权码 + */ + private String authentication; + + /** + * 拨号点名称 + */ + private String name; + + /** + * 拨号用户名 + */ + private String username; + + /** + * 拨号密码 + */ + private String password; + + /** + * 地址 + */ + private String address; + + /** + * TCP端口 + */ + private Integer tcpPort; + + /** + * UDP端口 + */ + private Integer udpPort; + + /** + * 连接到指定服务器时限 + */ + private Long timeLimit; + + @Override + public String toString() { + return "JTDeviceConnectionControl{" + + "switchOn=" + switchOn + + ", authentication='" + authentication + '\'' + + ", name='" + name + '\'' + + ", username='" + username + '\'' + + ", password='" + password + '\'' + + ", address='" + address + '\'' + + ", tcpPort=" + tcpPort + + ", udpPort=" + udpPort + + ", timeLimit=" + timeLimit + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceType.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceType.java new file mode 100644 index 0000000..caf49a2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceType.java @@ -0,0 +1,83 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * JT 终端类型 + */ +@Setter +@Getter +@Schema(description = "JT终端参数设置") +public class JTDeviceType { + + /** + * 适用客运车辆 + */ + private boolean passengerVehicles; + + /** + * 适用危险品车辆 + */ + private boolean dangerousGoodsVehicles; + + /** + * 普通货运车辆 + */ + private boolean freightVehicles; + + /** + * 出租车辆 + */ + private boolean rentalVehicles; + + /** + * 支持硬盘录像 + */ + private boolean hardDiskRecording; + + /** + * false:一体机 ,true:分体机 + */ + private boolean splittingMachine; + + /** + * 适用挂车 + */ + private boolean trailer; + + public static JTDeviceType getInstance(int content) { + boolean passengerVehicles = (content & 1) == 1; + boolean dangerousGoodsVehicles = (content >>> 1 & 1) == 1; + boolean freightVehicles = (content >>> 2 & 1) == 1; + boolean rentalVehicles = (content >>> 3 & 1) == 1; + boolean hardDiskRecording = (content >>> 6 & 1) == 1; + boolean splittingMachine = (content >>> 7 & 1) == 1; + boolean trailer = (content >>> 8 & 1) == 1; + return new JTDeviceType(passengerVehicles, dangerousGoodsVehicles, freightVehicles, rentalVehicles, hardDiskRecording, splittingMachine, trailer); + } + + public JTDeviceType(boolean passengerVehicles, boolean dangerousGoodsVehicles, boolean freightVehicles, boolean rentalVehicles, boolean hardDiskRecording, boolean splittingMachine, boolean trailer) { + this.passengerVehicles = passengerVehicles; + this.dangerousGoodsVehicles = dangerousGoodsVehicles; + this.freightVehicles = freightVehicles; + this.rentalVehicles = rentalVehicles; + this.hardDiskRecording = hardDiskRecording; + this.splittingMachine = splittingMachine; + this.trailer = trailer; + } + + @Override + public String toString() { + return "JTDeviceType{" + + "passengerVehicles=" + passengerVehicles + + ", dangerousGoodsVehicles=" + dangerousGoodsVehicles + + ", freightVehicles=" + freightVehicles + + ", rentalVehicles=" + rentalVehicles + + ", hardDiskRecording=" + hardDiskRecording + + ", splittingMachine=" + splittingMachine + + ", trailer=" + trailer + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDriverInformation.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDriverInformation.java new file mode 100644 index 0000000..3309e40 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDriverInformation.java @@ -0,0 +1,99 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.Charset; + +@Data +@Slf4j +@Schema(description = "驾驶员身份信息") +public class JTDriverInformation { + + @Schema(description = "0x01:从业资格证 IC卡插入( 驾驶员上班);0x02:从 业资格证 IC卡拔出(驾驶员下班)") + private int status; + + @Schema(description = "插卡/拔卡时间 ,以下字段在状 态为0x01 时才有效并做填充") + private String time; + + @Schema(description = "IC卡读取结果:" + + "0x00:IC卡读卡成功;" + + "0x01:读卡失败 ,原因为卡片密钥认证未通过;" + + "0x02:读卡失败 ,原因为卡片已被锁定;" + + "0x03:读卡失败 ,原因为卡片被拔出;" + + "0x04:读卡失败 ,原因为数据校验错误。" + + "以下字段在 IC卡读取结果等于0x00 时才有效") + private Integer result; + + @Schema(description = "驾驶员姓名") + private String name; + + @Schema(description = "从业资格证编码") + private String certificateCode; + + @Schema(description = "发证机构名称") + private String certificateIssuanceMechanismName; + + @Schema(description = "证件有效期") + private String expire; + + @Schema(description = "驾驶员身份证号") + private String driverIdNumber; + + public static JTDriverInformation decode(ByteBuf buf, boolean is2019) { + JTDriverInformation jtDriverInformation = new JTDriverInformation(); + jtDriverInformation.setStatus(buf.readUnsignedByte()); + byte[] bytes = new byte[6]; + buf.readBytes(bytes); + String timeStr = BCDUtil.transform(bytes); + try { + jtDriverInformation.setTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(timeStr)); + }catch (Exception e) { + log.error("[JT-驾驶员身份信息] 解码时无法格式化时间: {}", timeStr); + } + + if (jtDriverInformation.getStatus() == 1) { + int result = (int)buf.readUnsignedByte(); + jtDriverInformation.setResult(result); + if (result == 0) { + // IC卡读卡成功 + int nameLength = buf.readUnsignedByte(); + jtDriverInformation.setName(buf.readCharSequence(nameLength, Charset.forName("GBK")).toString().trim()); + jtDriverInformation.setCertificateCode(buf.readCharSequence(20, Charset.forName("GBK")).toString().trim()); + int certificateIssuanceMechanismNameLength = buf.readUnsignedByte(); + jtDriverInformation.setCertificateIssuanceMechanismName(buf.readCharSequence( + certificateIssuanceMechanismNameLength, Charset.forName("GBK")).toString().trim()); + byte[] bytesForExpire = new byte[4]; + buf.readBytes(bytesForExpire); + String bytesForExpireStr = BCDUtil.transform(bytesForExpire); + try { + jtDriverInformation.setExpire(DateUtil.jt1078dateToyyyy_MM_dd(bytesForExpireStr)); + }catch (Exception e) { + log.error("[JT-驾驶员身份信息] 解码时无法格式化时间: {}", bytesForExpireStr); + } + if (is2019) { + jtDriverInformation.setDriverIdNumber(buf.readCharSequence(20, Charset.forName("GBK")).toString().trim()); + } + } + } + return jtDriverInformation; + } + + @Override + public String toString() { + return "JTDriverInformation{" + + "status=" + status + + ", time='" + time + '\'' + + ", result=" + result + + ", name='" + name + '\'' + + ", certificateCode='" + certificateCode + '\'' + + ", certificateIssuanceMechanismName='" + certificateIssuanceMechanismName + '\'' + + ", expire='" + expire + '\'' + + ", driverIdNumber='" + driverIdNumber + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTGnssAttribute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTGnssAttribute.java new file mode 100644 index 0000000..37cd6f4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTGnssAttribute.java @@ -0,0 +1,47 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * JT GNSS 模块属性 + */ +@Setter +@Getter +@Schema(description = "JTGNSS 模块属性") +public class JTGnssAttribute { + + private boolean gps; + + private boolean beidou; + + private boolean glonass ; + + private boolean gaLiLeo; + + public static JTGnssAttribute getInstance(short content) { + boolean gps = (content & 1) == 1; + boolean beidou = (content >>> 1 & 1) == 1; + boolean glonass = (content >>> 2 & 1) == 1; + boolean gaLiLeo = (content >>> 3 & 1) == 1; + return new JTGnssAttribute(gps, beidou, glonass, gaLiLeo); + } + + public JTGnssAttribute(boolean gps, boolean beidou, boolean glonass, boolean gaLiLeo) { + this.gps = gps; + this.beidou = beidou; + this.glonass = glonass; + this.gaLiLeo = gaLiLeo; + } + + @Override + public String toString() { + return "JGnssAttribute{" + + "gps=" + gps + + ", beidou=" + beidou + + ", glonass=" + glonass + + ", gaLiLeo=" + gaLiLeo + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaAttribute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaAttribute.java new file mode 100644 index 0000000..f86c0ed --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaAttribute.java @@ -0,0 +1,129 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.bean.config.JTDeviceSubConfig; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 终端上传音视频属性 + */ +@Setter +@Getter +public class JTMediaAttribute implements JTDeviceSubConfig { + + /** + * 输入音频编码方式: + * 1 G. 721 + * 2 G. 722 + * 3 G. 723 + * 4 G. 728 + * 5 G. 729 + * 6 G. 711A + * 7 G. 711U + * 8 G. 726 + * 9 G. 729A + * 10 DVI4_3 + * 11 DVI4_4 + * 12 DVI4_8K + * 13 DVI4_16K + * 14 LPC + * 15 S16BE_STEREO + * 16 S16BE_MONO + * 17 MPEGAUDIO + * 18 LPCM + * 19 AAC + * 20 WMA9STD + * 21 HEAAC + * 22 PCM_VOICE + * 23 PCM_AUDIO + * 24 AACLC + * 25 MP3 + * 26 ADPCMA + * 27 MP4AUDIO + * 28 AMR + */ + private int audioEncoder; + + /** + * 输入音频声道数 + */ + private int audioChannels; + + /** + * 输入音频采样率: + * 0:8 kHz; + * 1:22. 05 kHz; + * 2:44. 1 kHz; + * 3:48 kHz + */ + private int audioSamplingRate; + + /** + * 输入音频采样位数: + * 0:8 位; + * 1:16 位; + * 2:32 位 + */ + private int audioSamplingBits; + + /** + * 音频帧长度: 范围 1 ~ 4 294 967 295 + */ + private int audioFrameLength; + + /** + * 是否支持音频输出: + * 0:不支持;1:支持 + */ + private int audioOutputEnable; + + /** + * 视频编码方式: + * 98 H. 264 + * 99 H. 265 + * 100 AVS + * 101 SVAC + */ + private int videoEncoder; + + /** + * 终端支持的最大音频物理通道数量: + */ + private int audioChannelMax; + + /** + * 终端支持的最大视频物理通道数量: + */ + private int videoChannelMax; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(audioEncoder); + byteBuf.writeByte(audioChannels); + byteBuf.writeByte(audioSamplingRate); + byteBuf.writeByte(audioSamplingBits); + byteBuf.writeShort(audioFrameLength); + byteBuf.writeByte(audioOutputEnable); + byteBuf.writeByte(videoEncoder); + byteBuf.writeByte(audioChannelMax); + byteBuf.writeByte(videoChannelMax); + return byteBuf; + } + + public static JTMediaAttribute decode(ByteBuf byteBuf) { + JTMediaAttribute jtMediaAttribute = new JTMediaAttribute(); + jtMediaAttribute.setAudioEncoder(byteBuf.readUnsignedByte()); + jtMediaAttribute.setAudioChannels(byteBuf.readUnsignedByte()); + jtMediaAttribute.setAudioSamplingRate(byteBuf.readUnsignedByte()); + jtMediaAttribute.setAudioSamplingBits(byteBuf.readUnsignedByte()); + jtMediaAttribute.setAudioFrameLength(byteBuf.readUnsignedShort()); + jtMediaAttribute.setAudioOutputEnable(byteBuf.readUnsignedByte()); + jtMediaAttribute.setVideoEncoder(byteBuf.readUnsignedByte()); + jtMediaAttribute.setAudioChannelMax(byteBuf.readUnsignedByte()); + jtMediaAttribute.setVideoChannelMax(byteBuf.readUnsignedByte()); + return jtMediaAttribute; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaDataInfo.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaDataInfo.java new file mode 100644 index 0000000..0c67e83 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaDataInfo.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "多媒体检索项数据") +public class JTMediaDataInfo { + + @Schema(description = "多媒体数据 ID") + private long id; + + @Schema(description = "多媒体类型, 0:图像;1:音频;2:视频") + private int type; + + @Schema(description = "事件项编码: 0:平台下发指令;1:定时动作;2:抢劫报警触发;3:碰 撞侧翻报警触发;4:门开拍照;5:门关拍照;6:车门由开 变关 ,车速从小于20km到超过20km;7:定距拍照") + private int eventCode; + + @Schema(description = "通道 ID") + private int channelId; + + @Schema(description = "表示拍摄或录制的起始时刻的汇报消息") + private JTPositionBaseInfo positionBaseInfo; + + public static JTMediaDataInfo decode(ByteBuf buf) { + JTMediaDataInfo jtMediaEventInfo = new JTMediaDataInfo(); + jtMediaEventInfo.setId(buf.readUnsignedInt()); + jtMediaEventInfo.setType(buf.readUnsignedByte()); + jtMediaEventInfo.setChannelId(buf.readUnsignedByte()); + jtMediaEventInfo.setEventCode(buf.readUnsignedByte()); + jtMediaEventInfo.setPositionBaseInfo(JTPositionBaseInfo.decode(buf)); + return jtMediaEventInfo; + } + + @Override + public String toString() { + return "JTMediaDataInfo{" + + "id=" + id + + ", type=" + type + + ", eventCode=" + eventCode + + ", channelId=" + channelId + + ", positionBaseInfo=" + positionBaseInfo + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaEventInfo.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaEventInfo.java new file mode 100644 index 0000000..fc3e062 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaEventInfo.java @@ -0,0 +1,62 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "多媒体事件信息") +public class JTMediaEventInfo { + + @Schema(description = "多媒体数据 ID") + private long id; + + @Schema(description = "多媒体类型, 0:图像;1:音频;2:视频") + private int type; + + @Schema(description = "多媒体格式编码, 0:JPEG;1:TIF;2:MP3;3:WAV;4:WMV;其他保留") + private int code; + + @Schema(description = "事件项编码: 0:平台下发指令;1:定时动作;2:抢劫报警触发;3:碰 撞侧翻报警触发;4:门开拍照;5:门关拍照;6:车门由开 变关 ,车速从小于20km到超过20km;7:定距拍照") + private int eventCode; + + @Schema(description = "通道 ID") + private int channelId; + + @Schema(description = "媒体数据") + private byte[] mediaData; + + @Schema(description = "位置信息汇报") + private JTPositionBaseInfo positionBaseInfo; + + + public static JTMediaEventInfo decode(ByteBuf buf) { + JTMediaEventInfo jtMediaEventInfo = new JTMediaEventInfo(); + jtMediaEventInfo.setId(buf.readUnsignedInt()); + jtMediaEventInfo.setType(buf.readUnsignedByte()); + jtMediaEventInfo.setCode(buf.readUnsignedByte()); + jtMediaEventInfo.setEventCode(buf.readUnsignedByte()); + jtMediaEventInfo.setChannelId(buf.readUnsignedByte()); + if (buf.readableBytes() > 28) { + ByteBuf byteBuf = buf.readSlice(28); + jtMediaEventInfo.setPositionBaseInfo(JTPositionBaseInfo.decode(byteBuf)); + byte[] bytes = new byte[buf.readableBytes()]; + buf.readBytes(bytes); + jtMediaEventInfo.setMediaData(bytes); + } + return jtMediaEventInfo; + } + + @Override + public String toString() { + return "JTMediaEventInfo{" + + "id=" + id + + ", type=" + type + + ", code=" + code + + ", eventCode=" + eventCode + + ", channelId=" + channelId + + ", fileSize=" + (mediaData == null ? 0 : mediaData.length) + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaStreamType.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaStreamType.java new file mode 100644 index 0000000..a5833ee --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaStreamType.java @@ -0,0 +1,5 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +public enum JTMediaStreamType { + PLAY,PLAYBACK,TALK +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPassengerNum.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPassengerNum.java new file mode 100644 index 0000000..34c8b87 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPassengerNum.java @@ -0,0 +1,63 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.bean.config.JTDeviceSubConfig; +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 终端上传乘客流量 + */ +@Setter +@Getter +public class JTPassengerNum implements JTDeviceSubConfig { + + /** + * 起始时间, YY-MM-DD-HH-MM-SS( GMT + 8 时间,本标准中之后涉及的时间均采用此时区) + */ + private String startTime; + + /** + * 结束时间, YY-MM-DD-HH-MM-SS( GMT + 8 时间,本标准中之后涉及的时间均采用此时区) + */ + private String endTime; + + /** + * 上车人数 + */ + private int getIn; + + /** + * 下车人数 + */ + private int getOut; + + @Override + public ByteBuf encode() { + return null; + } + + public static JTPassengerNum decode(ByteBuf buf) { + JTPassengerNum jtPassengerNum = new JTPassengerNum(); + byte[] bytes = new byte[6]; + buf.readBytes(bytes); + jtPassengerNum.setStartTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(bytes))); + buf.readBytes(bytes); + jtPassengerNum.setEndTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(bytes))); + jtPassengerNum.setGetIn(buf.readUnsignedShort()); + jtPassengerNum.setGetOut(buf.readUnsignedShort()); + return jtPassengerNum; + } + + @Override + public String toString() { + return "终端上传乘客流量:" + + " 时间: " + startTime + " 到 " + endTime + + ", 上车:" + getIn + + ", 下车:" + getOut + ; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPhoneBookContact.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPhoneBookContact.java new file mode 100644 index 0000000..b9a9b8c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPhoneBookContact.java @@ -0,0 +1,43 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; + +@Setter +@Getter +@Schema(description = "电话本联系人") +public class JTPhoneBookContact { + + @Schema(description = "1:呼入,2:呼出,3:呼入/呼出") + private int sign; + + @Schema(description = "电话号码") + private String phoneNumber; + + @Schema(description = "联系人") + private String contactName; + + public ByteBuf encode(){ + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(sign); + buffer.writeByte(phoneNumber.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(phoneNumber, Charset.forName("GBK")); + buffer.writeByte(contactName.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(contactName, Charset.forName("GBK")); + return buffer; + } + + @Override + public String toString() { + return "JTPhoneBookContact{" + + "sign=" + sign + + ", phoneNumber='" + phoneNumber + '\'' + + ", contactName='" + contactName + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPolygonArea.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPolygonArea.java new file mode 100644 index 0000000..e7d3a0f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPolygonArea.java @@ -0,0 +1,97 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +@Schema(description = "多边形区域") +public class JTPolygonArea implements JTAreaOrRoute{ + + @Schema(description = "区域 ID") + private long id; + + @Schema(description = "") + private JTAreaAttribute attribute; + + @Schema(description = "起始时间, yyyy-MM-dd HH:mm:ss") + private String startTime; + + @Schema(description = "结束时间, yyyy-MM-dd HH:mm:ss") + private String endTime; + + @Schema(description = "最高速度, 单位为千米每小时(km/h)") + private int maxSpeed; + + @Schema(description = "超速持续时间, 单位为秒(s)") + private int overSpeedDuration; + + @Schema(description = "区域顶点") + private List polygonPoints; + + @Schema(description = "夜间最高速度, 单位为千米每小时(km/h)") + private int nighttimeMaxSpeed; + + @Schema(description = "区域的名称") + private String name; + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeInt((int) (id & 0xffffffffL)); + byteBuf.writeBytes(attribute.encode()); + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime))); + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime))); + byteBuf.writeShort((short)(maxSpeed & 0xffff)); + byteBuf.writeByte(overSpeedDuration); + + byteBuf.writeShort((short)(polygonPoints.size() & 0xffff)); + if (!polygonPoints.isEmpty()) { + for (JTPolygonPoint polygonPoint : polygonPoints) { + byteBuf.writeBytes(polygonPoint.encode()); + } + } + byteBuf.writeShort((short)(nighttimeMaxSpeed & 0xffff)); + byteBuf.writeShort((short)(name.getBytes(Charset.forName("GBK")).length & 0xffff)); + byteBuf.writeCharSequence(name, Charset.forName("GBK")); + return byteBuf; + } + + public static JTPolygonArea decode(ByteBuf buf) { + JTPolygonArea area = new JTPolygonArea(); + area.setId(buf.readUnsignedInt()); + int attributeInt = buf.readUnsignedShort(); + JTAreaAttribute areaAttribute = JTAreaAttribute.decode(attributeInt); + area.setAttribute(areaAttribute); + byte[] startTimeBytes = new byte[6]; + buf.readBytes(startTimeBytes); + area.setStartTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(startTimeBytes))); + byte[] endTimeBytes = new byte[6]; + buf.readBytes(endTimeBytes); + area.setEndTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(endTimeBytes))); + area.setMaxSpeed(buf.readUnsignedShort()); + area.setOverSpeedDuration(buf.readUnsignedByte()); + int polygonPointsSize = buf.readUnsignedShort(); + List polygonPointList = new ArrayList<>(polygonPointsSize); + for (int i = 0; i < polygonPointsSize; i++) { + JTPolygonPoint polygonPoint = new JTPolygonPoint(); + polygonPoint.setLatitude(buf.readUnsignedInt()/1000000D); + polygonPoint.setLongitude(buf.readUnsignedInt()/1000000D); + polygonPointList.add(polygonPoint); + } + area.setPolygonPoints(polygonPointList); + area.setNighttimeMaxSpeed(buf.readUnsignedShort()); + int nameLength = buf.readUnsignedShort(); + area.setName(buf.readCharSequence(nameLength, Charset.forName("GBK")).toString().trim()); + return area; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPolygonPoint.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPolygonPoint.java new file mode 100644 index 0000000..6d81691 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPolygonPoint.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "多边形区域的顶点") +public class JTPolygonPoint { + + @Schema(description = "顶点纬度") + private Double latitude; + + @Schema(description = "顶点经度") + private Double longitude; + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeInt((int) (Math.round((latitude * 1000000)) & 0xffffffffL)); + byteBuf.writeInt((int) (Math.round((longitude * 1000000)) & 0xffffffffL)); + return byteBuf; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionAdditionalInfo.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionAdditionalInfo.java new file mode 100644 index 0000000..277d02f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionAdditionalInfo.java @@ -0,0 +1,29 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Schema(description = "位置附加信息") +public class JTPositionAdditionalInfo { + + @Schema(description = "里程, 单位为1/10km, 对应车上里程表读数") + private int mileage; + + @Schema(description = "油量, 单位为1/10L, 对应车上油量表读数") + private int oil; + + @Schema(description = "行驶记录功能获取的速度,单位为1/10km/h") + private int speed; + + @Schema(description = "报警事件的 ID") + private int alarmId; + // TODO 暂不支持胎压 + + @Schema(description = "车厢温度 ,单位为摄氏度") + private int carriageTemperature; + // TODO 暂不支持胎压 + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionBaseInfo.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionBaseInfo.java new file mode 100644 index 0000000..39b185b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionBaseInfo.java @@ -0,0 +1,118 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import io.netty.buffer.ByteBuf; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.nio.ByteBuffer; + +@Setter +@Getter +@Slf4j +@Schema(description = "位置基本信息") +public class JTPositionBaseInfo { + + /** + * 报警标志 + */ + @Schema(description = "报警标志") + private JTAlarmSign alarmSign; + + /** + * 状态 + */ + @Schema(description = "状态") + private JTStatus status; + + /** + * 经度 + */ + @Schema(description = "经度") + private Double longitude; + + /** + * 纬度 + */ + @Schema(description = "纬度") + private Double latitude; + + /** + * 高程 + */ + @Schema(description = "高程") + private Integer altitude; + + /** + * 速度 + */ + @Schema(description = "速度") + private Integer speed; + + /** + * 方向 + */ + @Schema(description = "方向") + private Integer direction; + + /** + * 时间 + */ + @Schema(description = "时间") + private String time; + + /** + * 视频报警 + */ + @Schema(description = "视频报警") + private JTVideoAlarm videoAlarm; + + public static JTPositionBaseInfo decode(ByteBuf buf) { + JTPositionBaseInfo positionInfo = new JTPositionBaseInfo(); + if (buf.readableBytes() < 17) { + log.error("[位置基本信息] 解码失败,长度不足: {}", buf.readableBytes()); + return positionInfo; + } + positionInfo.setAlarmSign(new JTAlarmSign(buf.readUnsignedInt())); + + positionInfo.setStatus(new JTStatus(buf.readUnsignedInt())); + + positionInfo.setLatitude(buf.readInt() * 0.000001D); + positionInfo.setLongitude(buf.readInt() * 0.000001D); + positionInfo.setAltitude(buf.readUnsignedShort()); + positionInfo.setSpeed(buf.readUnsignedShort()); + positionInfo.setDirection(buf.readUnsignedShort()); + byte[] timeBytes = new byte[6]; + buf.readBytes(timeBytes); + positionInfo.setTime(BCDUtil.transform(timeBytes)); + return positionInfo; + } + + + public String toSimpleString() { + return "简略位置汇报信息: " + + " \n 经度:" + longitude + + " \n 纬度:" + latitude + + " \n 高程: " + altitude + + " \n 速度: " + speed + + " \n 方向: " + direction + + " \n 时间: " + time + + " \n"; + } + + @Override + public String toString() { + return "位置汇报信息: " + + " \n 报警标志:" + alarmSign.toString() + + " \n 状态:" + status.toString() + + " \n 经度:" + longitude + + " \n 纬度:" + latitude + + " \n 高程: " + altitude + + " \n 速度: " + speed + + " \n 方向: " + direction + + " \n 时间: " + time + + " \n"; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionInfo.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionInfo.java new file mode 100644 index 0000000..c06db2d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionInfo.java @@ -0,0 +1,29 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +@Schema(description = "位置信息") +public class JTPositionInfo { + + /** + * 位置基本信息 + */ + @Schema(description = "位置基本信息") + private JTPositionBaseInfo base; + + /** + * 位置基本信息 + */ + @Schema(description = "位置附加信息") + private JTPositionAdditionalInfo additional; + + public void setBase(JTPositionBaseInfo base) { + this.base = base; + } + + public void setAdditional(JTPositionAdditionalInfo additional) { + this.additional = additional; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTQueryMediaDataCommand.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTQueryMediaDataCommand.java new file mode 100644 index 0000000..4ffbf36 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTQueryMediaDataCommand.java @@ -0,0 +1,67 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "存储多媒体数据") +public class JTQueryMediaDataCommand { + + @Schema(description = "多媒体类型: 0:图像;1:音频;2:视频") + private int type; + + @Schema(description = "通道 ID, 0 表示检索该媒体类型的所有通道") + private int chanelId; + + @Schema(description = "事件项编码: 0:平台下发指令;1:定时动作;2:抢劫报警触发;3:碰 撞侧翻报警触发;其他保留") + private int event; + + @Schema(description = "开始时间") + private String startTime; + + @Schema(description = "结束时间") + private String endTime; + + @Schema(description = "删除标志, 0:保留;1:删除, 存储多媒体数据上传命令中使用") + private Integer delete; + + + public ByteBuf decode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(type); + byteBuf.writeByte(chanelId); + byteBuf.writeByte(event); + if (startTime == null) { + byteBuf.writeBytes(BCDUtil.strToBcd("000000000000")); + }else { + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime))); + } + if (endTime == null) { + byteBuf.writeBytes(BCDUtil.strToBcd("000000000000")); + }else { + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime))); + } + if (delete != null) { + byteBuf.writeByte(delete); + } + return byteBuf; + } + + @Override + public String toString() { + return "JTQueryMediaDataCommand{" + + "type=" + type + + ", chanelId=" + chanelId + + ", event=" + event + + ", startTime='" + startTime + '\'' + + ", endTime='" + endTime + '\'' + + ", delete='" + delete + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRecordDownloadCatch.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRecordDownloadCatch.java new file mode 100644 index 0000000..f81de53 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRecordDownloadCatch.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.proc.response.J9206; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +public class JTRecordDownloadCatch implements Delayed { + + @Getter + @Setter + private String phoneNumber; + + @Getter + @Setter + private String path; + + @Getter + @Setter + private J9206 j9206; + + /** + * 超时时间(单位: 毫秒) + */ + @Getter + @Setter + private long delayTime; + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRectangleArea.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRectangleArea.java new file mode 100644 index 0000000..bda58a3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRectangleArea.java @@ -0,0 +1,97 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; + +@Setter +@Getter +@Schema(description = "矩形区域") +public class JTRectangleArea implements JTAreaOrRoute{ + + @Schema(description = "区域 ID") + private long id; + + @Schema(description = "") + private JTAreaAttribute attribute; + + @Schema(description = "左上点纬度") + private Double latitudeForUpperLeft; + + @Schema(description = "左上点经度") + private Double longitudeForUpperLeft; + + @Schema(description = "右下点纬度") + private Double latitudeForLowerRight; + + @Schema(description = "右下点经度") + private Double longitudeForLowerRight; + + @Schema(description = "起始时间, yyyy-MM-dd HH:mm:ss") + private String startTime; + + @Schema(description = "结束时间, yyyy-MM-dd HH:mm:ss") + private String endTime; + + @Schema(description = "最高速度, 单位为千米每小时(km/h)") + private int maxSpeed; + + @Schema(description = "超速持续时间, 单位为秒(s)") + private int overSpeedDuration; + + @Schema(description = "夜间最高速度, 单位为千米每小时(km/h)") + private int nighttimeMaxSpeed; + + @Schema(description = "区域的名称") + private String name; + + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeInt((int) (id & 0xffffffffL)); + byteBuf.writeBytes(attribute.encode()); + byteBuf.writeInt((int) (Math.round((latitudeForUpperLeft * 1000000)) & 0xffffffffL)); + byteBuf.writeInt((int) (Math.round((longitudeForUpperLeft * 1000000)) & 0xffffffffL)); + byteBuf.writeInt((int) (Math.round((latitudeForLowerRight * 1000000)) & 0xffffffffL)); + byteBuf.writeInt((int) (Math.round((longitudeForLowerRight * 1000000)) & 0xffffffffL)); + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime))); + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime))); + byteBuf.writeShort((short)(maxSpeed & 0xffff)); + byteBuf.writeByte(overSpeedDuration); + byteBuf.writeShort((short)(nighttimeMaxSpeed & 0xffff)); + byteBuf.writeShort((short)(name.getBytes(Charset.forName("GBK")).length & 0xffff)); + byteBuf.writeCharSequence(name, Charset.forName("GBK")); + return byteBuf; + } + + public static JTRectangleArea decode(ByteBuf buf) { + JTRectangleArea area = new JTRectangleArea(); + area.setId(buf.readUnsignedInt()); + int attributeInt = buf.readUnsignedShort(); + JTAreaAttribute areaAttribute = JTAreaAttribute.decode(attributeInt); + area.setAttribute(areaAttribute); + area.setLatitudeForUpperLeft(buf.readUnsignedInt()/1000000D); + area.setLongitudeForUpperLeft(buf.readUnsignedInt()/1000000D); + area.setLatitudeForLowerRight(buf.readUnsignedInt()/1000000D); + area.setLongitudeForLowerRight(buf.readUnsignedInt()/1000000D); + byte[] startTimeBytes = new byte[6]; + buf.readBytes(startTimeBytes); + area.setStartTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(startTimeBytes))); + byte[] endTimeBytes = new byte[6]; + buf.readBytes(endTimeBytes); + area.setEndTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(endTimeBytes))); + area.setMaxSpeed(buf.readUnsignedShort()); + area.setOverSpeedDuration(buf.readUnsignedByte()); + area.setNighttimeMaxSpeed(buf.readUnsignedShort()); + int nameLength = buf.readUnsignedShort(); + area.setName(buf.readCharSequence(nameLength, Charset.forName("GBK")).toString().trim()); + return area; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRoute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRoute.java new file mode 100644 index 0000000..ffc7802 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRoute.java @@ -0,0 +1,90 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +@Schema(description = "路线") +public class JTRoute implements JTAreaOrRoute{ + + @Schema(description = "路线 ID") + private long id; + + @Schema(description = "路线属性") + private JTRouteAttribute attribute; + + @Schema(description = "起始时间, yyyy-MM-dd HH:mm:ss") + private String startTime; + + @Schema(description = "结束时间, yyyy-MM-dd HH:mm:ss") + private String endTime; + + @Schema(description = "路线拐点") + private List routePointList; + + @Schema(description = "区域的名称") + private String name; + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeInt((int) (id & 0xffffffffL)); + byteBuf.writeBytes(attribute.encode()); + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime))); + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime))); + byteBuf.writeShort((short)(routePointList.size() & 0xffff)); + if (!routePointList.isEmpty()){ + for (JTRoutePoint jtRoutePoint : routePointList) { + byteBuf.writeBytes(jtRoutePoint.encode()); + } + } + byteBuf.writeShort((short)(name.getBytes(Charset.forName("GBK")).length & 0xffff)); + byteBuf.writeCharSequence(name, Charset.forName("GBK")); + return byteBuf; + } + + public static JTRoute decode(ByteBuf buf) { + JTRoute route = new JTRoute(); + route.setId(buf.readUnsignedInt()); + int attributeInt = buf.readUnsignedShort(); + JTRouteAttribute routeAttribute = JTRouteAttribute.decode(attributeInt); + route.setAttribute(routeAttribute); + byte[] startTimeBytes = new byte[6]; + buf.readBytes(startTimeBytes); + route.setStartTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(startTimeBytes))); + byte[] endTimeBytes = new byte[6]; + buf.readBytes(endTimeBytes); + route.setEndTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(endTimeBytes))); + + int routePointsSize = buf.readUnsignedShort(); + List jtRoutePoints = new ArrayList<>(routePointsSize); + for (int i = 0; i < routePointsSize; i++) { + jtRoutePoints.add(JTRoutePoint.decode(buf)); + } + route.setRoutePointList(jtRoutePoints); + int nameLength = buf.readUnsignedShort(); + route.setName(buf.readCharSequence(nameLength, Charset.forName("GBK")).toString().trim()); + return route; + } + + @Override + public String toString() { + return "JTRoute{" + + "id=" + id + + ", attribute=" + attribute + + ", startTime='" + startTime + '\'' + + ", endTime='" + endTime + '\'' + + ", routePointList=" + routePointList + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRouteAttribute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRouteAttribute.java new file mode 100644 index 0000000..f1aaecf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRouteAttribute.java @@ -0,0 +1,71 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "路线属性") +public class JTRouteAttribute { + + @Schema(description = "是否启用起始时间与结束时间的判断规则 ,false:否;true:是") + private boolean ruleForTimeLimit; + + @Schema(description = "进区域是否报警给驾驶员,false:否;true:是") + private boolean ruleForAlarmToDriverWhenEnter; + + @Schema(description = "进区域是否报警给平台 ,false:否;true:是") + private boolean ruleForAlarmToPlatformWhenEnter; + + @Schema(description = "出区域是否报警给驾驶员,false:否;true:是") + private boolean ruleForAlarmToDriverWhenExit; + + @Schema(description = "出区域是否报警给平台 ,false:否;true:是") + private boolean ruleForAlarmToPlatformWhenExit; + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + short content = 0; + if (ruleForTimeLimit) { + content |= 1; + } + if (ruleForAlarmToDriverWhenEnter) { + content |= (1 << 2); + } + if (ruleForAlarmToPlatformWhenEnter) { + content |= (1 << 3); + } + if (ruleForAlarmToDriverWhenExit) { + content |= (1 << 4); + } + if (ruleForAlarmToPlatformWhenExit) { + content |= (1 << 5); + } + byteBuf.writeShort((short)(content & 0xffff)); + return byteBuf; + } + + public static JTRouteAttribute decode(int attributeInt) { + JTRouteAttribute attribute = new JTRouteAttribute(); + attribute.setRuleForTimeLimit((attributeInt & 1) == 1); + attribute.setRuleForAlarmToDriverWhenEnter((attributeInt >> 2 & 1) == 1); + attribute.setRuleForAlarmToPlatformWhenEnter((attributeInt >> 3 & 1) == 1); + attribute.setRuleForAlarmToDriverWhenExit((attributeInt >> 4 & 1) == 1); + attribute.setRuleForAlarmToPlatformWhenExit((attributeInt >> 5 & 1) == 1); + return attribute; + } + + @Override + public String toString() { + return "JTRouteAttribute{" + + "ruleForTimeLimit=" + ruleForTimeLimit + + ", ruleForAlarmToDriverWhenEnter=" + ruleForAlarmToDriverWhenEnter + + ", ruleForAlarmToPlatformWhenEnter=" + ruleForAlarmToPlatformWhenEnter + + ", ruleForAlarmToDriverWhenExit=" + ruleForAlarmToDriverWhenExit + + ", ruleForAlarmToPlatformWhenExit=" + ruleForAlarmToPlatformWhenExit + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRoutePoint.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRoutePoint.java new file mode 100644 index 0000000..c494028 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRoutePoint.java @@ -0,0 +1,98 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "路线拐点") +public class JTRoutePoint { + + @Schema(description = "拐点 ID") + private long id; + + @Schema(description = "路段 ID") + private long routeSectionId; + + @Schema(description = "拐点纬度") + private Double latitude; + + @Schema(description = "拐点经度") + private Double longitude; + + @Schema(description = "路段宽度") + private int routeSectionAttributeWidth; + + @Schema(description = "路段属性") + private JTRouteSectionAttribute routeSectionAttribute; + + @Schema(description = "路段行驶过长國值") + private int routeSectionMaxLength; + + @Schema(description = "路段行驶不足國值") + private int routeSectionMinLength; + + @Schema(description = "路段最高速度") + private int routeSectionMaxSpeed; + + @Schema(description = "路段超速持续时间") + private int routeSectionOverSpeedDuration; + + @Schema(description = "路段夜间最高速度") + private int routeSectionNighttimeMaxSpeed; + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeInt((int) (id & 0xffffffffL)); + byteBuf.writeInt((int) (routeSectionId & 0xffffffffL)); + byteBuf.writeInt((int) (Math.round((latitude * 1000000)) & 0xffffffffL)); + byteBuf.writeInt((int) (Math.round((longitude * 1000000)) & 0xffffffffL)); + byteBuf.writeByte(routeSectionAttributeWidth); + byteBuf.writeByte(routeSectionAttribute.encode()); + byteBuf.writeShort((short)(routeSectionMaxLength & 0xffff)); + byteBuf.writeShort((short)(routeSectionMinLength & 0xffff)); + byteBuf.writeShort((short)(routeSectionMaxSpeed & 0xffff)); + byteBuf.writeByte(routeSectionOverSpeedDuration); + byteBuf.writeShort((short)(routeSectionNighttimeMaxSpeed & 0xffff)); + return byteBuf; + } + + public static JTRoutePoint decode(ByteBuf buf) { + JTRoutePoint point = new JTRoutePoint(); + point.setId(buf.readUnsignedInt()); + point.setRouteSectionId(buf.readUnsignedInt()); + point.setLatitude(buf.readUnsignedInt()/1000000D); + point.setLongitude(buf.readUnsignedInt()/1000000D); + point.setRouteSectionAttributeWidth(buf.readUnsignedByte()); + + JTRouteSectionAttribute areaAttribute = JTRouteSectionAttribute.decode(buf.readUnsignedByte()); + point.setRouteSectionAttribute(areaAttribute); + + point.setRouteSectionMaxLength(buf.readUnsignedShort()); + point.setRouteSectionMinLength(buf.readUnsignedShort()); + point.setRouteSectionMaxSpeed(buf.readUnsignedShort()); + point.setRouteSectionOverSpeedDuration(buf.readUnsignedByte()); + point.setRouteSectionNighttimeMaxSpeed(buf.readUnsignedShort()); + return point; + } + + @Override + public String toString() { + return "JTRoutePoint{" + + "id=" + id + + ", routeSectionId=" + routeSectionId + + ", latitude=" + latitude + + ", longitude=" + longitude + + ", routeSectionAttributeWidth=" + routeSectionAttributeWidth + + ", routeSectionAttribute=" + routeSectionAttribute + + ", routeSectionMaxLength=" + routeSectionMaxLength + + ", routeSectionMinLength=" + routeSectionMinLength + + ", routeSectionMaxSpeed=" + routeSectionMaxSpeed + + ", routeSectionOverSpeedDuration=" + routeSectionOverSpeedDuration + + ", routeSectionNighttimeMaxSpeed=" + routeSectionNighttimeMaxSpeed + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRouteSectionAttribute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRouteSectionAttribute.java new file mode 100644 index 0000000..3113f4e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRouteSectionAttribute.java @@ -0,0 +1,52 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "路段属性") +public class JTRouteSectionAttribute { + + @Schema(description = "行驶时间 ,false:否;true:是") + private boolean ruleForTimeLimit; + + @Schema(description = "限速 ,false:否;true:是") + private boolean ruleForSpeedLimit; + + @Schema(description = "false:北纬;true:南纬") + private boolean southLatitude; + + @Schema(description = "false:东经;true:西经") + private boolean westLongitude; + + public byte encode(){ + byte attributeByte = 0; + if (ruleForTimeLimit) { + attributeByte |= 1; + } + if (ruleForSpeedLimit) { + attributeByte |= (1 << 1); + } + if (southLatitude) { + attributeByte |= (1 << 2); + } + if (westLongitude) { + attributeByte |= (1 << 3); + } + return attributeByte; + } + + public static JTRouteSectionAttribute decode(short attributeShort) { + JTRouteSectionAttribute attribute = new JTRouteSectionAttribute(); + attribute.setRuleForTimeLimit((attributeShort & 1) == 1); + attribute.setRuleForSpeedLimit((attributeShort >> 1 & 1) == 1); + attribute.setSouthLatitude((attributeShort >> 2 & 1) == 1); + attribute.setWestLongitude((attributeShort >> 3 & 1) == 1); + return attribute; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTShootingCommand.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTShootingCommand.java new file mode 100644 index 0000000..9794f3b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTShootingCommand.java @@ -0,0 +1,85 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Schema(description = "拍摄命令参数") +public class JTShootingCommand { + + @Schema(description = "通道 ID") + private int chanelId; + + @Schema(description = "0:停止拍摄;0xFFFF:录像;其他:拍照张数") + private int command; + + @Schema(description = "拍照间隔/录像时间, 单位为秒(s) ,0 表示按最小间隔拍照或一直录像") + private int time; + + @Schema(description = "1:保存; 0:实时上传") + private int save; + + @Schema(description = "分辨率: " + + "0x00:最低分辨率" + + "0x01:320 x240;" + + "0x02:640 x480;" + + "0x03:800 x600;" + + "0x04:1024 x768;" + + "0x05:176 x144;" + + "0x06:352 x288;" + + "0x07:704 x288;" + + "0x08:704 x576;" + + "0xff:最高分辨率") + private int resolvingPower; + + @Schema(description = "图像/视频质量: 取值范围为 1 ~ 10 ,1 代表质量损失最小 ,10 表示压缩 比最大") + private int quality; + + @Schema(description = "亮度, 0 ~ 255") + private int brightness; + + @Schema(description = "对比度,0 ~ 127") + private int contrastRatio; + + @Schema(description = "饱和度,0 ~ 127") + private int saturation; + + @Schema(description = "色度,0 ~ 255") + private int chroma; + + public ByteBuf decode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(chanelId); + byteBuf.writeShort((short)(command & 0xffff)); + byteBuf.writeShort((short)(time & 0xffff)); + byteBuf.writeByte(save); + byteBuf.writeByte(resolvingPower); + byteBuf.writeByte(quality); + byteBuf.writeByte(brightness); + byteBuf.writeByte(contrastRatio); + byteBuf.writeByte(saturation); + byteBuf.writeByte(chroma); + return byteBuf; + } + + @Override + public String toString() { + return "JTShootingCommand{" + + "chanelId=" + chanelId + + ", command=" + command + + ", time=" + time + + ", save=" + save + + ", resolvingPower=" + resolvingPower + + ", quality=" + quality + + ", brightness=" + brightness + + ", contrastRatio=" + contrastRatio + + ", saturation=" + saturation + + ", chroma=" + chroma + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTStatus.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTStatus.java new file mode 100644 index 0000000..81174dc --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTStatus.java @@ -0,0 +1,136 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "状态信息") +public class JTStatus { + + @Schema(description = "false:ACC关;true: ACC开") + private boolean acc; + + @Schema(description = "false:未定位;true: 定位") + private boolean positioning; + + @Schema(description = "false:北纬;true: 南纬") + private boolean southLatitude; + + @Schema(description = "false:东经;true: 西经") + private boolean wesLongitude; + + @Schema(description = "false:运营状态;true: 停运状态") + private boolean outage; + + @Schema(description = "false:经纬度未经保密插件加密;true: 经纬度已经保密插件加密") + private boolean positionEncryption; + + + @Schema(description = "true: 紧急刹车系统采集的前撞预警") + private boolean warningFrontCrash; + + @Schema(description = "true: 车道偏移预警") + private boolean warningShifting; + + @Schema(description = "00:空车;01:半载;10:保留;11:满载。可表示客车的空载状态 ,重车及货车的空载、满载状态 ,该状态可由人工输入或传感器获取") + private int load; + + @Schema(description = "false:车辆油路正常;true: 车辆油路断开") + private boolean oilWayBreak; + + @Schema(description = "false:车辆电路正常;true: 车辆电路断开") + private boolean circuitBreak; + + @Schema(description = "false:车门解锁;true: 车门加锁") + private boolean doorLocking; + + @Schema(description = "false:门1 关;true: 门1 开(前门)") + private boolean door1Open; + + @Schema(description = "false:门2 关;true: 门2 开(中门)") + private boolean door2Open; + + @Schema(description = "false:门3 关;true: 门3 开(后门)") + private boolean door3Open; + + @Schema(description = "false:门4 关;true: 门4 开(驾驶席门)") + private boolean door4Open; + + @Schema(description = "false:门5 关;true: 门5 开(自定义)") + private boolean door5Open; + + @Schema(description = "false:未使用 GPS 卫星进行定位;true:使用 GPS 卫星进行定位") + private boolean gps; + + @Schema(description = "false:未使用北斗卫星进行定位;true:使用北斗卫星进行定位") + private boolean beidou; + + @Schema(description = "false:未使用GLONASS 卫星进行定位;true:使用GLONASS 卫星进行定位") + private boolean glonass; + + @Schema(description = "false:未使用GaLiLeo 卫星进行定位;true:使用GaLiLeo 卫星进行定位") + private boolean gaLiLeo; + + @Schema(description = "false:车辆处于停止状态;true:车辆处于行驶状态") + private boolean driving; + + public JTStatus() { + } + + public JTStatus(long statusInt) { + if (statusInt == 0) { + return; + } + this.acc = (statusInt & 1) == 1; + this.positioning = (statusInt >>> 1 & 1) == 1; + this.southLatitude = (statusInt >>> 2 & 1) == 1; + this.wesLongitude = (statusInt >>> 3 & 1) == 1; + this.outage = (statusInt >>> 4 & 1) == 1; + this.positionEncryption = (statusInt >>> 5 & 1) == 1; + this.warningFrontCrash = (statusInt >>> 6 & 1) == 1; + this.warningShifting = (statusInt >>> 7 & 1) == 1; + this.load = (int)(statusInt >>> 8 & 3); + this.oilWayBreak = (statusInt >>> 10 & 1) == 1; + this.circuitBreak = (statusInt >>> 11 & 1) == 1; + this.doorLocking = (statusInt >>> 12 & 1) == 1; + this.door1Open = (statusInt >>> 13 & 1) == 1; + this.door2Open = (statusInt >>> 14 & 1) == 1; + this.door3Open = (statusInt >>> 15 & 1) == 1; + this.door4Open = (statusInt >>> 16 & 1) == 1; + this.door5Open = (statusInt >>> 17 & 1) == 1; + this.gps = (statusInt >>> 18 & 1) == 1; + this.beidou = (statusInt >>> 19 & 1) == 1; + this.glonass = (statusInt >>> 20 & 1) == 1; + this.gaLiLeo = (statusInt >>> 21 & 1) == 1; + this.driving = (statusInt >>> 22 & 1) == 1; + } + + @Override + public String toString() { + return "状态位:" + + "\n acc状态:" + (acc?"开":"关") + + "\n 定位状态:" + (positioning?"定位":"未定位") + + "\n 南北纬:" + (southLatitude?"南纬":"北纬") + + "\n 东西经:" + (wesLongitude?"西经":"东经") + + "\n 运营状态:" + (outage?"停运":"运营") + + "\n 经纬度保密:" + (positionEncryption?"加密":"未加密") + + "\n 前撞预警:" + (warningFrontCrash?"紧急刹车系统采集的前撞预警":"无") + + "\n 车道偏移预警:" + (warningShifting?"车道偏移预警":"无") + + "\n 空/半/满载状态:" + (load == 0?"空车":(load == 1?"半载":(load == 3?"满载":"未定义状态"))) + + "\n 车辆油路状态:" + (oilWayBreak?"车辆油路断开":"车辆油路正常") + + "\n 车辆电路状态:" + (circuitBreak?"车辆电路断开":"车辆电路正常") + + "\n 门锁状态:" + (doorLocking?"车门加锁":"车门解锁") + + "\n 门1(前门)状态:" + (door1Open?"开":"关") + + "\n 门2(中门)状态:" + (door2Open?"开":"关") + + "\n 门3(后门)状态:" + (door3Open?"开":"关") + + "\n 门4(驾驶席门)状态:" + (door4Open?"开":"关") + + "\n 门5(自定义)状态:" + (door5Open?"开":"关") + + "\n GPS卫星定位状态: " + (gps?"使用":"未使用") + + "\n 北斗卫星定位状态: " + (beidou?"使用":"未使用") + + "\n GLONASS卫星定位状态: " + (glonass?"使用":"未使用") + + "\n GaLiLeo卫星定位状态: " + (gaLiLeo?"使用":"未使用") + + "\n GaLiLeo卫星定位状态: " + (gaLiLeo?"使用":"未使用") + + "\n 车辆行驶状态: " + (driving?"车辆行驶":"车辆停止") + + "\n "; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTTextSign.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTTextSign.java new file mode 100644 index 0000000..4bbcaf4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTTextSign.java @@ -0,0 +1,45 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 文本信息标志 + */ +@Data +@Schema(description = "文本信息标志") +public class JTTextSign { + + @Schema(description = "1紧急,2服务,3通知") + private int type; + + @Schema(description = "1终端显示器显示") + private boolean terminalDisplay; + + @Schema(description = "1广告屏显示") + private boolean adScreen; + + @Schema(description = "1终端 TTS 播读") + private boolean tts; + + @Schema(description = "false: 中心导航信息 true CAN故障码信息") + private boolean source; + + public byte encode(){ + byte byteSign = 0; + byteSign |= (byte) type; + if (terminalDisplay) { + byteSign |= (0x1 << 2); + } + if (tts) { + byteSign |= (0x1 << 3); + } + if (adScreen) { + byteSign |= (0x1 << 4); + } + if (source) { + byteSign |= (0x1 << 5); + } + return byteSign; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTVehicleControl.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTVehicleControl.java new file mode 100644 index 0000000..41fd574 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTVehicleControl.java @@ -0,0 +1,33 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.bean.common.ConfigAttribute; +import lombok.Getter; +import lombok.Setter; + +import java.util.Objects; + +/** + * 车辆控制类型 + */ +@Setter +@Getter +public class JTVehicleControl { + + private int length; + + private void setLength(Object value) { + if (Objects.isNull(value)) { + length--; + }else { + length ++; + } + } + + @ConfigAttribute(id = 0X0001, type="Byte", description = "车门, 0:车门锁闭 1:车门开启") + private Integer controlCarDoor; + + public void setControlCarDoor(Integer controlCarDoor) { + this.controlCarDoor = controlCarDoor; + setLength(controlCarDoor); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTVideoAlarm.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTVideoAlarm.java new file mode 100644 index 0000000..e55f5f6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTVideoAlarm.java @@ -0,0 +1,92 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +@Schema(description = "视频报警上报") +public class JTVideoAlarm { + + @Schema(description = "视频信号丢失报警的通道") + private List videoLossChannels; + + @Schema(description = "视频信号遮挡报警的通道") + private List videoOcclusionChannels; + + @Schema(description = "存储器故障报警状态,第 1-12 个主存储器,12-15 分别表示第 1-4 个灾备存储装置") + private List storageFaultAlarm; + + @Schema(description = "异常驾驶行为-疲劳") + private boolean drivingForFatigue; + + @Schema(description = "异常驾驶行为-打电话") + private boolean drivingForCall; + + @Schema(description = "异常驾驶行为-抽烟") + private boolean drivingSmoking; + + @Schema(description = "其他视频设备故障") + private boolean otherDeviceFailure; + + @Schema(description = "客车超员报警") + private boolean overcrowding; + + @Schema(description = "特殊报警录像达到存储阈值报警") + private boolean specialRecordFull; + + public JTVideoAlarm() { + } + + public static JTVideoAlarm getInstance(int alarm, int loss, int occlusion, short storageFault, short driving) { + JTVideoAlarm jtVideoAlarm = new JTVideoAlarm(); + if (alarm == 0) { + return jtVideoAlarm; + } + boolean lossAlarm = (alarm & 1) == 1; + boolean occlusionAlarm = (alarm >>> 1 & 1) == 1; + boolean storageFaultAlarm = (alarm >>> 2 & 1) == 1; + jtVideoAlarm.setOtherDeviceFailure((alarm >>> 3 & 1) == 1); + jtVideoAlarm.setOvercrowding((alarm >>> 4 & 1) == 1); + boolean drivingAlarm = (alarm >>> 5 & 1) == 1; + jtVideoAlarm.setSpecialRecordFull((alarm >>> 6 & 1) == 1); + if (lossAlarm && loss > 0) { + List videoLossChannels = new ArrayList<>(); + for (int i = 0; i < 32; i++) { + if ((loss >>> i & 1) == 1 ) { + videoLossChannels.add(i); + } + } + jtVideoAlarm.setVideoLossChannels(videoLossChannels); + } + if (occlusionAlarm && occlusion > 0) { + List videoOcclusionChannels = new ArrayList<>(); + for (int i = 0; i < 32; i++) { + if ((occlusion >>> i & 1) == 1) { + videoOcclusionChannels.add(i); + } + } + jtVideoAlarm.setVideoOcclusionChannels(videoOcclusionChannels); + } + if (storageFaultAlarm && storageFault > 0) { + List storageFaultAlarmContent = new ArrayList<>(); + for (int i = 0; i < 16; i++) { + if ((storageFault >>> i & 1) == 1) { + storageFaultAlarmContent.add(i); + } + } + jtVideoAlarm.setStorageFaultAlarm(storageFaultAlarmContent); + } + if (drivingAlarm && driving > 0) { + jtVideoAlarm.setDrivingForFatigue((driving & 1) == 1 ); + jtVideoAlarm.setDrivingForCall((driving >>> 1 & 1) == 1 ); + jtVideoAlarm.setDrivingSmoking((driving >>> 2 & 1) == 1 ); + } + return jtVideoAlarm; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/common/ConfigAttribute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/common/ConfigAttribute.java new file mode 100644 index 0000000..c6dde81 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/common/ConfigAttribute.java @@ -0,0 +1,14 @@ +package com.genersoft.iot.vmp.jt1078.bean.common; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface ConfigAttribute { + + long id(); + + String type(); + + String description(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAlarmRecordingParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAlarmRecordingParam.java new file mode 100644 index 0000000..77d79ca --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAlarmRecordingParam.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 特殊报警录像参数 + */ +@Setter +@Getter +public class JTAlarmRecordingParam implements JTDeviceSubConfig{ + + /** + * 特殊报警录像存储阈值, 百分比,取值 特殊报警录像占用主存储器存储阈值百 1 ~ 99,默认值为 20 + */ + private int storageLimit; + + /** + * 特殊报警录像持续时间,特殊报警录像的最长持续时间,单位为分钟(min) ,默认值为 5 + */ + private int duration; + + /** + * 特殊报警标识起始时间, 特殊报警发生前进行标记的录像时间, 单位为分钟( min) ,默认值为 1 + */ + private int startTime; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(storageLimit); + byteBuf.writeByte(duration); + byteBuf.writeByte(startTime); + return byteBuf; + } + + public static JTAlarmRecordingParam decode(ByteBuf byteBuf) { + JTAlarmRecordingParam alarmRecordingParam = new JTAlarmRecordingParam(); + alarmRecordingParam.setStorageLimit(byteBuf.readUnsignedByte()); + alarmRecordingParam.setDuration(byteBuf.readUnsignedByte()); + alarmRecordingParam.setStartTime(byteBuf.readUnsignedByte()); + return alarmRecordingParam; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAloneChanel.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAloneChanel.java new file mode 100644 index 0000000..22ceff3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAloneChanel.java @@ -0,0 +1,136 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 单独通道视频 + */ +@Setter +@Getter +public class JTAloneChanel implements JTDeviceSubConfig{ + + /** + * 逻辑通道号 + */ + private int logicChannelId; + + /** + * 实时流编码模式 + * 0:CBR( 固定码率) ; + * 1:VBR( 可变码率) ; + * 2:ABR( 平均码率) ; + * 100 ~ 127:自定义 + */ + private int liveStreamCodeRateType; + + /** + * 实时流分辨率 + * 0:QCIF; + * 1:CIF; + * 2:WCIF; + * 3:D1; + * 4:WD1; + * 5:720P; + * 6:1 080P; + * 100 ~ 127:自定义 + */ + private int liveStreamResolving; + + /** + * 实时流关键帧间隔, 范围(1 ~ 1 000) 帧 + */ + private int liveStreamIInterval; + + /** + * 实时流目标帧率,范围(1 ~ 120) 帧 / s + */ + private int liveStreamFrameRate; + + /** + * 实时流目标码率,单位为千位每秒( kbps) + */ + private long liveStreamCodeRate; + + + /** + * 存储流编码模式 + * 0:CBR( 固定码率) ; + * 1:VBR( 可变码率) ; + * 2:ABR( 平均码率) ; + * 100 ~ 127:自定义 + */ + private int storageStreamCodeRateType; + + /** + * 存储流分辨率 + * 0:QCIF; + * 1:CIF; + * 2:WCIF; + * 3:D1; + * 4:WD1; + * 5:720P; + * 6:1 080P; + * 100 ~ 127:自定义 + */ + private int storageStreamResolving; + + /** + * 存储流关键帧间隔, 范围(1 ~ 1 000) 帧 + */ + private int storageStreamIInterval; + + /** + * 存储流目标帧率,范围(1 ~ 120) 帧 / s + */ + private int storageStreamFrameRate; + + /** + * 存储流目标码率,单位为千位每秒( kbps) + */ + private long storageStreamCodeRate; + + /** + * 字幕叠加设置 + */ + private JTOSDConfig osd; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(logicChannelId); + byteBuf.writeByte(liveStreamCodeRateType); + byteBuf.writeByte(liveStreamResolving); + byteBuf.writeShort((short)(liveStreamIInterval & 0xffff)); + byteBuf.writeByte(liveStreamFrameRate); + byteBuf.writeInt((int) (liveStreamCodeRate & 0xffffffffL)); + + byteBuf.writeByte(storageStreamCodeRateType); + byteBuf.writeByte(storageStreamResolving); + byteBuf.writeShort((short)(storageStreamIInterval & 0xffff)); + byteBuf.writeByte(storageStreamFrameRate); + byteBuf.writeInt((int) (storageStreamCodeRate & 0xffffffffL)); + byteBuf.writeBytes(osd.encode()); + return byteBuf; + } + + public static JTAloneChanel decode(ByteBuf buf) { + JTAloneChanel jtAloneChanel = new JTAloneChanel(); + jtAloneChanel.setLogicChannelId(buf.readByte()); + jtAloneChanel.setLiveStreamCodeRateType(buf.readByte()); + jtAloneChanel.setLiveStreamResolving(buf.readByte()); + jtAloneChanel.setLiveStreamIInterval(buf.readUnsignedShort()); + jtAloneChanel.setLiveStreamFrameRate(buf.readByte()); + jtAloneChanel.setLiveStreamCodeRate(buf.readUnsignedInt()); + + jtAloneChanel.setStorageStreamCodeRateType(buf.readByte()); + jtAloneChanel.setStorageStreamResolving(buf.readByte()); + jtAloneChanel.setStorageStreamIInterval(buf.readUnsignedShort()); + jtAloneChanel.setStorageStreamFrameRate(buf.readByte()); + jtAloneChanel.setStorageStreamCodeRate(buf.readUnsignedInt()); + jtAloneChanel.setOsd(JTOSDConfig.decode(buf)); + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAnalyzeAlarmParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAnalyzeAlarmParam.java new file mode 100644 index 0000000..6e97f1f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAnalyzeAlarmParam.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 视频分析报警参数 + */ +@Setter +@Getter +public class JTAnalyzeAlarmParam implements JTDeviceSubConfig{ + + /** + * 车辆核载人数 + */ + private int numberForPeople; + + + /** + * 疲劳程度阈值 + */ + private int fatigueThreshold; + + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(numberForPeople); + byteBuf.writeByte(fatigueThreshold); + return byteBuf; + } + + public static JTAnalyzeAlarmParam decode(ByteBuf byteBuf) { + JTAnalyzeAlarmParam analyzeAlarmParam = new JTAnalyzeAlarmParam(); + analyzeAlarmParam.setNumberForPeople(byteBuf.readUnsignedByte()); + analyzeAlarmParam.setFatigueThreshold(byteBuf.readUnsignedByte()); + return analyzeAlarmParam; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAwakenParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAwakenParam.java new file mode 100644 index 0000000..53850b9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAwakenParam.java @@ -0,0 +1,270 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 终端休眠唤醒模式设置 + */ +@Setter +@Getter +public class JTAwakenParam implements JTDeviceSubConfig{ + + /** + * 休眠唤醒模式-条件唤醒 + */ + private boolean wakeUpModeByCondition; + + /** + * 休眠唤醒模式-定时唤醒 + */ + private boolean wakeUpModeByTime; + + /** + * 休眠唤醒模式-手动唤醒 + */ + private boolean wakeUpModeByManual; + + /** + * 唤醒条件类型-紧急报警 + */ + private boolean wakeUpConditionsByAlarm; + + /** + * 唤醒条件类型-碰撞侧翻报警 + */ + private boolean wakeUpConditionsByRollover; + + /** + * 唤醒条件类型-车辆开门 + */ + private boolean wakeUpConditionsByOpenTheDoor; + + /** + * 定时唤醒日设置-周一 + */ + private boolean awakeningDayForMonday; + + /** + * 定时唤醒日设置-周二 + */ + private boolean awakeningDayForTuesday; + + /** + * 定时唤醒日设置-周三 + */ + private boolean awakeningDayForWednesday; + + /** + * 定时唤醒日设置-周四 + */ + private boolean awakeningDayForThursday; + + /** + * 定时唤醒日设置-周五 + */ + private boolean awakeningDayForFriday; + + /** + * 定时唤醒日设置-周六 + */ + private boolean awakeningDayForSaturday; + + /** + * 定时唤醒日设置-周日 + */ + private boolean awakeningDayForSunday; + + /** + * 日定时唤醒-启用时间段1 + */ + private boolean time1Enable; + + /** + * 日定时唤醒-时间段1开始时间 + */ + private String time1StartTime; + + /** + * 日定时唤醒-时间段1结束时间 + */ + private String time1EndTime; + + /** + * 日定时唤醒-启用时间段2 + */ + private boolean time2Enable; + + /** + * 日定时唤醒-时间段2开始时间 + */ + private String time2StartTime; + + /** + * 日定时唤醒-时间段2结束时间 + */ + private String time2EndTime; + + /** + * 日定时唤醒-启用时间段3 + */ + private boolean time3Enable; + + /** + * 日定时唤醒-时间段3开始时间 + */ + private String time3StartTime; + + /** + * 日定时唤醒-时间段3结束时间 + */ + private String time3EndTime; + + /** + * 日定时唤醒-启用时间段4 + */ + private boolean time4Enable; + + /** + * 日定时唤醒-时间段4开始时间 + */ + private String time4StartTime; + + /** + * 日定时唤醒-时间段4结束时间 + */ + private String time4EndTime; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byte wakeUpTypeByte = 0; + byte wakeUpConditionsByte = 0; + byte wakeDayByte = 0; + if (wakeUpModeByCondition) { + wakeUpTypeByte = (byte)(wakeUpTypeByte | 1); + } + if (wakeUpModeByTime) { + wakeUpTypeByte = (byte)(wakeUpTypeByte | (1 << 1)); + } + if (wakeUpModeByManual) { + wakeUpTypeByte = (byte)(wakeUpTypeByte | (1 << 2)); + } + byteBuf.writeByte(wakeUpTypeByte); + if (wakeUpConditionsByAlarm) { + wakeUpConditionsByte = (byte)(wakeUpConditionsByte | 1); + } + if (wakeUpConditionsByRollover) { + wakeUpConditionsByte = (byte)(wakeUpConditionsByte | (1 << 1)); + } + if (wakeUpConditionsByOpenTheDoor) { + wakeUpConditionsByte = (byte)(wakeUpConditionsByte | (1 << 2)); + } + byteBuf.writeByte(wakeUpConditionsByte); + if (awakeningDayForMonday) { + wakeDayByte = (byte)(wakeDayByte | 1); + } + if (awakeningDayForTuesday) { + wakeDayByte = (byte)(wakeDayByte | (1 << 1)); + } + if (awakeningDayForWednesday) { + wakeDayByte = (byte)(wakeDayByte | (1 << 2)); + } + if (awakeningDayForThursday) { + wakeDayByte = (byte)(wakeDayByte | (1 << 3)); + } + if (awakeningDayForFriday) { + wakeDayByte = (byte)(wakeDayByte | (1 << 4)); + } + if (awakeningDayForSaturday) { + wakeDayByte = (byte)(wakeDayByte | (1 << 5)); + } + if (awakeningDayForSunday) { + wakeDayByte = (byte)(wakeDayByte | (1 << 6)); + } + byteBuf.writeByte(wakeDayByte); + byte enableByte = 0; + if (time1Enable) { + enableByte = (byte)(enableByte | 1); + } + if (time2Enable) { + enableByte = (byte)(enableByte | (1 << 1)); + } + if (time3Enable) { + enableByte = (byte)(enableByte | (1 << 2)); + } + if (time4Enable) { + enableByte = (byte)(enableByte | (1 << 3)); + } + byteBuf.writeByte(enableByte); + byteBuf.writeBytes(transportTime(time1StartTime)); + byteBuf.writeBytes(transportTime(time1EndTime)); + byteBuf.writeBytes(transportTime(time2StartTime)); + byteBuf.writeBytes(transportTime(time2EndTime)); + byteBuf.writeBytes(transportTime(time3StartTime)); + byteBuf.writeBytes(transportTime(time3EndTime)); + byteBuf.writeBytes(transportTime(time4StartTime)); + byteBuf.writeBytes(transportTime(time4EndTime)); + return byteBuf; + } + + private byte[] transportTime(String time) { + return BCDUtil.strToBcd(time.replace(":", "")); + } + + public static JTAwakenParam decode(ByteBuf byteBuf) { + JTAwakenParam awakenParam = new JTAwakenParam(); + short wakeUpTypeByte = byteBuf.readUnsignedByte(); + awakenParam.wakeUpModeByCondition = ((wakeUpTypeByte & 1) == 1); + awakenParam.wakeUpModeByTime = ((wakeUpTypeByte >>> 1 & 1) == 1); + awakenParam.wakeUpModeByManual = ((wakeUpTypeByte >>> 2 & 1) == 1); + + short wakeUpConditionsByte = byteBuf.readUnsignedByte(); + awakenParam.wakeUpConditionsByAlarm = ((wakeUpConditionsByte & 1) == 1); + awakenParam.wakeUpConditionsByRollover = ((wakeUpConditionsByte >>> 1 & 1) == 1); + awakenParam.wakeUpConditionsByOpenTheDoor = ((wakeUpConditionsByte >>> 2 & 1) == 1); + + short wakeDayByte = byteBuf.readUnsignedByte(); + awakenParam.awakeningDayForMonday = ((wakeDayByte & 1) == 1); + awakenParam.awakeningDayForTuesday = ((wakeDayByte >>> 1 & 1) == 1); + awakenParam.awakeningDayForWednesday = ((wakeDayByte >>> 2 & 1) == 1); + awakenParam.awakeningDayForThursday = ((wakeDayByte >>> 3 & 1) == 1); + awakenParam.awakeningDayForFriday = ((wakeDayByte >>> 4 & 1) == 1); + awakenParam.awakeningDayForSaturday = ((wakeDayByte >>> 5 & 1) == 1); + awakenParam.awakeningDayForSunday = ((wakeDayByte >>> 6 & 1) == 1); + short enableByte = byteBuf.readUnsignedByte(); + awakenParam.time1Enable = ((enableByte & 1) == 1); + awakenParam.time2Enable = ((enableByte >>> 1 & 1) == 1); + awakenParam.time3Enable = ((enableByte >>> 2 & 1) == 1); + awakenParam.time4Enable = ((enableByte >>> 3 & 1) == 1); + byte[] timeBytes = new byte[2]; + byteBuf.readBytes(timeBytes); + awakenParam.time1StartTime = transportTime(timeBytes); + byteBuf.readBytes(timeBytes); + awakenParam.time1EndTime = transportTime(timeBytes); + + byteBuf.readBytes(timeBytes); + awakenParam.time2StartTime = transportTime(timeBytes); + byteBuf.readBytes(timeBytes); + awakenParam.time2EndTime = transportTime(timeBytes); + + byteBuf.readBytes(timeBytes); + awakenParam.time3StartTime = transportTime(timeBytes); + byteBuf.readBytes(timeBytes); + awakenParam.time3EndTime = transportTime(timeBytes); + + byteBuf.readBytes(timeBytes); + awakenParam.time4StartTime = transportTime(timeBytes); + byteBuf.readBytes(timeBytes); + awakenParam.time4EndTime = transportTime(timeBytes); + return awakenParam; + } + + private static String transportTime(byte[] timeBytes) { + String time1Str = BCDUtil.transform(timeBytes); + return time1Str.replace(time1Str.substring(0, 2), time1Str.substring(0, 2) + ":"); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTCameraTimer.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTCameraTimer.java new file mode 100644 index 0000000..cfe4301 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTCameraTimer.java @@ -0,0 +1,113 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 定时拍照控制 + */ +@Setter +@Getter +public class JTCameraTimer implements JTDeviceSubConfig{ + /** + * 摄像通道1 定时拍照开关标志 + */ + private boolean switchForChannel1; + /** + * 摄像通道2 定时拍照开关标志 + */ + private boolean switchForChannel2; + /** + * 摄像通道3 定时拍照开关标志 + */ + private boolean switchForChannel3; + /** + * 摄像通道4 定时拍照开关标志 + */ + private boolean switchForChannel4; + /** + * 摄像通道5 定时拍照开关标志 + */ + private boolean switchForChannel5; + + /** + * 摄像通道1 定时拍照存储标志, true: 上传, false: 存储 + */ + private boolean storageFlagsForChannel1; + + /** + * 摄像通道2 定时拍照存储标志 true: 上传, false: 存储 + */ + private boolean storageFlagsForChannel2; + + /** + * 摄像通道3 定时拍照存储标志 true: 上传, false: 存储 + */ + private boolean storageFlagsForChannel3; + + /** + * 摄像通道4 定时拍照存储标志 true: 上传, false: 存储 + */ + private boolean storageFlagsForChannel4; + + /** + * 摄像通道5 定时拍照存储标志 true: 上传, false: 存储 + */ + private boolean storageFlagsForChannel5; + + /** + * 定时时间单位,true: 分, false: 秒,当数值小于5s时,终端按5s处理 + */ + private boolean timeUnit; + + /** + * 定时时间间隔 + */ + private Integer timeInterval; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byte[] bytes = new byte[4]; + bytes[0] = 0; + if (switchForChannel1) { + bytes[0] = (byte)(bytes[0] | 1); + } + if (switchForChannel2) { + bytes[0] = (byte)(bytes[0] | 2); + } + if (switchForChannel3) { + bytes[0] = (byte)(bytes[0] | 4); + } + if (switchForChannel4) { + bytes[0] = (byte)(bytes[0] | 8); + } + if (switchForChannel5) { + bytes[0] = (byte)(bytes[0] | 16); + } + bytes[1] = 0; + if (storageFlagsForChannel1) { + bytes[1] = (byte)(bytes[1] | 1); + } + if (storageFlagsForChannel2) { + bytes[1] = (byte)(bytes[1] | 2); + } + if (storageFlagsForChannel3) { + bytes[1] = (byte)(bytes[1] | 4); + } + if (storageFlagsForChannel4) { + bytes[1] = (byte)(bytes[1] | 8); + } + if (storageFlagsForChannel5) { + bytes[1] = (byte)(bytes[1] | 16); + } + bytes[3] = (byte)(timeInterval & 0xfe); + if (timeUnit) { + bytes[3] = (byte)(bytes[3] | 1); + } + byteBuf.writeBytes(bytes); + return byteBuf; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChanelConfig.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChanelConfig.java new file mode 100644 index 0000000..860dc82 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChanelConfig.java @@ -0,0 +1,56 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 音视频通道 + */ +@Setter +@Getter +public class JTChanelConfig implements JTDeviceSubConfig{ + + /** + * 物理通道号 单独 + */ + private int physicalChannelId; + + /** + * 逻辑通道号 + */ + private int logicChannelId; + + /** + * 通道类型: + * 0:音视频; + * 1:音频 + * 2:视频 + */ + private int channelType; + /** + * 是否连接云台: 通道类型为 0 和 2 时,此字段有效 + * 0:未连接;1:连接 + */ + private int ptzEnable; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(physicalChannelId); + byteBuf.writeByte(logicChannelId); + byteBuf.writeByte(channelType); + byteBuf.writeByte(ptzEnable); + return byteBuf; + } + + public static JTChanelConfig decode(ByteBuf byteBuf) { + JTChanelConfig jtChanel = new JTChanelConfig(); + jtChanel.setPhysicalChannelId(byteBuf.readUnsignedByte()); + jtChanel.setLogicChannelId(byteBuf.readUnsignedByte()); + jtChanel.setChannelType(byteBuf.readUnsignedByte()); + jtChanel.setPtzEnable(byteBuf.readUnsignedByte()); + return jtChanel; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChannelListParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChannelListParam.java new file mode 100644 index 0000000..ff03faa --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChannelListParam.java @@ -0,0 +1,61 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +/** + * 音视频通道列表设置 + */ +@Setter +@Getter +public class JTChannelListParam implements JTDeviceSubConfig{ + + /** + * 音视频通道总数 + */ + private int videoAndAudioCount; + + /** + * 音频通道总数 + */ + private int audioCount; + + /** + * 视频通道总数 + */ + private int videoCount; + + private List chanelList; + + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(videoAndAudioCount); + byteBuf.writeByte(audioCount); + byteBuf.writeByte(videoCount); + for (JTChanelConfig jtChanel : chanelList) { + byteBuf.writeBytes(jtChanel.encode()); + } + return byteBuf; + } + + public static JTChannelListParam decode(ByteBuf byteBuf) { + JTChannelListParam channelListParam = new JTChannelListParam(); + channelListParam.setVideoAndAudioCount(byteBuf.readUnsignedByte()); + channelListParam.setAudioCount(byteBuf.readUnsignedByte()); + channelListParam.setVideoCount(byteBuf.readUnsignedByte()); + int total = channelListParam.getVideoAndAudioCount() + channelListParam.getVideoCount() + channelListParam.getAudioCount(); + List chanelList = new ArrayList<>(total); + for (int i = 0; i < total; i++) { + chanelList.add(JTChanelConfig.decode(byteBuf)); + } + channelListParam.setChanelList(chanelList); + return channelListParam; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChannelParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChannelParam.java new file mode 100644 index 0000000..7efc710 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChannelParam.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +/** + * 单独视频通道参数设置 + */ +@Setter +@Getter +public class JTChannelParam implements JTDeviceSubConfig { + + /** + * 单独通道视频参数设置列表 + */ + private List jtAloneChanelList; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(jtAloneChanelList.size()); + for (JTAloneChanel jtAloneChanel : jtAloneChanelList) { + if (jtAloneChanel == null) { + continue; + } + byteBuf.writeBytes(jtAloneChanel.encode()); + } + return byteBuf; + } + + public static JTChannelParam decode(ByteBuf byteBuf) { + JTChannelParam channelParam = new JTChannelParam(); + int length = byteBuf.readUnsignedByte(); + List jtAloneChanelList = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + jtAloneChanelList.add(JTAloneChanel.decode(byteBuf)); + } + channelParam.setJtAloneChanelList(jtAloneChanelList); + return channelParam; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTCollisionAlarmParams.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTCollisionAlarmParams.java new file mode 100644 index 0000000..6d8c609 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTCollisionAlarmParams.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 碰撞报警参数设置 + */ +@Setter +@Getter +public class JTCollisionAlarmParams implements JTDeviceSubConfig{ + + /** + * 碰撞时间 单位为毫秒(ms) + */ + private int collisionAlarmTime; + + /** + * 碰撞加速度 单位为0.1g,设置范围为0~79,默认为10 + */ + private int collisionAcceleration; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byte[] bytes = new byte[2]; + bytes[0] = (byte) (collisionAlarmTime & 0xff); + bytes[1] = (byte) (collisionAcceleration & 0xff); + byteBuf.writeBytes(bytes); + return byteBuf; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTDeviceSubConfig.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTDeviceSubConfig.java new file mode 100644 index 0000000..96d3015 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTDeviceSubConfig.java @@ -0,0 +1,7 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; + +public interface JTDeviceSubConfig { + ByteBuf encode(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTGnssPositioningMode.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTGnssPositioningMode.java new file mode 100644 index 0000000..79b9e70 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTGnssPositioningMode.java @@ -0,0 +1,52 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * GNSS 定位模式 + */ +@Setter +@Getter +public class JTGnssPositioningMode implements JTDeviceSubConfig{ + + /** + * GPS 定位 true: 开启, false: 关闭 + */ + private boolean gps; + /** + * 北斗定位 true: 开启, false: 关闭 + */ + private boolean beidou; + /** + * GLONASS定位 true: 开启, false: 关闭 + */ + private boolean glonass; + /** + * GaLiLeo定位 true: 开启, false: 关闭 + */ + private boolean gaLiLeo; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byte[] bytes = new byte[1]; + bytes[0] = 0; + if (gps) { + bytes[0] = (byte)(bytes[0] | 1); + } + if (beidou) { + bytes[0] = (byte)(bytes[0] | 2); + } + if (glonass) { + bytes[0] = (byte)(bytes[0] | 4); + } + if (gaLiLeo) { + bytes[0] = (byte)(bytes[0] | 8); + } + byteBuf.writeBytes(bytes); + return byteBuf; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTIllegalDrivingPeriods.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTIllegalDrivingPeriods.java new file mode 100644 index 0000000..297a488 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTIllegalDrivingPeriods.java @@ -0,0 +1,37 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 违规行驶时段范围 ,精确到分 + */ +@Setter +@Getter +public class JTIllegalDrivingPeriods implements JTDeviceSubConfig{ + /** + * 违规行驶时段-开始时间 HH:mm + */ + private String startTime; + + /** + * 违规行驶时段-结束时间 HH:mm + */ + private String endTime; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byte[] bytes = new byte[4]; + String[] startTimeArray = startTime.split(":"); + String[] endTimeArray = endTime.split(":"); + bytes[0] = (byte)Integer.parseInt(startTimeArray[0]); + bytes[1] = (byte)Integer.parseInt(startTimeArray[1]); + bytes[2] = (byte)Integer.parseInt(endTimeArray[0]); + bytes[3] = (byte)Integer.parseInt(endTimeArray[1]); + byteBuf.writeBytes(bytes); + return byteBuf; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTOSDConfig.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTOSDConfig.java new file mode 100644 index 0000000..2bfde12 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTOSDConfig.java @@ -0,0 +1,92 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * OSD字幕叠加设置 + */ +@Setter +@Getter +public class JTOSDConfig { + + /** + * 日期和时间 + */ + private boolean time; + + /** + * 车牌号码 + */ + private boolean licensePlate; + + /** + * 逻辑通道号 + */ + private boolean channelId; + + /** + * 经纬度 + */ + private boolean position; + + /** + * 行驶记录速度 + */ + private boolean speed; + + /** + * 卫星定位速度 + */ + private boolean speedForGPS; + + /** + * 连续驾驶时间 + */ + private boolean drivingTime; + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + byte content = 0; + if (time) { + content = (byte)(content | 1); + } + if (licensePlate) { + content = (byte)(content | (1 << 1)); + } + if (channelId) { + content = (byte)(content | (1 << 2)); + } + if (position) { + content = (byte)(content | (1 << 3)); + } + if (speed) { + content = (byte)(content | (1 << 4)); + } + if (speedForGPS) { + content = (byte)(content | (1 << 5)); + } + if (drivingTime) { + content = (byte)(content | (1 << 6)); + } + byteBuf.writeByte(content); + byteBuf.writeByte(0); + return byteBuf; + + } + + public static JTOSDConfig decode(ByteBuf buf) { + JTOSDConfig config = new JTOSDConfig(); + int content = buf.readUnsignedShort(); + config.setTime((content & 1) == 1); + config.setLicensePlate((content >>> 1 & 1) == 1); + config.setChannelId((content >>> 2 & 1) == 1); + config.setPosition((content >>> 3 & 1) == 1); + config.setSpeed((content >>> 4 & 1) == 1); + config.setSpeedForGPS((content >>> 5 & 1) == 1); + config.setDrivingTime((content >>> 6 & 1) == 1); + return config; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTVideoAlarmBit.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTVideoAlarmBit.java new file mode 100644 index 0000000..27a52b3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTVideoAlarmBit.java @@ -0,0 +1,143 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +/** + * 视频报警标志位 + */ +public class JTVideoAlarmBit implements JTDeviceSubConfig{ + + /** + * 视频信号丢失报警 + */ + private boolean lossSignal; + /** + * 视频信号遮挡报警 + */ + private boolean occlusionSignal; + /** + * 存储单元故障报警 + */ + private boolean storageFault; + /** + * 其他视频设备故障报警 + */ + private boolean otherDeviceFailure; + /** + * 客车超员报警 + */ + private boolean overcrowding; + /** + * 异常驾驶行为报警 + */ + private boolean abnormalDriving; + /** + * 特殊报警录像达到存储阈值报警 + */ + private boolean storageLimit; + + public boolean isLossSignal() { + return lossSignal; + } + + public void setLossSignal(boolean lossSignal) { + this.lossSignal = lossSignal; + } + + public boolean isOcclusionSignal() { + return occlusionSignal; + } + + public void setOcclusionSignal(boolean occlusionSignal) { + this.occlusionSignal = occlusionSignal; + } + + public boolean isStorageFault() { + return storageFault; + } + + public void setStorageFault(boolean storageFault) { + this.storageFault = storageFault; + } + + public boolean isOtherDeviceFailure() { + return otherDeviceFailure; + } + + public void setOtherDeviceFailure(boolean otherDeviceFailure) { + this.otherDeviceFailure = otherDeviceFailure; + } + + public boolean isOvercrowding() { + return overcrowding; + } + + public void setOvercrowding(boolean overcrowding) { + this.overcrowding = overcrowding; + } + + public boolean isAbnormalDriving() { + return abnormalDriving; + } + + public void setAbnormalDriving(boolean abnormalDriving) { + this.abnormalDriving = abnormalDriving; + } + + public boolean isStorageLimit() { + return storageLimit; + } + + public void setStorageLimit(boolean storageLimit) { + this.storageLimit = storageLimit; + } + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byte content = 0; + if (lossSignal) { + content = content |= 1; + } + if (occlusionSignal) { + content = content |= (1 << 1); + } + if (storageFault) { + content = content |= (1 << 2); + } + if (otherDeviceFailure) { + content = content |= (1 << 3); + } + if (overcrowding) { + content = content |= (1 << 4); + } + if (abnormalDriving) { + content = content |= (1 << 5); + } + if (storageLimit) { + content = content |= (1 << 6); + } + byteBuf.writeByte(content); + byteBuf.writeByte(0); + byteBuf.writeByte(0); + byteBuf.writeByte(0); + return byteBuf; + } + + public static JTVideoAlarmBit decode(ByteBuf byteBuf) { + JTVideoAlarmBit videoAlarmBit = new JTVideoAlarmBit(); + byte content = byteBuf.readByte(); + videoAlarmBit.setLossSignal((content & 1) == 1); + videoAlarmBit.setOcclusionSignal((content >>> 1 & 1) == 1); + videoAlarmBit.setStorageFault((content >>> 2 & 1) == 1); + videoAlarmBit.setOtherDeviceFailure((content >>> 3 & 1) == 1); + videoAlarmBit.setOvercrowding((content >>> 4 & 1) == 1); + videoAlarmBit.setAbnormalDriving((content >>> 5 & 1) == 1); + videoAlarmBit.setStorageLimit((content >>> 6 & 1) == 1); + byteBuf.readByte(); + byteBuf.readByte(); + byteBuf.readByte(); + return videoAlarmBit; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTVideoParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTVideoParam.java new file mode 100644 index 0000000..098e7e6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTVideoParam.java @@ -0,0 +1,133 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Data; + +/** + * 违规行驶时段范围 ,精确到分 + */ +@Data +public class JTVideoParam implements JTDeviceSubConfig{ + /** + * 实时流编码模式 + * 0:CBR( 固定码率) ; + * 1:VBR( 可变码率) ; + * 2:ABR( 平均码率) ; + * 100 ~ 127:自定义 + */ + private int liveStreamCodeRateType; + + /** + * 实时流分辨率 + * 0:QCIF; + * 1:CIF; + * 2:WCIF; + * 3:D1; + * 4:WD1; + * 5:720P; + * 6:1 080P; + * 100 ~ 127:自定义 + */ + private int liveStreamResolving; + + /** + * 实时流关键帧间隔, 范围(1 ~ 1 000) 帧 + */ + private int liveStreamIInterval; + + /** + * 实时流目标帧率,范围(1 ~ 120) 帧 / s + */ + private int liveStreamFrameRate; + + /** + * 实时流目标码率,单位为千位每秒( kbps) + */ + private long liveStreamCodeRate; + + + /** + * 存储流编码模式 + * 0:CBR( 固定码率) + * 1:VBR( 可变码率) + * 2:ABR( 平均码率) + * 100 ~ 127:自定义 + */ + private int storageStreamCodeRateType; + + /** + * 存储流分辨率 + * 0:QCIF; + * 1:CIF; + * 2:WCIF; + * 3:D1; + * 4:WD1; + * 5:720P; + * 6:1 080P; + * 100 ~ 127:自定义 + */ + private int storageStreamResolving; + + /** + * 存储流关键帧间隔, 范围(1 ~ 1 000) 帧 + */ + private int storageStreamIInterval; + + /** + * 存储流目标帧率,范围(1 ~ 120) 帧 / s + */ + private int storageStreamFrameRate; + + /** + * 存储流目标码率,单位为千位每秒( kbps) + */ + private long storageStreamCodeRate; + + /** + * 字幕叠加设置 + */ + private JTOSDConfig osd; + + /** + * 是否启用音频输出, 0:不启用;1:启用 + */ + private int audioEnable; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(liveStreamCodeRateType); + byteBuf.writeByte(liveStreamResolving); + byteBuf.writeShort((short)(liveStreamIInterval & 0xffff)); + byteBuf.writeByte(liveStreamFrameRate); + byteBuf.writeInt((int) (liveStreamCodeRate & 0xffffffffL)); + + byteBuf.writeByte(storageStreamCodeRateType); + byteBuf.writeByte(storageStreamResolving); + byteBuf.writeShort((short)(storageStreamIInterval & 0xffff)); + byteBuf.writeByte(storageStreamFrameRate); + byteBuf.writeInt((int) (storageStreamCodeRate & 0xffffffffL)); + byteBuf.writeBytes(osd.encode()); + byteBuf.writeByte(audioEnable); + return byteBuf; + } + + public static JTVideoParam decode(ByteBuf buf) { + JTVideoParam videoParam = new JTVideoParam(); + videoParam.setLiveStreamCodeRateType(buf.readByte()); + videoParam.setLiveStreamResolving(buf.readByte()); + videoParam.setLiveStreamIInterval(buf.readUnsignedShort()); + videoParam.setLiveStreamFrameRate(buf.readByte()); + videoParam.setLiveStreamCodeRate(buf.readUnsignedInt()); + + videoParam.setStorageStreamCodeRateType(buf.readByte()); + videoParam.setStorageStreamResolving(buf.readByte()); + videoParam.setStorageStreamIInterval(buf.readUnsignedShort()); + videoParam.setStorageStreamFrameRate(buf.readByte()); + videoParam.setStorageStreamCodeRate(buf.readUnsignedInt()); + videoParam.setOsd(JTOSDConfig.decode(buf)); + videoParam.setAudioEnable(buf.readByte()); + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/cmd/JT1078Template.java b/src/main/java/com/genersoft/iot/vmp/jt1078/cmd/JT1078Template.java new file mode 100644 index 0000000..306b11e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/cmd/JT1078Template.java @@ -0,0 +1,693 @@ +package com.genersoft.iot.vmp.jt1078.cmd; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.jt1078.proc.entity.Cmd; +import com.genersoft.iot.vmp.jt1078.proc.response.*; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import org.springframework.stereotype.Component; + +import java.util.Random; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:58 + * @email qingtaij@163.com + */ +@Component +public class JT1078Template { + + private final Random random = new Random(); + + private static final String H8103 = "8103"; + private static final String H8104 = "8104"; + private static final String H8105 = "8105"; + private static final String H8106 = "8106"; + private static final String H8107 = "8107"; + private static final String H8201 = "8201"; + private static final String H8202 = "8202"; + private static final String H8203 = "8203"; + private static final String H8204 = "8204"; + private static final String H8300 = "8300"; + private static final String H8400 = "8400"; + private static final String H8401 = "8401"; + private static final String H8500 = "8500"; + private static final String H8600 = "8600"; + private static final String H8601 = "8601"; + private static final String H8602 = "8602"; + private static final String H8603 = "8603"; + private static final String H8604 = "8604"; + private static final String H8605 = "8605"; + private static final String H8606 = "8606"; + private static final String H8607 = "8607"; + private static final String H8608 = "8608"; + private static final String H8702 = "8702"; + private static final String H8801 = "8801"; + private static final String H8802 = "8802"; + private static final String H8803 = "8803"; + private static final String H8804 = "8804"; + private static final String H8805 = "8805"; + private static final String H9003 = "9003"; + private static final String H9101 = "9101"; + private static final String H9102 = "9102"; + private static final String H9201 = "9201"; + private static final String H9202 = "9202"; + private static final String H9205 = "9205"; + private static final String H9206 = "9206"; + private static final String H9207 = "9207"; + private static final String H9301 = "9301"; + private static final String H9302 = "9302"; + private static final String H9303 = "9303"; + private static final String H9304 = "9304"; + private static final String H9305 = "9305"; + private static final String H9306 = "9306"; + + private static final String H0001 = "0001"; + private static final String H0104 = "0104"; + private static final String H0107 = "0107"; + private static final String H0201 = "0201"; + private static final String H0500 = "0500"; + private static final String H0608 = "0608"; + private static final String H0702 = "0702"; + private static final String H0801 = "0801"; + private static final String H0802 = "0802"; + private static final String H0805 = "0805"; + private static final String H1003 = "1003"; + private static final String H1205 = "1205"; + + public void checkTerminalStatus(String devId){ + if (SessionManager.INSTANCE.get(devId) == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "终端不在线"); + } + } + + /** + * 开启直播视频 + * + * @param devId 设备号 + * @param j9101 开启视频参数 + */ + public Object startLive(String devId, J9101 j9101, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9101) + .setRespId(H0001) + .setRs(j9101) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 关闭直播视频 + * + * @param devId 设备号 + * @param j9102 关闭视频参数 + */ + public Object stopLive(String devId, J9102 j9102, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9102) + .setRespId(H0001) + .setRs(j9102) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 查询音视频列表 + * + * @param devId 设备号 + * @param j9205 查询音视频列表 + */ + public Object queryBackTime(String devId, J9205 j9205, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9205) + .setRespId(H1205) + .setRs(j9205) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 开启视频回放 + * + * @param devId 设备号 + * @param j9201 视频回放参数 + */ + public Object startBackLive(String devId, J9201 j9201, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9201) + .setRespId(H1205) + .setRs(j9201) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 视频回放控制 + * + * @param devId 设备号 + * @param j9202 控制视频回放参数 + */ + public Object controlBackLive(String devId, J9202 j9202, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9202) + .setRespId(H0001) + .setRs(j9202) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 文件上传 + * + * @param devId 设备号 + * @param j9206 文件上传参数 + */ + public Object fileUpload(String devId, J9206 j9206, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9206) + .setRespId(H0001) + .setRs(j9206) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 文件上传控制 + * + * @param devId 设备号 + * @param j9207 文件上传控制参数 + */ + public Object fileUploadControl(String devId, J9207 j9207, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9207) + .setRespId(H0001) + .setRs(j9207) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 云台控制指令-云台旋转 + * + * @param devId 设备号 + * @param j9301 云台旋转参数 + */ + public Object ptzRotate(String devId, J9301 j9301, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9301) + .setRespId(H0001) + .setRs(j9301) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 云台控制指令-云台调整焦距控制 + * + * @param devId 设备号 + * @param j9302 云台焦距控制参数 + */ + public Object ptzFocal(String devId, J9302 j9302, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9302) + .setRespId(H0001) + .setRs(j9302) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 云台控制指令-云台调整光圈控制 + * + * @param devId 设备号 + * @param j9303 云台光圈控制参数 + */ + public Object ptzIris(String devId, J9303 j9303, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9303) + .setRespId(H0001) + .setRs(j9303) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 云台控制指令-云台雨刷控制 + * + * @param devId 设备号 + * @param j9304 云台雨刷控制参数 + */ + public Object ptzWiper(String devId, J9304 j9304, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9304) + .setRespId(H0001) + .setRs(j9304) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 云台控制指令-红外补光控制 + * + * @param devId 设备号 + * @param j9305 云台红外补光控制参数 + */ + public Object ptzSupplementaryLight(String devId, J9305 j9305, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9305) + .setRespId(H0001) + .setRs(j9305) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 云台控制指令-变倍控制 + * + * @param devId 设备号 + * @param j9306 云台变倍控制参数 + */ + public Object ptzZoom(String devId, J9306 j9306, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9306) + .setRespId(H0001) + .setRs(j9306) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 查询终端参数 + * + * @param devId 设备号 + */ + public Object getDeviceConfig(String devId, J8104 j8104, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8104) + .setRespId(H0104) + .setRs(j8104) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 查询指定终端参数 + * + * @param devId 设备号 + */ + public Object getDeviceSpecifyConfig(String devId, J8106 j8106, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8106) + .setRespId(H0104) + .setRs(j8106) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 设置终端参数 + * + * @param devId 设备号 + */ + public Object setDeviceSpecifyConfig(String devId, J8103 j8103, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8103) + .setRespId(H0001) + .setRs(j8103) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + private Long randomInt() { + return (long) random.nextInt(1000) + 1; + } + + /** + * 设备控制 + */ + public Object deviceControl(String devId, J8105 j8105, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8105) + .setRespId(H0001) + .setRs(j8105) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 查询终端属性 + */ + public Object deviceAttribute(String devId, J8107 j8107, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8107) + .setRespId(H0107) + .setRs(j8107) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 位置信息查询 + */ + public Object queryPositionInfo(String devId, J8201 j8201, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8201) + .setRespId(H0201) + .setRs(j8201) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object tempPositionTrackingControl(String devId, J8202 j8202, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8202) + .setRespId(H0001) + .setRs(j8202) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object confirmationAlarmMessage(String devId, J8203 j8203, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8203) + .setRespId(H0001) + .setRs(j8203) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object linkDetection(String devId, J8204 j8204, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8204) + .setRespId(H0001) + .setRs(j8204) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object textMessage(String devId, J8300 j8300, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8300) + .setRespId(H0001) + .setRs(j8300) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object telephoneCallback(String devId, J8400 j8400, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8400) + .setRespId(H0001) + .setRs(j8400) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object setPhoneBook(String devId, J8401 j8401, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8401) + .setRespId(H0001) + .setRs(j8401) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object vehicleControl(String devId, J8500 j8500, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8500) + .setRespId(H0500) + .setRs(j8500) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object setAreaForCircle(String devId, J8600 j8600, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8600) + .setRespId(H0001) + .setRs(j8600) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object deleteAreaForCircle(String devId, J8601 j8601, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8601) + .setRespId(H0001) + .setRs(j8601) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object setAreaForRectangle(String devId, J8602 j8602, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8602) + .setRespId(H0001) + .setRs(j8602) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object deleteAreaForRectangle(String devId, J8603 j8603, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8603) + .setRespId(H0001) + .setRs(j8603) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object setAreaForPolygon(String devId, J8604 j8604, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8604) + .setRespId(H0001) + .setRs(j8604) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object deleteAreaForPolygon(String devId, J8605 j8605, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8605) + .setRespId(H0001) + .setRs(j8605) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object setRoute(String devId, J8606 j8606, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8606) + .setRespId(H0001) + .setRs(j8606) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object deleteRoute(String devId, J8607 j8607, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8607) + .setRespId(H0001) + .setRs(j8607) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object queryAreaOrRoute(String devId, J8608 j8608, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8608) + .setRespId(H0608) + .setRs(j8608) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object queryDriverInformation(String devId, J8702 j8702, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8702) + .setRespId(H0702) + .setRs(j8702) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object shooting(String devId, J8801 j8801, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8801) + .setRespId(H0805) + .setRs(j8801) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object queryMediaData(String devId, J8802 j8802, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8802) + .setRespId(H0802) + .setRs(j8802) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object uploadMediaData(String devId, J8803 j8803, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8803) + .setRespId(H0801) + .setRs(j8803) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object record(String devId, J8804 j8804, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8804) + .setRespId(H0001) + .setRs(j8804) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object uploadMediaDataForSingle(String devId, J8805 j8805, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8805) + .setRespId(H0801) + .setRs(j8805) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object queryMediaAttribute(String devId, J9003 j9003, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9003) + .setRespId(H1003) + .setRs(j9003) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/decode/Jt808Decoder.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/decode/Jt808Decoder.java new file mode 100644 index 0000000..3bfcfcf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/decode/Jt808Decoder.java @@ -0,0 +1,177 @@ +package com.genersoft.iot.vmp.jt1078.codec.decode; + +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.factory.CodecFactory; +import com.genersoft.iot.vmp.jt1078.proc.request.Re; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:10 + * @email qingtaij@163.com + */ +@Slf4j +public class Jt808Decoder extends ByteToMessageDecoder { + + private ApplicationEventPublisher applicationEventPublisher = null; + private Ijt1078Service service = null; + + public Jt808Decoder(ApplicationEventPublisher applicationEventPublisher, Ijt1078Service service ) { + this.applicationEventPublisher = applicationEventPublisher; + this.service = service; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + Session session = ctx.channel().attr(Session.KEY).get(); + log.info("> {} hex: 7e{}7e", session, ByteBufUtil.hexDump(in)); + try { + // 按照部标定义执行校验和转义 + ByteBuf buf = unEscapeAndCheck(in); + buf.retain(); + Header header = new Header(); + header.setMsgId(ByteBufUtil.hexDump(buf.readSlice(2))); + header.setMsgPro(buf.readUnsignedShort()); + // 从消息属性中读取是否存在分包 + boolean isSubpackage = (header.getMsgPro() >>> 13 & 1) == 1; + if (header.is2019Version()) { + header.setVersion(buf.readUnsignedByte()); + String devId = ByteBufUtil.hexDump(buf.readSlice(10)); + header.setPhoneNumber(devId.replaceFirst("^0*", "")); + } else { + header.setPhoneNumber(ByteBufUtil.hexDump(buf.readSlice(6)).replaceFirst("^0*", "")); + } + header.setSn(buf.readUnsignedShort()); + if (isSubpackage) { + int packageCount = buf.readUnsignedShort(); + int packageNumber = buf.readUnsignedShort(); + log.debug("[分包消息] header: {}, 序号: {}, 总数: {}", header, packageNumber, packageCount); + // 缓存带合并的分包消息 + ByteBuf intactBuf = MultiPacketManager.INSTANCE.add(header, packageCount, buf); + if (intactBuf == null) { + return; + } + buf = intactBuf; + } + Re handler = CodecFactory.getHandler(header.getMsgId()); + if (handler == null) { + log.error("get msgId is null {}", header.getMsgId()); + buf.release(); + return; + } + + Rs decode = handler.decode(buf, header, session, service); + ApplicationEvent applicationEvent = handler.getEvent(); + if (applicationEvent != null) { + applicationEventPublisher.publishEvent(applicationEvent); + } + if (decode != null) { + out.add(decode); + } + } finally { + in.skipBytes(in.readableBytes()); + } + } + + + /** + * 转义与验证校验码 + * + * @param byteBuf 转义Buf + * @return 转义好的数据 + */ + public ByteBuf unEscapeAndCheck(ByteBuf byteBuf) throws Exception { + int low = byteBuf.readerIndex(); + int high = byteBuf.writerIndex(); + byte checkSum = 0; + int calculationCheckSum = 0; + + byte aByte = byteBuf.getByte(high - 2); + byte protocolEscapeFlag7d = 0x7d; + //0x7d转义 + byte protocolEscapeFlag01 = 0x01; + //0x7e转义 + byte protocolEscapeFlag02 = 0x02; + if (aByte == protocolEscapeFlag7d) { + byte b2 = byteBuf.getByte(high - 1); + if (b2 == protocolEscapeFlag01) { + checkSum = protocolEscapeFlag7d; + } else if (b2 == protocolEscapeFlag02) { + checkSum = 0x7e; + } else { + log.error("转义1异常:{}", ByteBufUtil.hexDump(byteBuf)); + throw new Exception("转义错误"); + } + high = high - 2; + } else { + high = high - 1; + checkSum = byteBuf.getByte(high); + } + List bufList = new ArrayList<>(); + int index = low; + while (index < high) { + byte b = byteBuf.getByte(index); + if (b == protocolEscapeFlag7d) { + byte c = byteBuf.getByte(index + 1); + if (c == protocolEscapeFlag01) { + ByteBuf slice = slice0x01(byteBuf, low, index); + bufList.add(slice); + b = protocolEscapeFlag7d; + } else if (c == protocolEscapeFlag02) { + ByteBuf slice = slice0x02(byteBuf, low, index); + bufList.add(slice); + b = 0x7e; + } else { + log.error("转义2异常:{}", ByteBufUtil.hexDump(byteBuf)); + throw new Exception("转义错误"); + } + index += 2; + low = index; + } else { + index += 1; + } + calculationCheckSum = calculationCheckSum ^ b; + } + + if (calculationCheckSum == checkSum) { + if (bufList.isEmpty()) { + return byteBuf.slice(low, high); + } else { + bufList.add(byteBuf.slice(low, high - low)); + return new CompositeByteBuf(UnpooledByteBufAllocator.DEFAULT, false, bufList.size(), bufList); + } + } else { + log.info("{} 解析校验码:{}--计算校验码:{}", ByteBufUtil.hexDump(byteBuf), checkSum, calculationCheckSum); + throw new Exception("校验码错误!"); + } + } + + + private ByteBuf slice0x01(ByteBuf buf, int low, int sign) { + return buf.slice(low, sign - low + 1); + } + + private ByteBuf slice0x02(ByteBuf buf, int low, int sign) { + buf.setByte(sign, 0x7e); + return buf.slice(low, sign - low + 1); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/decode/MultiPacketManager.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/decode/MultiPacketManager.java new file mode 100644 index 0000000..73f64b1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/decode/MultiPacketManager.java @@ -0,0 +1,59 @@ +package com.genersoft.iot.vmp.jt1078.codec.decode; + +import com.genersoft.iot.vmp.jt1078.proc.Header; +import io.netty.buffer.*; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +public enum MultiPacketManager { + INSTANCE; + // 用与消息的缓存 + private final Map packetMap = new ConcurrentHashMap<>(); + private final Map packetTimeMap = new ConcurrentHashMap<>(); + + MultiPacketManager() { + startLister(); + } + + /** + * 增加待合并的分包,如果分包接受完毕会返回完整的数据包 + */ + public ByteBuf add(Header header, Integer count, ByteBuf byteBuf) { + String key = header.getMsgId() + "/" + header.getPhoneNumber(); + CompositeByteBuf compositeBuf = packetMap.computeIfAbsent(key, k -> new CompositeByteBuf(UnpooledByteBufAllocator.DEFAULT, false, count)); +// compositeBuf.addComponent(true, byteBuf.readSlice(byteBuf.readableBytes())); + compositeBuf.addComponent(true, byteBuf); + packetTimeMap.put(key, System.currentTimeMillis()); + if (count == compositeBuf.numComponents()) { + packetMap.remove(key); + packetTimeMap.remove(key); + compositeBuf.retain(); + return compositeBuf; + } + return null; + } + + private void startLister(){ + Timer timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + long expireTime = System.currentTimeMillis() - 20 * 1000; + if (!packetTimeMap.isEmpty()) { + for (String key : packetTimeMap.keySet()) { + if (packetTimeMap.get(key) < expireTime) { + log.info("分包消息超时 key: {}", key); + packetTimeMap.remove(key); + packetMap.remove(key); + } + } + } + } + }, 2000L, 2000L); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808Encoder.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808Encoder.java new file mode 100644 index 0000000..c3c6d1c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808Encoder.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.jt1078.codec.encode; + + +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:10 + * @email qingtaij@163.com + */ +@Slf4j +public class Jt808Encoder extends MessageToByteEncoder { + + @Override + protected void encode(ChannelHandlerContext ctx, Rs msg, ByteBuf out) throws Exception { + Session session = ctx.channel().attr(Session.KEY).get(); + + List encodeList = Jt808EncoderCmd.encode(msg, session, session.nextSerialNo()); + if(encodeList!=null && !encodeList.isEmpty()){ + for (ByteBuf byteBuf : encodeList) { + log.debug("< {} hex:{}", session, ByteBufUtil.hexDump(byteBuf)); + out.writeBytes(byteBuf); + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808EncoderCmd.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808EncoderCmd.java new file mode 100644 index 0000000..3513c38 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808EncoderCmd.java @@ -0,0 +1,184 @@ +package com.genersoft.iot.vmp.jt1078.codec.encode; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.entity.Cmd; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.util.Bin; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import io.netty.util.ByteProcessor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:25 + * @email qingtaij@163.com + */ +@Slf4j +public class Jt808EncoderCmd extends MessageToByteEncoder { + + @Override + protected void encode(ChannelHandlerContext ctx, Cmd cmd, ByteBuf out) throws Exception { + Session session = ctx.channel().attr(Session.KEY).get(); + Rs msg = cmd.getRs(); + List encodeList = encode(msg, session, cmd.getPackageNo().intValue()); + if (encodeList != null && !encodeList.isEmpty()) { + for (ByteBuf byteBuf : encodeList) { + log.debug("< {} hex:{}", session, ByteBufUtil.hexDump(byteBuf)); + out.writeBytes(byteBuf); + } + } + } + + + public static List encode(Rs msg, Session session, Integer packageNo) { + String id = msg.getClass().getAnnotation(MsgId.class).id(); + if (!StringUtils.hasLength(id)) { + log.error("Not find msgId"); + return null; + } + ByteBuf encode = msg.encode(); + Header header = msg.getHeader(); + + List byteBufList = new LinkedList<>(); + if (encode.readableBytes() > 1000) { + int index = 1; + int total = encode.readableBytes()%1000 == 0 ? encode.readableBytes()/1000 : (encode.readableBytes()/1000 + 1); + while (encode.isReadable()) { + ByteBuf byteBuf; + if (index == total) { + byteBuf = buildMsgByte(header, id, session, packageNo, encode.readRetainedSlice(encode.readableBytes()), index, total); + }else { + byteBuf = buildMsgByte(header, id, session, packageNo, encode.readRetainedSlice(1000), index, total); + } + + byteBufList.add(byteBuf); + index ++; + } + }else { + byteBufList.add(buildMsgByte(header, id, session, packageNo, encode, 0, 0)); + } + return byteBufList; + } + + // 分包 + private static ByteBuf buildMsgByte(Header header, String id, Session session, Integer packageNo, ByteBuf encode, Integer packetIndex, Integer packetTotal) { + ByteBuf byteBuf = Unpooled.buffer(); + + byteBuf.writeBytes(ByteBufUtil.decodeHexDump(id)); + + if (header == null) { + header = session.getHeader(); + } + + if (header.is2019Version()) { + int msgBody = encode.readableBytes() | 1 << 14; + if (packetIndex > 0) { + msgBody = msgBody | 1 << 13; + } + // 消息体属性 + byteBuf.writeShort(msgBody); + + // 版本号 + byteBuf.writeByte(header.getVersion()); + + // 终端手机号 + byteBuf.writeBytes(ByteBufUtil.decodeHexDump(Bin.strHexPaddingLeft(header.getPhoneNumber(), 20))); + } else { + // 消息体属性 + byteBuf.writeShort(encode.readableBytes()); + + byteBuf.writeBytes(ByteBufUtil.decodeHexDump(Bin.strHexPaddingLeft(header.getPhoneNumber(), 12))); + } + + // 消息体流水号 + byteBuf.writeShort(packageNo); + + if (packetIndex > 0) { + byteBuf.writeShort(packetTotal); + byteBuf.writeShort(packetIndex); + } + + // 写入消息体 + byteBuf.writeBytes(encode); + + // 计算校验码,并反转义 + byteBuf = escapeAndCheck0(byteBuf); + return byteBuf; + } + + + private static final ByteProcessor searcher = value -> !(value == 0x7d || value == 0x7e); + + //转义与校验 + public static ByteBuf escapeAndCheck0(ByteBuf source) { + + sign(source); + + int low = source.readerIndex(); + int high = source.writerIndex(); + + LinkedList bufList = new LinkedList<>(); + int mark, len; + while ((mark = source.forEachByte(low, high - low, searcher)) > 0) { + + len = mark + 1 - low; + ByteBuf[] slice = slice(source, low, len); + bufList.add(slice[0]); + bufList.add(slice[1]); + low += len; + } + + if (bufList.size() > 0) { + bufList.add(source.slice(low, high - low)); + } else { + bufList.add(source); + } + + ByteBuf delimiter = Unpooled.buffer(1, 1).writeByte(0x7e).retain(); + bufList.addFirst(delimiter); + bufList.addLast(delimiter); + + CompositeByteBuf byteBufLs = Unpooled.compositeBuffer(bufList.size()); + byteBufLs.addComponents(true, bufList); + return byteBufLs; + } + + public static void sign(ByteBuf buf) { + byte checkCode = bcc(buf); + buf.writeByte(checkCode); + } + + public static byte bcc(ByteBuf byteBuf) { + byte cs = 0; + while (byteBuf.isReadable()) + cs ^= byteBuf.readByte(); + byteBuf.resetReaderIndex(); + return cs; + } + + protected static ByteBuf[] slice(ByteBuf byteBuf, int index, int length) { + byte first = byteBuf.getByte(index + length - 1); + + ByteBuf[] byteBufList = new ByteBuf[2]; + byteBufList[0] = byteBuf.retainedSlice(index, length); + + if (first == 0x7d) { + byteBufList[1] = Unpooled.buffer(1, 1).writeByte(0x01); + } else { + byteBuf.setByte(index + length - 1, 0x7d); + byteBufList[1] = Unpooled.buffer(1, 1).writeByte(0x02); + } + return byteBufList; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/Jt808Handler.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/Jt808Handler.java new file mode 100644 index 0000000..202f81e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/Jt808Handler.java @@ -0,0 +1,98 @@ +package com.genersoft.iot.vmp.jt1078.codec.netty; + +import com.genersoft.iot.vmp.jt1078.event.ConnectChangeEvent; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.util.ReferenceCountUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; +import lombok.extern.slf4j.Slf4j; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:14 + * @email qingtaij@163.com + */ +@Slf4j +public class Jt808Handler extends ChannelInboundHandlerAdapter { + + private ApplicationEventPublisher applicationEventPublisher = null; + + public Jt808Handler(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof Rs) { + ctx.writeAndFlush(msg); + } else { + ctx.fireChannelRead(msg); + } + // 读取完成后的消息释放 + ReferenceCountUtil.release(msg); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + Channel channel = ctx.channel(); + Session session = SessionManager.INSTANCE.newSession(channel); + channel.attr(Session.KEY).set(session); + log.info("> Tcp connect {}", session); + if (session.getPhoneNumber() == null) { + return; + } + ConnectChangeEvent event = new ConnectChangeEvent(this); + event.setConnected(true); + event.setPhoneNumber(session.getPhoneNumber()); + applicationEventPublisher.publishEvent(event); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + Session session = ctx.channel().attr(Session.KEY).get(); + log.info("< Tcp disconnect {}", session); + ctx.close(); + if (session.getPhoneNumber() == null) { + return; + } + ConnectChangeEvent event = new ConnectChangeEvent(this); + event.setConnected(false); + event.setPhoneNumber(session.getPhoneNumber()); + applicationEventPublisher.publishEvent(event); + + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) { + Session session = ctx.channel().attr(Session.KEY).get(); + String message = e.getMessage(); + if (message.toLowerCase().contains("Connection reset by peer".toLowerCase())) { + log.info("< exception{} {}", session, e.getMessage()); + } else { + log.info("< exception{} {}", session, e.getMessage(), e); + } + + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof IdleStateEvent) { + IdleStateEvent event = (IdleStateEvent) evt; + IdleState state = event.state(); + if (state == IdleState.READER_IDLE || state == IdleState.WRITER_IDLE) { + Session session = ctx.channel().attr(Session.KEY).get(); + log.warn("< Proactively disconnect{}", session); + ctx.close(); + } + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/TcpServer.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/TcpServer.java new file mode 100644 index 0000000..3996c3f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/TcpServer.java @@ -0,0 +1,123 @@ +package com.genersoft.iot.vmp.jt1078.codec.netty; + +import com.genersoft.iot.vmp.jt1078.codec.decode.Jt808Decoder; +import com.genersoft.iot.vmp.jt1078.codec.encode.Jt808Encoder; +import com.genersoft.iot.vmp.jt1078.codec.encode.Jt808EncoderCmd; +import com.genersoft.iot.vmp.jt1078.proc.factory.CodecFactory; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioChannelOption; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.DelimiterBasedFrameDecoder; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.concurrent.Future; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:01 + * @email qingtaij@163.com + */ + +@Slf4j +public class TcpServer { + + private final Integer port; + private boolean isRunning = false; + private EventLoopGroup bossGroup = null; + private EventLoopGroup workerGroup = null; + private ApplicationEventPublisher applicationEventPublisher = null; + private Ijt1078Service service = null; + + private final ByteBuf DECODER_JT808 = Unpooled.wrappedBuffer(new byte[]{0x7e}); + + public TcpServer(Integer port, ApplicationEventPublisher applicationEventPublisher, Ijt1078Service service) { + this.port = port; + this.applicationEventPublisher = applicationEventPublisher; + this.service = service; + } + + private void startTcpServer() { + try { + CodecFactory.init(); + this.bossGroup = new NioEventLoopGroup(); + this.workerGroup = new NioEventLoopGroup(); + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.channel(NioServerSocketChannel.class); + bootstrap.group(bossGroup, workerGroup); + + bootstrap.option(NioChannelOption.SO_BACKLOG, 1024) + .option(NioChannelOption.SO_REUSEADDR, true) + .childOption(NioChannelOption.TCP_NODELAY, true) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(NioSocketChannel channel) { + channel.pipeline() + .addLast(new IdleStateHandler(10, 0, 0, TimeUnit.MINUTES)) + .addLast(new DelimiterBasedFrameDecoder(1024 * 2, DECODER_JT808)) + .addLast(new Jt808Decoder(applicationEventPublisher, service)) + .addLast(new Jt808Encoder()) + .addLast(new Jt808EncoderCmd()) + .addLast(new Jt808Handler(applicationEventPublisher)); + } + }); + ChannelFuture channelFuture = bootstrap.bind(port).sync(); + // 监听设备TCP端口是否启动成功 + channelFuture.addListener(future -> { + if (!future.isSuccess()) { + log.error("Binding port:{} fail! cause: {}", port, future.cause().getCause(), future.cause()); + } + }); + log.info("服务:JT808 Server 启动成功, port:{}", port); + channelFuture.channel().closeFuture().sync(); + } catch (Exception e) { + log.warn("服务:JT808 Server 启动异常, port:{},{}", port, e.getMessage(), e); + } finally { + stop(); + } + } + + /** + * 开启一个新的线程,拉起来Netty + */ + public synchronized void start() { + if (this.isRunning) { + log.warn("服务:JT808 Server 已经启动, port:{}", port); + return; + } + this.isRunning = true; + new Thread(this::startTcpServer).start(); + } + + public synchronized void stop() { + if (!this.isRunning) { + log.warn("服务:JT808 Server 已经停止, port:{}", port); + } + this.isRunning = false; + Future future = this.bossGroup.shutdownGracefully(); + if (!future.isSuccess()) { + log.warn("bossGroup 无法正常停止", future.cause()); + } + future = this.workerGroup.shutdownGracefully(); + if (!future.isSuccess()) { + log.warn("workerGroup 无法正常停止", future.cause()); + } + log.warn("服务:JT808 Server 已经停止, port:{}", port); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078AutoConfiguration.java b/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078AutoConfiguration.java new file mode 100644 index 0000000..507a05a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078AutoConfiguration.java @@ -0,0 +1,33 @@ +package com.genersoft.iot.vmp.jt1078.config; + +import com.genersoft.iot.vmp.jt1078.codec.netty.TcpServer; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +/** + * @author QingtaiJiang + * @date 2023/4/27 19:35 + * @email qingtaij@163.com + */ +@Order(Integer.MIN_VALUE) +@Configuration +@ConditionalOnProperty(value = "jt1078.enable", havingValue = "true") +public class JT1078AutoConfiguration { + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + private Ijt1078Service service; + + @Bean(initMethod = "start", destroyMethod = "stop") + public TcpServer jt1078Server(@Value("${jt1078.port}") Integer port) { + return new TcpServer(port, applicationEventPublisher, service); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078Config.java b/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078Config.java new file mode 100644 index 0000000..6a6afee --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078Config.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.jt1078.config; + +import com.genersoft.iot.vmp.jt1078.bean.common.ConfigAttribute; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Component +@Data +@ConfigurationProperties(prefix = "jt1078", ignoreInvalidFields = true) +@Order(3) +public class JT1078Config { + + private Integer port; + + private String password; +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/JT1078Controller.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/JT1078Controller.java new file mode 100644 index 0000000..5310ff0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/JT1078Controller.java @@ -0,0 +1,1011 @@ +package com.genersoft.iot.vmp.jt1078.controller; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.ftpServer.FtpSetting; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.controller.bean.*; +import com.genersoft.iot.vmp.jt1078.proc.request.J1205; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078PlayService; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.List; + + +@Slf4j +@ConditionalOnProperty(value = "jt1078.enable", havingValue = "true") +@RestController +@Tag(name = "部标设备控制") +@RequestMapping("/api/jt1078") +public class JT1078Controller { + + @Resource + private Ijt1078Service service; + + @Resource + private Ijt1078PlayService jt1078PlayService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private FtpSetting ftpSetting; + + @Operation(summary = "JT-开始点播", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道编号, 一般为从1开始的数字", required = true) + @Parameter(name = "type", description = "类型:0:音视频,1:视频,3:音频", required = true) + @GetMapping("/live/start") + public DeferredResult> startLive(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId, + @Parameter(required = false) Integer type) { + if (type == null || (type != 0 && type != 1 && type != 3)) { + type = 0; + } + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + result.onTimeout(()->{ + log.info("[JT-点播等待超时] phoneNumber:{}, channelId:{}, ", phoneNumber, channelId); + // 释放rtpserver + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("超时"); + result.setResult(wvpResult); + jt1078PlayService.stopPlay(phoneNumber, channelId); + }); + + jt1078PlayService.play(phoneNumber, channelId, type, wvpResult -> { + WVPResult wvpResultForFinish = new WVPResult<>(); + wvpResultForFinish.setCode(wvpResult.getCode()); + wvpResultForFinish.setMsg(wvpResult.getMsg()); + if (wvpResult.getCode() == InviteErrorCode.SUCCESS.getCode()) { + StreamInfo streamInfo = wvpResult.getData(); + + if (streamInfo != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + wvpResultForFinish.setData(new StreamContent(streamInfo)); + } + } + result.setResult(wvpResultForFinish); + }); + + return result; + } + + @Operation(summary = "JT-结束点播", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @GetMapping("/live/stop") + public void stopLive(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId) { + jt1078PlayService.stopPlay(phoneNumber, channelId); + } + + @Operation(summary = "JT-语音对讲", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @GetMapping("/talk/start") + public StreamContent startTalk(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId) { + + StreamInfo streamInfo = jt1078PlayService.startTalk(phoneNumber, channelId); + if (userSetting.getUseSourceIpAsStreamIp()) { + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + streamInfo.setIp("localhost"); + return new StreamContent(streamInfo); + } + + @Operation(summary = "JT-结束对讲", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @GetMapping("/talk/stop") + public void stopTalk(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId) { + jt1078PlayService.stopTalk(phoneNumber, channelId); + } + + + @Operation(summary = "JT-暂停点播", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @GetMapping("/live/pause") + public void pauseLive(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId) { + jt1078PlayService.pausePlay(phoneNumber, channelId); + } + + @Operation(summary = "JT-继续点播", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @GetMapping("/live/continue") + public void continueLive(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId) { + + jt1078PlayService.continueLivePlay(phoneNumber, channelId); + } + + @Operation(summary = "JT-切换码流类型", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @Parameter(name = "streamType", description = "0:主码流; 1:子码流", required = true) + @GetMapping("/live/switch") + public void changeStreamType(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId, + @Parameter(required = true) Integer streamType) { + service.changeStreamType(phoneNumber, channelId, streamType); + } + + @Operation(summary = "JT-录像-查询资源列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @Parameter(name = "startTime", description = "开始时间,格式: yyyy-MM-dd HH:mm:ss", required = true) + @Parameter(name = "endTime", description = "结束时间,格式: yyyy-MM-dd HH:mm:ss", required = true) + @GetMapping("/record/list") + public WVPResult> playbackList(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId, + @Parameter(required = true) String startTime, + @Parameter(required = true) String endTime + ) { + List recordList = jt1078PlayService.getRecordList(phoneNumber, channelId, startTime, endTime); + if (recordList == null) { + return WVPResult.fail(ErrorCode.ERROR100); + }else { + return WVPResult.success(recordList); + } + } + @Operation(summary = "JT-录像-开始回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @Parameter(name = "startTime", description = "开始时间,格式: yyyy-MM-dd HH:mm:ss", required = true) + @Parameter(name = "endTime", description = "结束时间,格式: yyyy-MM-dd HH:mm:ss", required = true) + @Parameter(name = "type", description = "0.音视频 1.音频 2.视频 3.视频或音视频", required = true) + @Parameter(name = "rate", description = "0.所有码流 1.主码流 2.子码流(如果此通道只传输音频,此字段置0)", required = true) + @Parameter(name = "playbackType", description = "0.正常回放 1.快进回放 2.关键帧快退回放 3.关键帧播放 4.单帧上传", required = true) + @Parameter(name = "playbackSpeed", description = "0.无效 1.1倍 2.2倍 3.4倍 4.8倍 5.16倍 (回放控制为1和2时,此字段内容有效,否则置0)", required = true) + @GetMapping("/playback/start") + public DeferredResult> recordLive(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId, + @Parameter(required = true) String startTime, + @Parameter(required = true) String endTime, + @Parameter(required = false) Integer type, + @Parameter(required = false) Integer rate, + @Parameter(required = false) Integer playbackType, + @Parameter(required = false) Integer playbackSpeed + + ) { + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + result.onTimeout(()->{ + log.info("[JT-回放-等待超时] phoneNumber:{}, channelId:{}, ", phoneNumber, channelId); + // 释放rtpserver + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("回放超时"); + result.setResult(wvpResult); + jt1078PlayService.stopPlay(phoneNumber, channelId); + }); + + jt1078PlayService.playback(phoneNumber, channelId, startTime, endTime,type, rate, playbackType, playbackSpeed, wvpResult -> { + WVPResult wvpResultForFinish = new WVPResult<>(); + wvpResultForFinish.setCode(wvpResult.getCode()); + wvpResultForFinish.setMsg(wvpResult.getMsg()); + if (wvpResult.getCode() == InviteErrorCode.SUCCESS.getCode()) { + StreamInfo streamInfo = wvpResult.getData(); + if (streamInfo != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + wvpResultForFinish.setData(new StreamContent(streamInfo)); + } + } + result.setResult(wvpResultForFinish); + }); + + return result; + } + + @Operation(summary = "JT-录像-回放控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @Parameter(name = "command", description = "0:开始回放; 1:暂停回放; 2:结束回放; 3:快进回放; 4:关键帧快退回放; 5:拖动回放; 6:关键帧播放", required = true) + @Parameter(name = "playbackSpeed", description = "0.无效 1.1倍 2.2倍 3.4倍 4.8倍 5.16倍 (回放控制为3和4时,此字段内容有效,否则置0)", required = false) + @Parameter(name = "time", description = "拖动回放位置(时间)", required = false) + @GetMapping("/playback/control") + public void recordControl(@Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId, + @Parameter(required = false) Integer command, + @Parameter(required = false) String time, + @Parameter(required = false) Integer playbackSpeed + + ) { + jt1078PlayService.playbackControl(phoneNumber, channelId, command, playbackSpeed, time); + } + + @Operation(summary = "JT-录像-结束回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @GetMapping("/playback/stop") + public void stopPlayback(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId) { + jt1078PlayService.stopPlayback(phoneNumber, channelId); + } + + @Operation(summary = "JT-录像-获取下载地址", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @Parameter(name = "startTime", description = "开始时间,格式: yyyy-MM-dd HH:mm:ss", required = true) + @Parameter(name = "endTime", description = "结束时间,格式: yyyy-MM-dd HH:mm:ss", required = true) + @Parameter(name = "alarmSign", description = "报警标志", required = true) + @Parameter(name = "mediaType", description = "音视频资源类型: 0.音视频 1.音频 2.视频 3.视频或音视频", required = true) + @Parameter(name = "streamType", description = "码流类型:0.所有码流 1.主码流 2.子码流(如果此通道只传输音频,此字段置0)", required = true) + @Parameter(name = "storageType", description = "存储器类型", required = true) + @GetMapping("/playback/downloadUrl") + public String getRecordTempUrl(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId, + @Parameter(required = true) String startTime, + @Parameter(required = true) String endTime, + @Parameter(required = false) Integer alarmSign, + @Parameter(required = false) Integer mediaType, + @Parameter(required = false) Integer streamType, + @Parameter(required = false) Integer storageType + + ){ + log.info("[JT-录像] 下载,设备:{}, 通道: {}, 开始时间: {}, 结束时间: {},报警标志: {}, 音视频类型: {}, 码流类型: {},存储器类型: {}, ", + phoneNumber, channelId, startTime, endTime, alarmSign, mediaType, streamType, storageType); + if (!ftpSetting.getEnable()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未启用ftp服务,无法下载录像"); + } + return service.getRecordTempUrl(phoneNumber, channelId, startTime, endTime, alarmSign, mediaType, streamType, storageType); + } + + @Operation(summary = "JT-录像-下载", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "path", description = "临时下载路径", required = true) + @GetMapping("/playback/download") + public void download(HttpServletRequest request, HttpServletResponse response, @Parameter(required = true) String path) throws IOException { + if (!ftpSetting.getEnable()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未启用ftp服务,无法下载录像"); + } + DeferredResult result = new DeferredResult<>(); + ServletOutputStream outputStream = response.getOutputStream(); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(path + ".mp4", "UTF-8")); +// response.setContentLength(394983300); + response.setStatus(HttpServletResponse.SC_OK); + service.recordDownload(path, outputStream); + } + + @Operation(summary = "JT-云台控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @Parameter(name = "command", description = "控制指令,允许值: left, right, up, down, zoomin, zoomout, irisin, irisout, focusnear, focusfar, stop", required = true) + @Parameter(name = "speed", description = "速度(0-255), command,值 left, right, up, down时有效", required = true) + @GetMapping("/ptz") + public void ptz(String phoneNumber, Integer channelId, String command, int speed){ + + log.info("[JT-云台控制] phoneNumber:{}, channelId:{}, command: {}, speed: {}", phoneNumber, channelId, command, speed); + service.ptzControl(phoneNumber, channelId, command, speed); + } + + @Operation(summary = "JT-补光灯开关", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @Parameter(name = "command", description = "控制指令,允许值: on off", required = true) + @GetMapping("/fill-light") + public void fillLight(String phoneNumber, Integer channelId, String command){ + + log.info("[JT-补光灯开关] phoneNumber:{}, channelId:{}, command: {}", phoneNumber, channelId, command); + service.supplementaryLight(phoneNumber, channelId, command); + } + + @Operation(summary = "JT-雨刷开关", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @Parameter(name = "command", description = "控制指令,允许值: on off", required = true) + @GetMapping("/wiper") + public void wiper(String phoneNumber, Integer channelId, String command){ + + log.info("[JT-雨刷开关] phoneNumber:{}, channelId:{}, command: {}", phoneNumber, channelId, command); + service.wiper(phoneNumber, channelId, command); + } + + @Operation(summary = "JT-查询终端参数", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @GetMapping("/config/get") + public JTDeviceConfig config(String phoneNumber, String[] params){ + + log.info("[JT-查询终端参数] phoneNumber:{}", phoneNumber); + return service.queryConfig(phoneNumber, params); + } + + @Operation(summary = "JT-设置终端参数", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "config", description = "终端参数", required = true) + @PostMapping("/config/set") + public void setConfig(@RequestBody SetConfigParam config){ + + log.info("[JT-设置终端参数] 参数: {}", config.toString()); + service.setConfig(config.getPhoneNumber(), config.getConfig()); + } + + @Operation(summary = "终端控制-连接指定的服务器", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "control", description = "终端控制参数", required = true) + @PostMapping("/control/connection") + public void connectionControl(@RequestBody ConnectionControlParam control){ + + log.info("[JT-终端控制] 参数: {}", control.toString()); + service.connectionControl(control.getPhoneNumber(), control.getControl()); + } + + @Operation(summary = "JT-终端控制-复位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @PostMapping("/control/reset") + public void resetControl(String phoneNumber){ + + log.info("[JT-复位] phoneNumber: {}", phoneNumber); + service.resetControl(phoneNumber); + } + + @Operation(summary = "JT-终端控制-恢复出厂设置", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @PostMapping("/control/factory-reset") + public void factoryResetControl(String phoneNumber){ + + log.info("[JT-恢复出厂设置] phoneNumber: {}", phoneNumber); + service.factoryResetControl(phoneNumber); + } + + @Operation(summary = "JT-查询终端属性", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @GetMapping("/attribute") + public JTDeviceAttribute attribute(String phoneNumber){ + + log.info("[JT-查询终端属性] phoneNumber: {}", phoneNumber); + return service.attribute(phoneNumber); + } + + @Operation(summary = "JT-查询位置信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @GetMapping("/position-info") + public JTPositionBaseInfo queryPositionInfo(String phoneNumber){ + + log.info("[JT-查询位置信息] phoneNumber: {}", phoneNumber); + return service.queryPositionInfo(phoneNumber); + } + + @Operation(summary = "JT-临时位置跟踪控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "timeInterval", description = "时间间隔,单位为秒,时间间隔为0 时停止跟踪,停止跟踪无需带后继字段", required = true) + @Parameter(name = "validityPeriod", description = "位置跟踪有效期, 单位为秒,终端在接收到位置跟踪控制消息后,在有效期截止时间之前依据消息中的时间间隔发送位置汇报", required = true) + @GetMapping("/control/temp-position-tracking") + public void tempPositionTrackingControl(String phoneNumber, Integer timeInterval, Long validityPeriod){ + + log.info("[JT-临时位置跟踪控制] phoneNumber: {}, 时间间隔 {}秒, 位置跟踪有效期 {}秒", phoneNumber, timeInterval, validityPeriod); + service.tempPositionTrackingControl(phoneNumber, timeInterval, validityPeriod); + } + + @Operation(summary = "JT-人工确认报警消息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "timeInterval", description = "时间间隔,单位为秒,时间间隔为0 时停止跟踪,停止跟踪无需带后继字段", required = true) + @Parameter(name = "validityPeriod", description = "位置跟踪有效期, 单位为秒,终端在接收到位置跟踪控制消息后,在有效期截止时间之前依据消息中的时间间隔发送位置汇报", required = true) + @PostMapping("/confirmation-alarm-message") + public void confirmationAlarmMessage(@RequestBody ConfirmationAlarmMessageParam param){ + + log.info("[JT-人工确认报警消息] 参数: {}", param); + service.confirmationAlarmMessage(param.getPhoneNumber(), param.getAlarmPackageNo(), param.getAlarmMessageType()); + } + + @Operation(summary = "JT-链路检测", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @GetMapping("/link-detection") + public Integer linkDetection(String phoneNumber){ + + log.info("[JT-链路检测] phoneNumber: {}", phoneNumber); + return service.linkDetection(phoneNumber); + } + + @Operation(summary = "JT-文本信息下发", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "textMessageParam", description = "文本信息下发参数", required = true) + @PostMapping("/text-msg") + public WVPResult textMessage(@RequestBody TextMessageParam textMessageParam){ + + log.info("[JT-文本信息下发] textMessageParam: {}", textMessageParam); + int result = service.textMessage(textMessageParam.getPhoneNumber(), textMessageParam.getSign(), textMessageParam.getTextType(), textMessageParam.getContent()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-电话回拨", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "sign", description = "标志: 0:普通通话,1:监听", required = true) + @Parameter(name = "destPhoneNumber", description = "回拨电话号码", required = true) + @GetMapping("/telephone-callback") + public WVPResult telephoneCallback(String phoneNumber, Integer sign, String destPhoneNumber){ + + log.info("[JT-电话回拨] phoneNumber: {}, sign: {}, phoneNumber: {},", phoneNumber, sign, phoneNumber); + int result = service.telephoneCallback(phoneNumber, sign, destPhoneNumber); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-设置电话本", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "setPhoneBookParam", description = "设置电话本参数", required = true) + @PostMapping("/set-phone-book") + public WVPResult setPhoneBook(@RequestBody SetPhoneBookParam setPhoneBookParam){ + + log.info("[JT-设置电话本] setPhoneBookParam: {}", setPhoneBookParam); + int result = service.setPhoneBook(setPhoneBookParam.getPhoneNumber(), setPhoneBookParam.getType(), setPhoneBookParam.getPhoneBookContactList()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-车门控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "open", description = "开启车门", required = true) + @GetMapping("/control/door") + public WVPResult controlDoor(String phoneNumber, Boolean open){ + + log.info("[JT-车门控制] phoneNumber: {}, open: {},", phoneNumber, open); + JTPositionBaseInfo positionBaseInfo = service.controlDoor(phoneNumber, open); + if (positionBaseInfo == null || positionBaseInfo.getStatus() == null) { + return WVPResult.fail(ErrorCode.ERROR100.getCode(), "控制失败"); + } + if (open == !positionBaseInfo.getStatus().isDoorLocking()) { + return WVPResult.success(null); + }else { + return WVPResult.fail(ErrorCode.ERROR100.getCode(), "控制失败"); + } + } + + @Operation(summary = "JT-更新圆形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "areaParam", description = "设置区域参数", required = true) + @PostMapping("/area/circle/update") + public WVPResult updateAreaForCircle(@RequestBody SetAreaParam areaParam){ + + log.info("[JT-更新圆形区域] areaParam: {},", areaParam); + int result = service.setAreaForCircle(0, areaParam.getPhoneNumber(), areaParam.getCircleAreaList()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-追加圆形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "areaParam", description = "设置区域参数", required = true) + @PostMapping("/area/circle/add") + public WVPResult addAreaForCircle(@RequestBody SetAreaParam areaParam){ + + log.info("[JT-追加圆形区域] areaParam: {},", areaParam); + int result = service.setAreaForCircle(1, areaParam.getPhoneNumber(), areaParam.getCircleAreaList()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-修改圆形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "areaParam", description = "设置区域参数", required = true) + @PostMapping("/area/circle/edit") + public WVPResult editAreaForCircle(@RequestBody SetAreaParam areaParam){ + + log.info("[JT-修改圆形区域] areaParam: {},", areaParam); + int result = service.setAreaForCircle(2, areaParam.getPhoneNumber(), areaParam.getCircleAreaList()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-删除圆形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "ids", description = "待删除圆形区域的id,例如1,2,3", required = true) + @GetMapping("/area/circle/delete") + public WVPResult deleteAreaForCircle(String phoneNumber, @RequestParam(value = "ids", required = false) List ids){ + + log.info("[JT-删除圆形区域] phoneNumber: {}, ids:{}", phoneNumber, ids); + int result = service.deleteAreaForCircle(phoneNumber, ids); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-查询圆形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @GetMapping("/area/circle/query") + public WVPResult> queryAreaForCircle(String phoneNumber, @RequestParam(value = "ids", required = false) List ids){ + + log.info("[JT-查询圆形区域] phoneNumber: {}, ids:{}", phoneNumber, ids); + List result = service.queryAreaForCircle(phoneNumber, ids); + if (result != null) { + return WVPResult.success(result); + }else { + return WVPResult.fail(ErrorCode.ERROR100); + } + } + + + @Operation(summary = "JT-更新矩形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "areaParam", description = "设置区域参数", required = true) + @PostMapping("/area/rectangle/update") + public WVPResult updateAreaForRectangle(@RequestBody SetAreaParam areaParam){ + + log.info("[JT-更新矩形区域] areaParam: {},", areaParam); + int result = service.setAreaForRectangle(0, areaParam.getPhoneNumber(), areaParam.getRectangleAreas()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-追加矩形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "areaParam", description = "设置区域参数", required = true) + @PostMapping("/area/rectangle/add") + public WVPResult addAreaForRectangle(@RequestBody SetAreaParam areaParam){ + + log.info("[JT-追加矩形区域] areaParam: {},", areaParam); + int result = service.setAreaForRectangle(1, areaParam.getPhoneNumber(), areaParam.getRectangleAreas()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-修改矩形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "areaParam", description = "设置区域参数", required = true) + @PostMapping("/area/rectangle/edit") + public WVPResult editAreaForRectangle(@RequestBody SetAreaParam areaParam){ + + log.info("[JT-修改矩形区域] areaParam: {},", areaParam); + int result = service.setAreaForRectangle(2, areaParam.getPhoneNumber(), areaParam.getRectangleAreas()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-删除矩形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "ids", description = "待删除圆形区域的id,例如1,2,3", required = true) + @GetMapping("/area/rectangle/delete") + public WVPResult deleteAreaForRectangle(String phoneNumber, @RequestParam(value = "ids", required = false) List ids){ + + log.info("[JT-删除矩形区域] phoneNumber: {}, ids:{}", phoneNumber, ids); + int result = service.deleteAreaForRectangle(phoneNumber, ids); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-查询矩形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @GetMapping("/area/rectangle/query") + public WVPResult> queryAreaForRectangle(String phoneNumber, @RequestParam(value = "ids", required = false) List ids){ + + log.info("[JT-查询矩形区域] phoneNumber: {}, ids:{}", phoneNumber, ids); + List result = service.queryAreaForRectangle(phoneNumber, ids); + if (result != null) { + return WVPResult.success(result); + }else { + return WVPResult.fail(ErrorCode.ERROR100); + } + } + + @Operation(summary = "JT-设置多边形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "areaParam", description = "设置区域参数", required = true) + @PostMapping("/area/polygon/set") + public WVPResult setAreaForPolygon(@RequestBody SetAreaParam areaParam){ + + log.info("[JT-设置多边形区域] areaParam: {},", areaParam); + int result = service.setAreaForPolygon(areaParam.getPhoneNumber(), areaParam.getPolygonArea()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-删除多边形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "ids", description = "待删除圆形区域的id,例如1,2,3", required = true) + @GetMapping("/area/polygon/delete") + public WVPResult deleteAreaForPolygon(String phoneNumber, @RequestParam(value = "ids", required = false) List ids){ + + log.info("[JT-删除多边形区域] phoneNumber: {}, ids:{}", phoneNumber, ids); + int result = service.deleteAreaForPolygon(phoneNumber, ids); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-查询多边形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @GetMapping("/area/polygon/query") + public WVPResult> queryAreaForPolygon(String phoneNumber, @RequestParam(value = "ids", required = false) List ids){ + + log.info("[JT-查询多边形区域] phoneNumber: {}, ids:{}", phoneNumber, ids); + List result = service.queryAreaForPolygon(phoneNumber, ids); + if (result != null) { + return WVPResult.success(result); + }else { + return WVPResult.fail(ErrorCode.ERROR100); + } + } + + @Operation(summary = "JT-设置路线", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "areaParam", description = "设置区域参数", required = true) + @PostMapping("/route/set") + public WVPResult setRoute(@RequestBody SetAreaParam areaParam){ + + log.info("[JT-设置路线] areaParam: {},", areaParam); + int result = service.setRoute(areaParam.getPhoneNumber(), areaParam.getRoute()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-删除路线", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "ids", description = "待删除圆形区域的id,例如1,2,3", required = true) + @GetMapping("/route/delete") + public WVPResult deleteRoute(String phoneNumber, @RequestParam(value = "ids", required = false) List ids){ + + log.info("[JT-删除路线] phoneNumber: {}, ids:{}", phoneNumber, ids); + int result = service.deleteRoute(phoneNumber, ids); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-查询路线", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @GetMapping("/route/query") + public WVPResult> queryRoute(String phoneNumber, @RequestParam(value = "ids", required = false) List ids){ + + log.info("[JT-查询路线] phoneNumber: {}, ids:{}", phoneNumber, ids); + List result = service.queryRoute(phoneNumber, ids); + if (result != null) { + return WVPResult.success(result); + }else { + return WVPResult.fail(ErrorCode.ERROR100); + } + } + + // TODO 待实现 行驶记录数据采集命令 行驶记录数据上传 行驶记录参数下传命令 电子运单上报 CAN总线数据上传 + + @Operation(summary = "JT-上报驾驶员身份信息请求", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @GetMapping("/driver-information") + public WVPResult queryDriverInformation(String phoneNumber){ + + log.info("[JT-上报驾驶员身份信息请求] phoneNumber: {}", phoneNumber); + JTDriverInformation jtDriverInformation = service.queryDriverInformation(phoneNumber); + if (jtDriverInformation != null) { + return WVPResult.success(jtDriverInformation); + }else { + return WVPResult.fail(ErrorCode.ERROR100); + } + } + + @Operation(summary = "JT-摄像头立即拍摄命令", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @PostMapping("/shooting") + public WVPResult> shooting(@RequestBody ShootingParam param){ + + log.info("[JT-摄像头立即拍摄命令] param: {}", param ); + List ids = service.shooting(param.getPhoneNumber(), param.getShootingCommand()); + if (ids != null) { + return WVPResult.success(ids); + }else { + return WVPResult.fail(ErrorCode.ERROR100); + } + } + + @Operation(summary = "JT-抓图", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "channelId", description = "通道编号", required = true) + @GetMapping("/snap") + public void snap(HttpServletResponse response, String phoneNumber, Integer channelId){ + + log.info("[JT-抓图] 设备编号: {}, 通道编号: {}", phoneNumber, channelId ); + Assert.notNull(channelId, "缺少通道编号"); + try { + ServletOutputStream outputStream = response.getOutputStream(); + response.setContentType(MediaType.IMAGE_JPEG_VALUE); +// response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(phoneNumber + "_" + channelId + ".jpg", "UTF-8")); + byte[] data = service.snap(phoneNumber, channelId); + outputStream.write(data); + outputStream.flush(); + }catch (Exception e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + } + + @Operation(summary = "JT-存储多媒体数据检索", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "param", description = "存储多媒体数据参数", required = true) + @PostMapping("/media/list") + public WVPResult> queryMediaData(@RequestBody QueryMediaDataParam param){ + + log.info("[JT-存储多媒体数据检索] param: {}", param ); + List ids = service.queryMediaData(param.getPhoneNumber(), param.getQueryMediaDataCommand()); + if (ids != null) { + return WVPResult.success(ids); + }else { + return WVPResult.fail(ErrorCode.ERROR100); + } + } + + @Operation(summary = "JT-单条存储多媒体数据上传", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "mediaId", description = "多媒体ID", required = true) + @GetMapping("/media/upload/one/upload") + public void uploadOneMedia(HttpServletResponse response, String phoneNumber, Long mediaId){ + + log.info("[JT-单条存储多媒体数据上传] 设备编号: {}, 多媒体ID: {}", phoneNumber, mediaId ); + Assert.notNull(mediaId, "缺少通道编号"); + try { + ServletOutputStream outputStream = response.getOutputStream(); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + service.uploadOneMedia(phoneNumber, mediaId, outputStream, false); + }catch (Exception e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + } + + @Operation(summary = "JT-单条存储多媒体数据删除", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "mediaId", description = "多媒体ID", required = true) + @GetMapping("/media/upload/one/delete") + public void deleteOneMedia(HttpServletResponse response, String phoneNumber, Long mediaId){ + + log.info("[JT-单条存储多媒体数据上传] 设备编号: {}, 多媒体ID: {}", phoneNumber, mediaId ); + Assert.notNull(mediaId, "缺少通道编号"); + try { + ServletOutputStream outputStream = response.getOutputStream(); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + service.uploadOneMedia(phoneNumber, mediaId, outputStream, true); + }catch (Exception e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + } + +// @Operation(summary = "JT-存储多媒体数据上传命令", security = @SecurityRequirement(name = JwtUtils.HEADER)) +// @Parameter(name = "param", description = "存储多媒体数据参数", required = true) +// @PostMapping("/media-data-upload") +// public DeferredResult>> updateMediaData(@RequestBody QueryMediaDataParam param){ +// +// log.info("[JT-存储多媒体数据上传命令] param: {}", param ); +// DeferredResult>> deferredResult = new DeferredResult<>(30000L); +// List resultList = new ArrayList<>(); +// +// deferredResult.onTimeout(()->{ +// log.info("[JT-存储多媒体数据上传命令超时] param: {}", param ); +// WVPResult> fail = WVPResult.fail(ErrorCode.ERROR100); +// fail.setMsg("超时"); +// fail.setData(resultList); +// deferredResult.setResult(fail); +// }); +// List ids; +// if (param.getMediaId() != null) { +// ids = new ArrayList<>(); +// JTMediaDataInfo mediaDataInfo = new JTMediaDataInfo(); +// mediaDataInfo.setId(param.getMediaId()); +// ids.add(mediaDataInfo); +// }else { +// ids = service.queryMediaData(param.getPhoneNumber(), param.getQueryMediaDataCommand()); +// } +// if (ids.isEmpty()) { +// deferredResult.setResult(WVPResult.fail(ErrorCode.ERROR100)); +// return deferredResult; +// } +// Map idMap= new HashMap<>(); +// for (JTMediaDataInfo mediaDataInfo : ids) { +// idMap.put(mediaDataInfo.getId() + ".jpg", mediaDataInfo); +// } +// // 开启文件监听 +// FileAlterationObserver observer = new FileAlterationObserver(new File("mediaEvent")); +// observer.addListener(new FileAlterationListenerAdaptor() { +// @Override +// public void onFileCreate(File file) { +// if (idMap.containsKey(file.getName())) { +// idMap.remove(file.getName()); +// resultList.add("mediaEvent" + File.separator + file.getName()); +// if (idMap.isEmpty()) { +// deferredResult.setResult(WVPResult.success(resultList)); +// } +// } +// } +// }); +// FileAlterationMonitor monitor = new FileAlterationMonitor(5, observer); +// try { +// monitor.start(); +// } catch (Exception e) { +// log.info("[JT-存储多媒体数据上传命令监听文件失败] param: {}", param ); +// deferredResult.setResult(null); +// return deferredResult; +// } +// taskExecutor.execute(()->{ +// if (param.getMediaId() != null) { +// service.uploadMediaDataForSingle(param.getPhoneNumber(), param.getMediaId(), param.getDelete()); +// }else { +// service.uploadMediaData(param.getPhoneNumber(), param.getQueryMediaDataCommand()); +// } +// +// }); +// return deferredResult; +// } + + @Operation(summary = "JT-开始录音", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "time", description = "录音时间,单位为秒(s) ,0 表示一直录音", required = false) + @Parameter(name = "save", description = "0:实时上传;1:保存", required = false) + @Parameter(name = "samplingRate", description = "音频采样率, 0:8K;1:11K;2:23K;3:32K", required = false) + @GetMapping("/record/start") + public void startRecord(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = false) Integer time, + @Parameter(required = false) Integer save, + @Parameter(required = false) Integer samplingRate + ) { + if (ObjectUtils.isEmpty(time)) { + time = 0; + } + if (ObjectUtils.isEmpty(save)) { + save = 0; + } + if (ObjectUtils.isEmpty(samplingRate)) { + samplingRate = 0; + } + service.record(phoneNumber, 1, time, save, samplingRate); + } + + @Operation(summary = "JT-停止录音", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "time", description = "录音时间,单位为秒(s) ,0 表示一直录音", required = false) + @Parameter(name = "save", description = "0:实时上传;1:保存", required = false) + @Parameter(name = "samplingRate", description = "音频采样率, 0:8K;1:11K;2:23K;3:32K", required = false) + @GetMapping("/record/stop") + public void stopRecord(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = false) Integer time, + @Parameter(required = false) Integer save, + @Parameter(required = false) Integer samplingRate + ) { + if (ObjectUtils.isEmpty(time)) { + time = 0; + } + if (ObjectUtils.isEmpty(save)) { + save = 0; + } + if (ObjectUtils.isEmpty(samplingRate)) { + samplingRate = 0; + } + service.record(phoneNumber, 0, time, save, samplingRate); + } + + @Operation(summary = "JT-查询终端音视频属性", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @GetMapping("/media/attribute") + public JTMediaAttribute queryMediaAttribute( @Parameter(required = true) String phoneNumber + ) { + return service.queryMediaAttribute(phoneNumber); + } + + // TODO 视频报警上报 + + +} + diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/JT1078TerminalController.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/JT1078TerminalController.java new file mode 100644 index 0000000..58058e4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/JT1078TerminalController.java @@ -0,0 +1,120 @@ +package com.genersoft.iot.vmp.jt1078.controller; + +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.jt1078.bean.JTChannel; +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + + + +@Slf4j +@ConditionalOnProperty(value = "jt1078.enable", havingValue = "true") +@RestController +@Tag(name = "部标终端以及通道管理") +@RequestMapping("/api/jt1078/terminal") +public class JT1078TerminalController { + + @Resource + Ijt1078Service service; + + @Operation(summary = "JT-分页查询部标设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @GetMapping("/list") + public PageInfo getDevices(int page, int count, + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online) { + return service.getDeviceList(page, count, query, online); + } + + @Operation(summary = "更新设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "device", description = "设备", required = true) + @PostMapping("/update") + public void updateDevice(JTDevice device){ + assert device.getId() > 0; + assert device.getPhoneNumber() != null; + service.updateDevice(device); + } + + @Operation(summary = "JT-新增设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "device", description = "设备", required = true) + @PostMapping("/add") + public void addDevice(JTDevice device){ + assert device.getPhoneNumber() != null; + String phoneNumber = device.getPhoneNumber().replaceFirst("^0*", ""); + device.setPhoneNumber(phoneNumber); + service.addDevice(device); + } + @Operation(summary = "删除设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @DeleteMapping("/delete") + public void addDevice(String phoneNumber){ + assert phoneNumber != null; + service.deleteDeviceByPhoneNumber(phoneNumber); + } + @Operation(summary = "查询设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @GetMapping("/query") + public JTDevice getDevice(Integer deviceId){ + return service.getDeviceById(deviceId); + } + + + @Operation(summary = "JT-查询部标通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "deviceId", description = "设备ID", required = true) + @Parameter(name = "query", description = "查询内容") + @GetMapping("/channel/list") + public PageInfo getChannels(int page, int count, + @RequestParam(required = true) Integer deviceId, + @RequestParam(required = false) String query) { + assert deviceId != null; + return service.getChannelList(page, count, deviceId, query); + } + + @Operation(summary = "JT-查询单个部标通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "通道数据库ID", required = true) + @GetMapping("/channel/one") + public JTChannel getChannel(Integer id) { + assert id != null; + return service.getChannelByDbId(id); + } + + @Operation(summary = "JT-更新通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channel", description = "通道", required = true) + @PostMapping("/channel/update") + public void updateChannel(@RequestBody JTChannel channel){ + assert channel.getId() > 0; + assert channel.getChannelId() != null; + service.updateChannel(channel); + } + + @Operation(summary = "JT-新增通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channel", description = "通道", required = true) + @PostMapping("/channel/add") + public JTChannel addChannel(@RequestBody JTChannel channel){ + assert channel.getChannelId() != null; + assert channel.getTerminalDbId() != 0; + service.addChannel(channel); + return channel; + } + @Operation(summary = "JT-删除通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "通道的数据库ID", required = true) + @DeleteMapping("/channel/delete") + public void deleteChannel(Integer id){ + service.deleteChannelById(id); + } +} + diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ConfirmationAlarmMessageParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ConfirmationAlarmMessageParam.java new file mode 100644 index 0000000..10c3ece --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ConfirmationAlarmMessageParam.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.jt1078.controller.bean; + +import com.genersoft.iot.vmp.jt1078.bean.JTConfirmationAlarmMessageType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * 人工确认报警消息参数 + */ +@Setter +@Getter +@Schema(description = "人工确认报警消息参数") +public class ConfirmationAlarmMessageParam { + + @Schema(description = "终端手机号") + private String phoneNumber; + @Schema(description = "报警消息流水号") + private int alarmPackageNo; + @Schema(description = "人工确认报警类型") + private JTConfirmationAlarmMessageType alarmMessageType; + + @Override + public String toString() { + return "ConfirmationAlarmMessageParam{" + + "PhoneNumber='" + phoneNumber + '\'' + + ", alarmPackageNo=" + alarmPackageNo + + ", alarmMessageType=" + alarmMessageType + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ConnectionControlParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ConnectionControlParam.java new file mode 100644 index 0000000..235c47a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ConnectionControlParam.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.jt1078.controller.bean; + +import com.genersoft.iot.vmp.jt1078.bean.JTDeviceConnectionControl; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class ConnectionControlParam { + + @Schema(description = "终端手机号") + private String phoneNumber; + private JTDeviceConnectionControl control; + + @Override + public String toString() { + return "ConnectionControlParam{" + + "deviceId='" + phoneNumber + '\'' + + ", control=" + control + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/QueryMediaDataParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/QueryMediaDataParam.java new file mode 100644 index 0000000..debacab --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/QueryMediaDataParam.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.jt1078.controller.bean; + +import com.genersoft.iot.vmp.jt1078.bean.JTQueryMediaDataCommand; +import com.genersoft.iot.vmp.jt1078.bean.JTShootingCommand; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "存储多媒体数据参数") +public class QueryMediaDataParam { + + @Schema(description = "终端手机号") + private String phoneNumber; + + @Schema(description = "多媒体 ID, 单条存储多媒体数据检索上传时有效") + private Long mediaId; + + @Schema(description = "删除标志, 单条存储多媒体数据检索上传时有效") + private int delete; + + @Schema(description = "存储多媒体数据参数") + private JTQueryMediaDataCommand queryMediaDataCommand; + + @Override + public String toString() { + return "QueryMediaDataParam{" + + "设备手机号='" + phoneNumber + '\'' + + ", mediaId=" + mediaId + + ", queryMediaDataCommand=" + queryMediaDataCommand + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetAreaParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetAreaParam.java new file mode 100644 index 0000000..7a0cb5c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetAreaParam.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.controller.bean; + +import com.genersoft.iot.vmp.jt1078.bean.*; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Setter +@Getter +@Schema(description = "设置区域参数") +public class SetAreaParam { + + @Schema(description = "终端手机号") + private String phoneNumber; + + @Schema(description = "圆形区域项") + private List circleAreaList; + + @Schema(description = "矩形区域项") + private List rectangleAreas; + + @Schema(description = "多边形区域") + private JTPolygonArea polygonArea; + + @Schema(description = "路线") + private JTRoute route; + + + @Override + public String toString() { + return "SetAreaParam{" + + "设备手机号='" + phoneNumber + '\'' + + ", circleAreaList=" + circleAreaList + + ", rectangleAreas=" + rectangleAreas + + ", polygonArea=" + polygonArea + + ", route=" + route + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetConfigParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetConfigParam.java new file mode 100644 index 0000000..e6d0e2f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetConfigParam.java @@ -0,0 +1,26 @@ +package com.genersoft.iot.vmp.jt1078.controller.bean; + +import com.genersoft.iot.vmp.jt1078.bean.JTDeviceConfig; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "终端参数设置") +public class SetConfigParam { + + @Schema(description = "终端手机号") + private String phoneNumber; + + @Schema(description = "终端参数设置") + private JTDeviceConfig config; + + @Override + public String toString() { + return "SetConfigParam{" + + "phoneNumber='" + phoneNumber + '\'' + + ", config=" + config + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetPhoneBookParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetPhoneBookParam.java new file mode 100644 index 0000000..7493bea --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetPhoneBookParam.java @@ -0,0 +1,37 @@ +package com.genersoft.iot.vmp.jt1078.controller.bean; + +import com.genersoft.iot.vmp.jt1078.bean.JTPhoneBookContact; +import com.genersoft.iot.vmp.jt1078.bean.JTTextSign; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Setter +@Getter +@Schema(description = "设置电话本") +public class SetPhoneBookParam { + + @Schema(description = "终端手机号") + private String phoneNumber; + + @Schema(description = "设置类型:\n" + + "0: 删除终端上所有存储的联系人,\n" + + "1: 表示更新电话本, 删除终端中已有全部联系人并追加消息中的联系人,\n" + + "2: 表示追加电话本,\n" + + "3: 表示修改电话本$以联系人为索引") + private int type; + + @Schema(description = "联系人") + private List phoneBookContactList; + + @Override + public String toString() { + return "SetPhoneBookParam{" + + "设备手机号='" + phoneNumber + '\'' + + ", type=" + type + + ", phoneBookContactList=" + phoneBookContactList + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ShootingParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ShootingParam.java new file mode 100644 index 0000000..101749a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ShootingParam.java @@ -0,0 +1,26 @@ +package com.genersoft.iot.vmp.jt1078.controller.bean; + +import com.genersoft.iot.vmp.jt1078.bean.JTShootingCommand; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "摄像头立即拍摄命令参数") +public class ShootingParam { + + @Schema(description = "终端手机号") + private String phoneNumber; + + @Schema(description = "拍摄命令参数") + private JTShootingCommand shootingCommand; + + @Override + public String toString() { + return "ShootingParam{" + + "设备手机号='" + phoneNumber + '\'' + + ", shootingCommand=" + shootingCommand + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/TextMessageParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/TextMessageParam.java new file mode 100644 index 0000000..5df0f89 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/TextMessageParam.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.jt1078.controller.bean; + +import com.genersoft.iot.vmp.jt1078.bean.JTTextSign; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * 文本信息下发参数 + */ +@Setter +@Getter +@Schema(description = "人工确认报警消息参数") +public class TextMessageParam { + + @Schema(description = "终端手机号") + private String phoneNumber; + @Schema(description = "标志") + private JTTextSign sign; + @Schema(description = "文本类型,1 = 通知 ,2 = 服务") + private int textType; + @Schema(description = "消息内容,最长为1024字节") + private String content; + + @Override + public String toString() { + return "TextMessageParam{" + + "phoneNumber='" + phoneNumber + '\'' + + ", sign=" + sign + + ", textType=" + textType + + ", content='" + content + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/dao/JTChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/jt1078/dao/JTChannelMapper.java new file mode 100644 index 0000000..5781026 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/dao/JTChannelMapper.java @@ -0,0 +1,50 @@ +package com.genersoft.iot.vmp.jt1078.dao; + +import com.genersoft.iot.vmp.jt1078.bean.JTChannel; +import com.genersoft.iot.vmp.jt1078.dao.provider.JTChannelProvider; +import org.apache.ibatis.annotations.*; + +import java.util.List; + +@Mapper +public interface JTChannelMapper { + + @SelectProvider(type = JTChannelProvider.class, method = "selectAll") + List selectAll(@Param("terminalDbId") int terminalDbId, @Param("query") String query); + + @Update(value = {" "}) + void update(JTChannel channel); + + @Insert(value = {" "}) + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void add(JTChannel channel); + + @Delete("delete from wvp_jt_channel where id = #{id}") + void delete(@Param("id") int id); + + @SelectProvider(type = JTChannelProvider.class, method = "selectChannelByChannelId") + JTChannel selectChannelByChannelId(@Param("terminalDbId") int terminalDbId, @Param("channelId") Integer channelId); + + @SelectProvider(type = JTChannelProvider.class, method = "selectChannelById") + JTChannel selectChannelById(@Param("id") Integer id); +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/dao/JTTerminalMapper.java b/src/main/java/com/genersoft/iot/vmp/jt1078/dao/JTTerminalMapper.java new file mode 100644 index 0000000..c27a42e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/dao/JTTerminalMapper.java @@ -0,0 +1,115 @@ +package com.genersoft.iot.vmp.jt1078.dao; + +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import org.apache.ibatis.annotations.*; + +import java.util.List; + +@Mapper +public interface JTTerminalMapper { + + @Select("SELECT * FROM wvp_jt_terminal where phone_number=#{phoneNumber}") + JTDevice getDevice(@Param("phoneNumber") String phoneNumber); + + @Update(value = {" "}) + void updateDevice(JTDevice device); + @Select(value = {" "}) + List getDeviceList(@Param("query") String query, @Param("online") Boolean online); + + @Insert("INSERT INTO wvp_jt_terminal (" + + "terminal_id,"+ + "province_id,"+ + "province_text,"+ + "city_id,"+ + "city_text,"+ + "maker_id,"+ + "phone_number,"+ + "model,"+ + "plate_color,"+ + "plate_no,"+ + "longitude,"+ + "latitude,"+ + "create_time,"+ + "register_time,"+ + "update_time"+ + ") VALUES (" + + "#{terminalId}," + + "#{provinceId}," + + "#{provinceText}," + + "#{cityId}," + + "#{cityText}," + + "#{makerId}," + + "#{phoneNumber}," + + "#{model}," + + "#{plateColor}," + + "#{plateNo}," + + "#{longitude}," + + "#{latitude}," + + "#{createTime}," + + "#{registerTime}," + + "#{updateTime}" + + ")") + void addDevice(JTDevice device); + + @Delete("delete from wvp_jt_terminal where phone_number = #{phoneNumber}") + void deleteDeviceByPhoneNumber(@Param("phoneNumber") String phoneNumber); + + @Update(value = {" "}) + void updateDeviceStatus(@Param("connected") boolean connected, @Param("phoneNumber") String phoneNumber); + + @Select("SELECT * FROM wvp_jt_terminal where id=#{deviceId}") + JTDevice getDeviceById(@Param("deviceId") Integer deviceId); + + @Update({""}) + void batchUpdateDevicePosition(List devices); +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/dao/provider/JTChannelProvider.java b/src/main/java/com/genersoft/iot/vmp/jt1078/dao/provider/JTChannelProvider.java new file mode 100644 index 0000000..c06f0ec --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/dao/provider/JTChannelProvider.java @@ -0,0 +1,37 @@ +package com.genersoft.iot.vmp.jt1078.dao.provider; + +import java.util.Map; + +public class JTChannelProvider { + + public final static String BASE_SQL = + "SELECT jc.*, jc.id as data_device_id, wdc.*, wdc.id as gb_id " + + " from wvp_jt_channel jc " + + " LEFT join wvp_device_channel wdc " + + " on jc.id = wdc.data_device_id and wdc.data_type = 200 "; + + public String selectChannelByChannelId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" WHERE jc.terminal_db_id = #{terminalDbId} and jc.channel_id = #{channelId} "); + return sqlBuild.toString(); + } + public String selectChannelById(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" WHERE jc.id = #{id}"); + return sqlBuild.toString(); + } + public String selectAll(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" WHERE jc.terminal_db_id = #{terminalDbId} "); + if (params.get("query") != null) { + sqlBuild.append(" AND ") + .append(" jc.name LIKE ").append("'%").append(params.get("query")).append("%'") + ; + } + sqlBuild.append(" ORDER BY jc.channel_id "); + return sqlBuild.toString(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/event/ConnectChangeEvent.java b/src/main/java/com/genersoft/iot/vmp/jt1078/event/ConnectChangeEvent.java new file mode 100755 index 0000000..c298c2c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/event/ConnectChangeEvent.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.jt1078.event; + +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; +import java.time.Clock; + +/** + * 链接断或者连接的事件 + */ + +@Setter +@Getter +public class ConnectChangeEvent extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public ConnectChangeEvent(Object source) { + super(source); + } + + + private boolean connected; + + private String phoneNumber; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/event/DeviceUpdateEvent.java b/src/main/java/com/genersoft/iot/vmp/jt1078/event/DeviceUpdateEvent.java new file mode 100755 index 0000000..552236f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/event/DeviceUpdateEvent.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.jt1078.event; + +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; + +/** + * 设备更新事件 + */ +public class DeviceUpdateEvent extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public DeviceUpdateEvent(Object source) { + super(source); + } + + @Getter + @Setter + private JTDevice device; +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/event/FtpUploadEvent.java b/src/main/java/com/genersoft/iot/vmp/jt1078/event/FtpUploadEvent.java new file mode 100644 index 0000000..51c4d43 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/event/FtpUploadEvent.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.jt1078.event; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +@Setter +@Getter +public class FtpUploadEvent extends ApplicationEvent { + + public FtpUploadEvent(Object source) { + super(source); + } + + private String fileName; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/event/JTPositionEvent.java b/src/main/java/com/genersoft/iot/vmp/jt1078/event/JTPositionEvent.java new file mode 100755 index 0000000..4155ba9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/event/JTPositionEvent.java @@ -0,0 +1,30 @@ +package com.genersoft.iot.vmp.jt1078.event; + +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import com.genersoft.iot.vmp.jt1078.bean.JTPositionBaseInfo; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; + +/** + * 设备更新事件 + */ +public class JTPositionEvent extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public JTPositionEvent(Object source) { + super(source); + } + + @Getter + @Setter + private String phoneNumber; + + @Getter + @Setter + private JTPositionBaseInfo positionInfo; +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/event/eventListener/ConnectChangeEventListener.java b/src/main/java/com/genersoft/iot/vmp/jt1078/event/eventListener/ConnectChangeEventListener.java new file mode 100644 index 0000000..28fb289 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/event/eventListener/ConnectChangeEventListener.java @@ -0,0 +1,36 @@ +package com.genersoft.iot.vmp.jt1078.event.eventListener; + +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import com.genersoft.iot.vmp.jt1078.event.ConnectChangeEvent; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +@Component +public class ConnectChangeEventListener implements ApplicationListener { + + private final static Logger log = LoggerFactory.getLogger(ConnectChangeEventListener.class); + + @Autowired + private Ijt1078Service service; + + @Override + public void onApplicationEvent(ConnectChangeEvent event) { + if (event.isConnected()) { + log.info("[JT-设备已连接] 终端ID: {}", event.getPhoneNumber()); + }else{ + log.info("[JT-设备连接已断开] 终端ID: {}", event.getPhoneNumber()); + if(SessionManager.INSTANCE.get(event.getPhoneNumber()) != null) { + SessionManager.INSTANCE.get(event.getPhoneNumber()).unregister(); + } + } + JTDevice device = service.getDevice(event.getPhoneNumber()); + if (device != null) { + service.updateDeviceStatus(event.isConnected(), event.getPhoneNumber()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/Header.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/Header.java new file mode 100644 index 0000000..fdf8080 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/Header.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.jt1078.proc; + +import com.genersoft.iot.vmp.jt1078.util.Bin; +import lombok.Data; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:22 + * @email qingtaij@163.com + */ +@Data +public class Header { + // 消息ID + String msgId; + + // 消息体属性 + Integer msgPro; + + // 终端手机号 + String phoneNumber; + + // 消息体流水号 + Integer sn; + + // 协议版本号 + Short version = -1; + + + /** + * 判断是否是2019的版本 + * + * @return true 2019后的版本。false 2013 + */ + public boolean is2019Version() { + return Bin.get(msgPro, 14); + } + + @Override + public String toString() { + return "Header{" + + "消息ID='" + msgId + '\'' + + ", 消息体属性=" + msgPro + + ", 终端手机号='" + phoneNumber + '\'' + + ", 消息体流水号=" + sn + + ", 协议版本号=" + version + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/entity/Cmd.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/entity/Cmd.java new file mode 100644 index 0000000..d6c3a8e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/entity/Cmd.java @@ -0,0 +1,78 @@ +package com.genersoft.iot.vmp.jt1078.proc.entity; + +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import lombok.Data; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:23 + * @email qingtaij@163.com + */ +@Data +public class Cmd { + String phoneNumber; + Long packageNo; + String msgId; + String respId; + Rs rs; + + public Cmd() { + } + + public Cmd(Builder builder) { + this.phoneNumber = builder.phoneNumber; + this.packageNo = builder.packageNo; + this.msgId = builder.msgId; + this.respId = builder.respId; + this.rs = builder.rs; + } + + public static class Builder { + String phoneNumber; + Long packageNo; + String msgId; + String respId; + Rs rs; + + public Builder setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber.replaceFirst("^0*", ""); + return this; + } + + public Builder setPackageNo(Long packageNo) { + this.packageNo = packageNo; + return this; + } + + public Builder setMsgId(String msgId) { + this.msgId = msgId; + return this; + } + + public Builder setRespId(String respId) { + this.respId = respId; + return this; + } + + public Builder setRs(Rs re) { + this.rs = re; + return this; + } + + public Cmd build() { + return new Cmd(this); + } + } + + + @Override + public String toString() { + return "Cmd{" + + "devId='" + phoneNumber + '\'' + + ", packageNo=" + packageNo + + ", msgId='" + msgId + '\'' + + ", respId='" + respId + '\'' + + ", rs=" + rs + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/factory/CodecFactory.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/factory/CodecFactory.java new file mode 100644 index 0000000..8551e39 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/factory/CodecFactory.java @@ -0,0 +1,42 @@ +package com.genersoft.iot.vmp.jt1078.proc.factory; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.request.Re; +import com.genersoft.iot.vmp.jt1078.util.ClassUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:29 + * @email qingtaij@163.com + */ +@Slf4j +public class CodecFactory { + + private static Map> protocolHash; + + public static void init() { + protocolHash = new HashMap<>(); + List> classList = ClassUtil.getClassList("com.genersoft.iot.vmp.jt1078.proc", MsgId.class); + for (Class handlerClass : classList) { + String id = handlerClass.getAnnotation(MsgId.class).id(); + protocolHash.put(id, handlerClass); + } + if (log.isDebugEnabled()) { + log.debug("消息ID缓存表 protocolHash:{}", protocolHash); + } + } + + public static Re getHandler(String msgId) { + Class aClass = protocolHash.get(msgId); + Object bean = ClassUtil.getBean(aClass); + if (bean instanceof Re) { + return (Re) bean; + } + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0001.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0001.java new file mode 100644 index 0000000..c775e15 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0001.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +/** + * 终端通用应答 + * + * @author QingtaiJiang + * @date 2023/4/27 18:04 + * @email qingtaij@163.com + */ +@Getter +@MsgId(id = "0001") +public class J0001 extends Re { + int respNo; + String respId; + /** + * 0:成功/确认;1:失败;2:消息有误;3:不支持 + */ + int result; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + respNo = buf.readUnsignedShort(); + respId = ByteBufUtil.hexDump(buf.readSlice(2)); + result = buf.readUnsignedByte(); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0001", (long) respNo, result); + return null; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0002.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0002.java new file mode 100644 index 0000000..71a7523 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0002.java @@ -0,0 +1,42 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEvent; + +/** + * 终端心跳 + * + * @author QingtaiJiang + * @date 2023/4/27 18:04 + * @email qingtaij@163.com + */ +@Slf4j +@MsgId(id = "0002") +public class J0002 extends Re { + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + log.info("[终端心跳] {}", header.getPhoneNumber()); + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0003.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0003.java new file mode 100644 index 0000000..f32a4cb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0003.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEvent; + +/** + * 终端注销 + */ +@Slf4j +@Getter +@MsgId(id = "0003") +public class J0003 extends Re { + + int respNo; + String respId; + int result; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + respNo = buf.readUnsignedShort(); + respId = ByteBufUtil.hexDump(buf.readSlice(2)); + result = buf.readUnsignedByte(); + log.info("[JT-注销] 设备: {}", header.getPhoneNumber()); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0001", (long) respNo, result); + return null; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0004.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0004.java new file mode 100644 index 0000000..3db25ce --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0004.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import org.springframework.context.ApplicationEvent; + +/** + * 查询服务器时间 + * + * @author QingtaiJiang + * @date 2023/4/27 18:06 + * @email qingtaij@163.com + */ +@MsgId(id = "0004") +public class J0004 extends Re { + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + return null; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0100.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0100.java new file mode 100644 index 0000000..8be3abc --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0100.java @@ -0,0 +1,132 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.common.CivilCodePo; +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import com.genersoft.iot.vmp.jt1078.event.DeviceUpdateEvent; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8100; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.utils.CivilCodeUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEvent; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.UUID; + +/** + * 终端注册 + * + * @author QingtaiJiang + * @date 2023/4/27 18:06 + * @email qingtaij@163.com + */ +@Slf4j +@MsgId(id = "0100") +public class J0100 extends Re { + + private JTDevice device; + private JTDevice deviceForUpdate; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + Short version = header.getVersion(); + device = new JTDevice(); + device.setProvinceId(buf.readUnsignedShort() + ""); + if (version >= 1) { + device.setCityId(buf.readUnsignedShort() + ""); + // decode as 2019 + device.setMakerId(buf.readCharSequence(11, Charset.forName("GBK")) + .toString().trim()); + + device.setModel(buf.readCharSequence(30, Charset.forName("GBK")) + .toString().trim()); + + device.setTerminalId(buf.readCharSequence(30, Charset.forName("GBK")) + .toString().trim()); + + device.setPlateColor(buf.readByte()); + device.setPlateNo(buf.readCharSequence(buf.readableBytes(), Charset.forName("GBK")) + .toString().trim()); + } else { + // decode as 2013 + device.setCityId(buf.readUnsignedShort() + ""); + // decode as 2019 + byte[] bytes5 = new byte[5]; + buf.readBytes(bytes5); + device.setMakerId(new String(bytes5).trim()); + + byte[] bytes20 = new byte[20]; + buf.readBytes(bytes20); + device.setModel(new String(bytes20).trim()); + + byte[] bytes7 = new byte[7]; + buf.readBytes(bytes7); + device.setTerminalId(new String(bytes7).trim()); + + device.setPlateColor(buf.readByte()); + byte[] plateColorBytes = new byte[buf.readableBytes()]; + buf.readBytes(plateColorBytes); + try { + device.setPlateNo(new String(plateColorBytes, "GBK").trim()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8100 j8100 = new J8100(); + j8100.setRespNo(header.getSn()); + // 从数据库判断这个设备是否合法 + deviceForUpdate = service.getDevice(header.getPhoneNumber()); + if (deviceForUpdate != null) { + j8100.setResult(J8100.SUCCESS); + String authenticationCode = UUID.randomUUID().toString(); + j8100.setCode(authenticationCode); + session.setAuthenticationCode(authenticationCode); + deviceForUpdate.setStatus(true); + deviceForUpdate.setProvinceId(device.getProvinceId()); + deviceForUpdate.setRegisterTime(DateUtil.getNow()); + CivilCodePo provinceCivilCodePo = CivilCodeUtil.INSTANCE.get(device.getProvinceId()); + if (provinceCivilCodePo != null) { + deviceForUpdate.setProvinceText(provinceCivilCodePo.getName()); + } + deviceForUpdate.setCityId(device.getCityId()); + CivilCodePo cityCivilCodePo = CivilCodeUtil.INSTANCE.get(device.getProvinceId() + + String.format("%04d", Integer.parseInt(device.getCityId()))); + if (cityCivilCodePo != null) { + deviceForUpdate.setCityText(cityCivilCodePo.getName()); + } + deviceForUpdate.setModel(device.getModel()); + deviceForUpdate.setMakerId(device.getMakerId()); + deviceForUpdate.setTerminalId(device.getTerminalId()); + // TODO 支持直接展示车牌颜色的描述 + deviceForUpdate.setPlateColor(device.getPlateColor()); + deviceForUpdate.setPlateNo(device.getPlateNo()); + log.info("[JT-注册成功] 设备: {}", deviceForUpdate); + }else { + log.info("[JT-注册失败] 未授权设备: {}", header.getPhoneNumber()); + j8100.setResult(J8100.FAIL); + // 断开连接,清理资源 + if (session.isRegistered()) { + session.unregister(); + } + } + return j8100; + } + + @Override + public ApplicationEvent getEvent() { + DeviceUpdateEvent registerEvent = new DeviceUpdateEvent(this); + registerEvent.setDevice(deviceForUpdate); + return registerEvent; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0102.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0102.java new file mode 100644 index 0000000..8d3e2a7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0102.java @@ -0,0 +1,70 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import com.genersoft.iot.vmp.jt1078.event.DeviceUpdateEvent; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEvent; + +import java.nio.charset.Charset; + +/** + * 终端鉴权 + * + * @author QingtaiJiang + * @date 2023/4/27 18:06 + * @email qingtaij@163.com + */ +@Slf4j +@MsgId(id = "0102") +public class J0102 extends Re { + + private String authenticationCode; + private JTDevice deviceForUpdate; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + if (header.is2019Version()) { + int lenCode = buf.readUnsignedByte(); + authenticationCode = buf.readCharSequence(lenCode, Charset.forName("GBK")).toString(); + }else { + authenticationCode = buf.readCharSequence(buf.readableBytes(), Charset.forName("GBK")).toString(); + } + log.info("设备鉴权: authenticationCode: " + authenticationCode); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + if (session.getAuthenticationCode() == null || + !session.getAuthenticationCode().equals(authenticationCode)) { + j8001.setResult(J8001.FAIL); + }else { + j8001.setResult(J8001.SUCCESS); + JTDevice device = service.getDevice(header.getPhoneNumber()); + if (device != null && !device.isStatus()) { + deviceForUpdate = device; + deviceForUpdate.setStatus(true); + service.updateDevice(device); + } + } + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + DeviceUpdateEvent registerEvent = new DeviceUpdateEvent(this); + registerEvent.setDevice(deviceForUpdate); + return registerEvent; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0104.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0104.java new file mode 100644 index 0000000..fbd8dda --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0104.java @@ -0,0 +1,191 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTAlarmSign; +import com.genersoft.iot.vmp.jt1078.bean.JTDeviceConfig; +import com.genersoft.iot.vmp.jt1078.bean.common.ConfigAttribute; +import com.genersoft.iot.vmp.jt1078.bean.config.*; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.ApplicationEvent; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +/** + * 查询终端参数应答 + * + */ +@MsgId(id = "0104") +public class J0104 extends Re { + + Integer respNo; + Integer paramLength; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + respNo = buf.readUnsignedShort(); + paramLength = (int) buf.readUnsignedByte(); + if (paramLength <= 0) { + return null; + } + JTDeviceConfig deviceConfig = new JTDeviceConfig(); + Field[] fields = deviceConfig.getClass().getDeclaredFields(); + Map allFieldMap = new HashMap<>(); + Map allConfigAttributeMap = new HashMap<>(); + for (Field field : fields) { + ConfigAttribute configAttribute = field.getAnnotation(ConfigAttribute.class); + if (configAttribute != null) { + allFieldMap.put(configAttribute.id(), field); + allConfigAttributeMap.put(configAttribute.id(), configAttribute); + } + } + for (int i = 0; i < paramLength; i++) { + long id = buf.readUnsignedInt(); + if (!allFieldMap.containsKey(id)) { + continue; + } + short length = buf.readUnsignedByte(); + Field field = allFieldMap.get(id); + try { + + switch (allConfigAttributeMap.get(id).type()) { + case "Long": + Method methodForLong = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), Long.class); + methodForLong.invoke(deviceConfig, buf.readUnsignedInt()); + continue; + case "String": + Method methodForString = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), String.class); + String val = buf.readCharSequence(length, Charset.forName("GBK")).toString().trim(); + methodForString.invoke(deviceConfig, val); + continue; + case "Integer": + Method methodForInteger = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), Integer.class); + methodForInteger.invoke(deviceConfig, buf.readUnsignedShort()); + continue; + case "Short": + Method methodForShort = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), Short.class); + methodForShort.invoke(deviceConfig, buf.readUnsignedByte()); + continue; + case "IllegalDrivingPeriods": + JTIllegalDrivingPeriods illegalDrivingPeriods = new JTIllegalDrivingPeriods(); + int startHour = buf.readUnsignedByte(); + int startMinute = buf.readUnsignedByte(); + int stopHour = buf.readUnsignedByte(); + int stopMinute = buf.readUnsignedByte(); + illegalDrivingPeriods.setStartTime(startHour + ":" + startMinute); + illegalDrivingPeriods.setEndTime(stopHour + ":" + stopMinute); + Method methodForIllegalDrivingPeriods = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTIllegalDrivingPeriods.class); + methodForIllegalDrivingPeriods.invoke(deviceConfig, illegalDrivingPeriods); + continue; + case "CollisionAlarmParams": + JTCollisionAlarmParams collisionAlarmParams = new JTCollisionAlarmParams(); + collisionAlarmParams.setCollisionAlarmTime(buf.readUnsignedByte()); + collisionAlarmParams.setCollisionAcceleration(buf.readUnsignedByte()); + Method methodForCollisionAlarmParams = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTCollisionAlarmParams.class); + methodForCollisionAlarmParams.invoke(deviceConfig, collisionAlarmParams); + continue; + case "CameraTimer": + JTCameraTimer cameraTimer = new JTCameraTimer(); + long cameraTimerContent = buf.readUnsignedInt(); + cameraTimer.setSwitchForChannel1((cameraTimerContent & 1) == 1); + cameraTimer.setSwitchForChannel2((cameraTimerContent >>> 1 & 1) == 1); + cameraTimer.setSwitchForChannel3((cameraTimerContent >>> 2 & 1) == 1); + cameraTimer.setSwitchForChannel4((cameraTimerContent >>> 3 & 1) == 1); + cameraTimer.setSwitchForChannel5((cameraTimerContent >>> 4 & 1) == 1); + cameraTimer.setStorageFlagsForChannel1((cameraTimerContent >>> 7 & 1) == 1); + cameraTimer.setStorageFlagsForChannel2((cameraTimerContent >>> 8 & 1) == 1); + cameraTimer.setStorageFlagsForChannel3((cameraTimerContent >>> 9 & 1) == 1); + cameraTimer.setStorageFlagsForChannel4((cameraTimerContent >>> 10 & 1) == 1); + cameraTimer.setStorageFlagsForChannel5((cameraTimerContent >>> 11 & 1) == 1); + cameraTimer.setTimeUnit((cameraTimerContent >>> 15 & 1) == 1); + cameraTimer.setTimeInterval((int)cameraTimerContent >>> 16); + Method methodForCameraTimer = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTCameraTimer.class); + methodForCameraTimer.invoke(deviceConfig, cameraTimer); + continue; + case "GnssPositioningMode": + JTGnssPositioningMode gnssPositioningMode = new JTGnssPositioningMode(); + short gnssPositioningModeContent = buf.readUnsignedByte(); + gnssPositioningMode.setGps((gnssPositioningModeContent& 1) == 1); + gnssPositioningMode.setBeidou((gnssPositioningModeContent >>> 1 & 1) == 1); + gnssPositioningMode.setGlonass((gnssPositioningModeContent >>> 2 & 1) == 1); + gnssPositioningMode.setGaLiLeo((gnssPositioningModeContent >>> 3 & 1) == 1); + Method methodForGnssPositioningMode = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTGnssPositioningMode.class); + methodForGnssPositioningMode.invoke(deviceConfig, gnssPositioningMode); + continue; + case "VideoParam": + JTVideoParam videoParam = JTVideoParam.decode(buf); + Method methodForVideoParam = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTVideoParam.class); + methodForVideoParam.invoke(deviceConfig, videoParam); + continue; + case "ChannelListParam": + JTChannelListParam channelListParam = JTChannelListParam.decode(buf); + Method methodForChannelListParam = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTChannelListParam.class); + methodForChannelListParam.invoke(deviceConfig, channelListParam); + continue; + case "ChannelParam": + JTChannelParam channelParam = JTChannelParam.decode(buf); + Method methodForChannelParam = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTChannelParam.class); + methodForChannelParam.invoke(deviceConfig, channelParam); + continue; + case "AlarmRecordingParam": + JTAlarmRecordingParam alarmRecordingParam = JTAlarmRecordingParam.decode(buf); + Method methodForAlarmRecordingParam = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTAlarmRecordingParam.class); + methodForAlarmRecordingParam.invoke(deviceConfig, alarmRecordingParam); + continue; + case "VideoAlarmBit": + JTVideoAlarmBit videoAlarmBit = JTVideoAlarmBit.decode(buf); + Method methodForVideoAlarmBit = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTVideoAlarmBit.class); + methodForVideoAlarmBit.invoke(deviceConfig, videoAlarmBit); + continue; + case "AnalyzeAlarmParam": + JTAnalyzeAlarmParam analyzeAlarmParam = JTAnalyzeAlarmParam.decode(buf); + Method methodForAnalyzeAlarmParam = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTAnalyzeAlarmParam.class); + methodForAnalyzeAlarmParam.invoke(deviceConfig, analyzeAlarmParam); + continue; + case "AwakenParam": + JTAwakenParam awakenParamParam = JTAwakenParam.decode(buf); + Method methodForAwakenParam = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTAwakenParam.class); + methodForAwakenParam.invoke(deviceConfig, awakenParamParam); + continue; + case "AlarmSign": + JTAlarmSign alarmSign = JTAlarmSign.decode(buf); + Method methodForAlarmSign = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTAlarmSign.class); + methodForAlarmSign.invoke(deviceConfig, alarmSign); + continue; + default: + System.err.println(field.getGenericType().getTypeName()); + continue; + } + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0104", (long) respNo, deviceConfig); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0107.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0107.java new file mode 100644 index 0000000..6690b11 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0107.java @@ -0,0 +1,77 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +import java.nio.charset.Charset; + +/** + * 查询终端属性应答 + * + */ +@Slf4j +@MsgId(id = "0107") +public class J0107 extends Re { + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + + JTDeviceAttribute deviceAttribute = new JTDeviceAttribute(); + + deviceAttribute.setType(JTDeviceType.getInstance(buf.readUnsignedShort())); + + deviceAttribute.setMakerId(buf.readCharSequence(5, Charset.forName("GBK")).toString().trim()); + + deviceAttribute.setDeviceModel(buf.readCharSequence(20, Charset.forName("GBK")).toString().trim()); + if (header.is2019Version()) { + buf.readCharSequence(10, Charset.forName("GBK")); + } + + deviceAttribute.setTerminalId(buf.readCharSequence(7, Charset.forName("GBK")).toString().trim()); + if (header.is2019Version()) { + buf.readCharSequence(23, Charset.forName("GBK")); + } + + byte[] bytes = new byte[10]; + buf.readBytes(bytes); + deviceAttribute.setIccId(BCDUtil.transform(bytes)); + + int hardwareVersionLength = buf.readUnsignedByte(); + deviceAttribute.setHardwareVersion(buf.readCharSequence(hardwareVersionLength, Charset.forName("GBK")).toString().trim()); + + int firmwareVersionLength = buf.readUnsignedByte(); + deviceAttribute.setFirmwareVersion(buf.readCharSequence(firmwareVersionLength, Charset.forName("GBK")).toString().trim()); + + deviceAttribute.setGnssAttribute(JTGnssAttribute.getInstance(buf.readUnsignedByte())); + deviceAttribute.setCommunicationModuleAttribute(JTCommunicationModuleAttribute.getInstance(buf.readUnsignedByte())); + log.info("[查询终端属性应答] {}, {}", header.getPhoneNumber(), deviceAttribute); + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0107", null, deviceAttribute); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0200.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0200.java new file mode 100644 index 0000000..f420540 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0200.java @@ -0,0 +1,105 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +/** + * 位置信息汇报 + * + */ +@Slf4j +@MsgId(id = "0200") +public class J0200 extends Re { + + private JTPositionBaseInfo positionInfo; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + positionInfo = JTPositionBaseInfo.decode(buf); + log.debug("[JT-位置汇报]: phoneNumber={} {}", header.getPhoneNumber(), positionInfo.toSimpleString()); + // 读取附加信息 +// JTPositionAdditionalInfo positionAdditionalInfo = new JTPositionAdditionalInfo(); +// Map additionalMsg = new HashMap<>(); +// getAdditionalMsg(buf, positionAdditionalInfo); +// log.info("[JT-位置汇报]: phoneNumber={} {}", header.getPhoneNumber(), positionInfo.toSimpleString()); + return null; + } + + private void getAdditionalMsg(ByteBuf buf, JTPositionAdditionalInfo additionalInfo) { + + if (buf.isReadable()) { + int msgId = buf.readUnsignedByte(); + int length = buf.readUnsignedByte(); + ByteBuf byteBuf = buf.readBytes(length); + switch (msgId) { + case 1: + // 里程 + long mileage = byteBuf.readUnsignedInt(); + log.info("[JT-位置汇报]: 里程: {} km", (double)mileage/10); + break; + case 2: + // 油量 + int oil = byteBuf.readUnsignedShort(); + log.info("[JT-位置汇报]: 油量: {} L", (double)oil/10); + break; + case 3: + // 速度 + int speed = byteBuf.readUnsignedShort(); + log.info("[JT-位置汇报]: 速度: {} km/h", (double)speed/10); + break; + case 4: + // 需要人工确认报警事件的 ID + int alarmId = byteBuf.readUnsignedShort(); + log.info("[JT-位置汇报]: 需要人工确认报警事件的 ID: {}", alarmId); + break; + case 5: + byte[] tirePressureBytes = new byte[30]; + // 胎压 + byteBuf.readBytes(tirePressureBytes); + log.info("[JT-位置汇报]: 胎压 {}", tirePressureBytes); + break; + case 6: + // 车厢温度 + short carriageTemperature = byteBuf.readShort(); + log.info("[JT-位置汇报]: 车厢温度 {}摄氏度", carriageTemperature); + break; + case 11: + // 超速报警 + short positionType = byteBuf.readUnsignedByte(); + long positionId = byteBuf.readUnsignedInt(); + log.info("[JT-位置汇报]: 超速报警, 位置类型: {}, 区域或路段 ID: {}", positionType, positionId); + break; + default: + log.info("[JT-位置汇报]: 附加消息ID: {}, 消息长度: {}", msgId, length); + break; + + } + getAdditionalMsg(buf, additionalInfo); + } + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + service.updateDevicePosition(header.getPhoneNumber(), positionInfo.getLongitude(), positionInfo.getLatitude()); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0201.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0201.java new file mode 100644 index 0000000..100812e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0201.java @@ -0,0 +1,61 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.event.DeviceUpdateEvent; +import com.genersoft.iot.vmp.jt1078.event.JTPositionEvent; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +/** + * 位置信息查询应答 + * + * @author QingtaiJiang + * @date 2023/4/27 18:06 + * @email qingtaij@163.com + */ +@MsgId(id = "0201") +public class J0201 extends Re { + + private final static Logger log = LoggerFactory.getLogger(J0100.class); + private JTPositionBaseInfo positionInfo; + private String phoneNumber; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + phoneNumber = header.getPhoneNumber(); + int respNo = buf.readUnsignedShort(); + positionInfo = JTPositionBaseInfo.decode(buf); + log.info("[JT-位置信息查询应答]: {}", positionInfo); + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0201", (long) respNo, positionInfo); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + if (positionInfo == null || phoneNumber == null ) { + return null; + } + JTPositionEvent registerEvent = new JTPositionEvent(this); + registerEvent.setPhoneNumber(phoneNumber); + registerEvent.setPositionInfo(positionInfo); + return registerEvent; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0500.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0500.java new file mode 100644 index 0000000..58af88b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0500.java @@ -0,0 +1,58 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.event.JTPositionEvent; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +/** + * 车辆控制应答 + * + */ +@Slf4j +@MsgId(id = "0500") +public class J0500 extends Re { + + private JTPositionBaseInfo positionInfo; + private String phoneNumber; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + phoneNumber = header.getPhoneNumber(); + int respNo = buf.readUnsignedShort(); + positionInfo = JTPositionBaseInfo.decode(buf); + log.info("[车辆控制应答] {}", header.getPhoneNumber()); + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0500", (long) respNo, positionInfo); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + if (positionInfo == null || phoneNumber == null ) { + return null; + } + JTPositionEvent registerEvent = new JTPositionEvent(this); + registerEvent.setPhoneNumber(phoneNumber); + registerEvent.setPositionInfo(positionInfo); + return registerEvent; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0608.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0608.java new file mode 100644 index 0000000..e366579 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0608.java @@ -0,0 +1,100 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * 查询区域或线路数据应答 + */ +@Slf4j +@MsgId(id = "0608") +public class J0608 extends Re { + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + int type = buf.readByte(); + long dataLength = buf.readUnsignedInt(); + log.info("[JT-查询区域或线路数据应答]: 类型: {}, 数量: {}", type, dataLength); + List areaOrRoutes = new ArrayList<>(); + if (dataLength == 0) { + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0608", null, areaOrRoutes); + return null; + } + switch (type) { + case 1: + buf.readUnsignedByte(); + int areaLengthForCircleArea = buf.readUnsignedByte(); + List jtCircleAreas = new ArrayList<>(); + for (int i = 0; i < areaLengthForCircleArea; i++) { + // 查询圆形区域数据 + JTCircleArea jtCircleArea = JTCircleArea.decode(buf); + jtCircleAreas.add(jtCircleArea); + } + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0608", null, jtCircleAreas); + break; + case 2: + buf.readUnsignedByte(); + int areaLengthForRectangleArea = buf.readUnsignedByte(); + // 查询矩形区域数据 + List jtRectangleAreas = new ArrayList<>(); + for (int i = 0; i < areaLengthForRectangleArea; i++) { + // 查询圆形区域数据 + JTRectangleArea jtRectangleArea = JTRectangleArea.decode(buf); + jtRectangleAreas.add(jtRectangleArea); + } + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0608", null, jtRectangleAreas); + break; + case 3: + // 查询多 边形区域数据 + List jtPolygonAreas = new ArrayList<>(); + for (int i = 0; i < dataLength; i++) { + // 查询圆形区域数据 + JTPolygonArea jtRectangleArea = JTPolygonArea.decode(buf); + jtPolygonAreas.add(jtRectangleArea); + } + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0608", null, jtPolygonAreas); + break; + case 4: + // 查询线路数据 + List jtRoutes = new ArrayList<>(); + for (int i = 0; i < dataLength; i++) { + // 查询圆形区域数据 + JTRoute jtRoute = JTRoute.decode(buf); + jtRoutes.add(jtRoute); + } + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0608", null, jtRoutes); + break; + default: + break; + } + + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0702.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0702.java new file mode 100644 index 0000000..45ba170 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0702.java @@ -0,0 +1,45 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTDriverInformation; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +/** + * 驾驶员身份信息采集上报 + * + */ +@Slf4j +@MsgId(id = "0702") +public class J0702 extends Re { + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + JTDriverInformation driverInformation = JTDriverInformation.decode(buf, header.is2019Version()); + log.info("[JT-驾驶员身份信息采集上报]: {}", driverInformation.toString()); + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0702", null, driverInformation); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0704.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0704.java new file mode 100644 index 0000000..346977a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0704.java @@ -0,0 +1,57 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTDriverInformation; +import com.genersoft.iot.vmp.jt1078.bean.JTPositionBaseInfo; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * 定位数据批量上传 + */ +@Slf4j +@MsgId(id = "0704") +public class J0704 extends Re { + + private final List positionBaseInfoList = new ArrayList<>(); + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + int length = buf.readUnsignedShort(); + int type = buf.readUnsignedByte(); + for (int i = 0; i < length; i++) { + int dateLength = buf.readUnsignedShort(); + ByteBuf byteBuf = buf.readBytes(dateLength); + JTPositionBaseInfo positionInfo = JTPositionBaseInfo.decode(byteBuf); + byteBuf.release(); + positionBaseInfoList.add(positionInfo); + } + log.info("[JT-定位数据批量上传]: 共{}条", positionBaseInfoList.size()); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0800.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0800.java new file mode 100644 index 0000000..74ee4f8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0800.java @@ -0,0 +1,47 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTMediaEventInfo; +import com.genersoft.iot.vmp.jt1078.bean.JTPositionBaseInfo; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * 多媒体事件信息上传 + * + */ +@Slf4j +@MsgId(id = "0800") +public class J0800 extends Re { + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + JTMediaEventInfo mediaEventInfo = JTMediaEventInfo.decode(buf); + log.info("[JT-多媒体事件信息上传]: {}", mediaEventInfo); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0801.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0801.java new file mode 100644 index 0000000..d24579b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0801.java @@ -0,0 +1,56 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTMediaEventInfo; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEvent; + +/** + * 多媒体数据上传 + */ +@Slf4j +@MsgId(id = "0801") +public class J0801 extends Re { + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + JTMediaEventInfo mediaEventInfo = JTMediaEventInfo.decode(buf); + log.info("[JT-多媒体数据上传]: {}", mediaEventInfo); +// try { +// if (mediaEventInfo.getMediaData() != null) { +// File file = new File("./source.jpg"); +// if (file.exists()) { +// file.delete(); +// } +// FileOutputStream fileOutputStream = new FileOutputStream(file); +// fileOutputStream.write(mediaEventInfo.getMediaData()); +// fileOutputStream.flush(); +// fileOutputStream.close(); +// } +// }catch (Exception e) { +// log.error("[JT-多媒体数据上传] 写入文件异常", e); +// } + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0801", null, mediaEventInfo); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0802.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0802.java new file mode 100644 index 0000000..ee18d0a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0802.java @@ -0,0 +1,58 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTMediaDataInfo; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * 存储多媒体数据检索应答 + * + */ +@Slf4j +@MsgId(id = "0802") +public class J0802 extends Re { + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + int respNo = buf.readUnsignedShort(); + int length = buf.readUnsignedShort(); + if (length == 0) { + log.info("[JT-存储多媒体数据检索应答]: {}", length); + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0802", (long) respNo, new ArrayList<>()); + return null; + } + List mediaDataInfoList = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + mediaDataInfoList.add(JTMediaDataInfo.decode(buf)); + } + log.info("[JT-存储多媒体数据检索应答]: {}", mediaDataInfoList.size()); + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0802", (long) respNo, mediaDataInfoList); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0805.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0805.java new file mode 100644 index 0000000..d0a72ef --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0805.java @@ -0,0 +1,60 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * 摄像头立即拍摄命令应答 + */ +@Setter +@Getter +@MsgId(id = "0805") +public class J0805 extends Re { + + private int respNo; + /** + * 0:成功/确认;1:失败;2:消息有误;3:不支持 + */ + private int result; + + /** + * 表示拍摄成功的多媒体个数 + */ + private List ids = new ArrayList<>(); + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + respNo = buf.readUnsignedShort(); + result = buf.readUnsignedByte(); + if (result == 0) { + int length = buf.readUnsignedShort(); + for (int i = 0; i < length; i++) { + ids.add(buf.readUnsignedInt()); + } + } + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0805", null, ids); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0001", (long) respNo, result); + return null; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0900.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0900.java new file mode 100644 index 0000000..9271592 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0900.java @@ -0,0 +1,55 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +/** + * 数据上行透传 + */ +@Setter +@Getter +@MsgId(id = "0900") +public class J0900 extends Re { + + /** + * 透传消息类型, 0x00: GNSS 模块详细定位数据, 0X0B: 道路运输证 IC卡信息, 0X41: 串口1 透传, 0X42: 串口2 透传, 0XF0 ~ 0XFF: 用户自定义透传 + */ + private Integer type; + + /** + * 透传消息内容 + */ + private byte[] content; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + type = (int)buf.readUnsignedByte(); + byte[] content = new byte[buf.readableBytes()]; + buf.readBytes(content); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return null; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0901.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0901.java new file mode 100644 index 0000000..521c4a3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0901.java @@ -0,0 +1,53 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +/** + * 数据压缩上报 + */ +@Setter +@Getter +@MsgId(id = "0901") +public class J0901 extends Re { + + /** + * 平台 RSA公钥{e ,n}中的 e + */ + private Long e; + + /** + * RSA公钥{e ,n}中的 n + */ + private byte[] n; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + e = buf.readUnsignedInt(); + byte[] content = new byte[buf.readableBytes()]; + buf.readBytes(content); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0A00.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0A00.java new file mode 100644 index 0000000..582691f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0A00.java @@ -0,0 +1,54 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +/** + * 终端 RSA公钥 + */ +@Setter +@Getter +@MsgId(id = "0900") +public class J0A00 extends Re { + + /** + * 透传消息类型, 0x00: GNSS 模块详细定位数据, 0X0B: 道路运输证 IC卡信息, 0X41: 串口1 透传, 0X42: 串口2 透传, 0XF0 ~ 0XFF: 用户自定义透传 + */ + + private Integer type; + + /** + * 透传消息内容 + */ + private byte[] content; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + type = (int)buf.readUnsignedByte(); + byte[] content = new byte[buf.readableBytes()]; + buf.readBytes(content); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return null; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1003.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1003.java new file mode 100644 index 0000000..570f904 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1003.java @@ -0,0 +1,43 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTMediaAttribute; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import org.springframework.context.ApplicationEvent; + +/** + * 终端上传音视频属性 + * + */ +@MsgId(id = "1003") +public class J1003 extends Re { + + JTMediaAttribute mediaAttribute; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + mediaAttribute = JTMediaAttribute.decode(buf); + SessionManager.INSTANCE.response(header.getPhoneNumber(), "1003", null, mediaAttribute); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1005.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1005.java new file mode 100644 index 0000000..20ac8ae --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1005.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTMediaAttribute; +import com.genersoft.iot.vmp.jt1078.bean.JTPassengerNum; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEvent; + +/** + * 终端上传乘客流量 + * + */ +@Slf4j +@MsgId(id = "1005") +public class J1005 extends Re { + + JTPassengerNum passengerNum; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + passengerNum = JTPassengerNum.decode(buf); + log.info("[终端上传乘客流量] {}", passengerNum); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1205.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1205.java new file mode 100644 index 0000000..bbe33a5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1205.java @@ -0,0 +1,124 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * 终端上传音视频资源列表 + * + * @author QingtaiJiang + * @date 2023/4/28 10:36 + * @email qingtaij@163.com + */ +@Setter +@Getter +@MsgId(id = "1205") +public class J1205 extends Re { + + Integer respNo; + + private List recordList = new ArrayList<>(); + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + respNo = buf.readUnsignedShort(); + long size = buf.readUnsignedInt(); + + for (int i = 0; i < size; i++) { + JRecordItem item = new JRecordItem(); + item.setChannelId(buf.readUnsignedByte()); + String startTime = ByteBufUtil.hexDump(buf.readSlice(6)); + item.setStartTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(startTime)); + String endTime = ByteBufUtil.hexDump(buf.readSlice(6)); + item.setEndTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(endTime)); + item.setAlarmSign(buf.readLong()); + item.setMediaType(buf.readUnsignedByte()); + item.setStreamType(buf.readUnsignedByte()); + item.setStorageType(buf.readUnsignedByte()); + item.setSize(buf.readUnsignedInt()); + recordList.add(item); + } + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + SessionManager.INSTANCE.response(header.getPhoneNumber(), "1205", (long) respNo, recordList); + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + + @Setter + @Getter + public static class JRecordItem { + + // 逻辑通道号 + private int channelId; + + // 开始时间 + private String startTime; + + // 结束时间 + private String endTime; + + // 报警标志 + private long alarmSign; + + // 音视频资源类型 + private int mediaType; + + // 码流类型 + private int streamType = 1; + + // 存储器类型 + private int storageType; + + // 文件大小 + private long size; + + @Override + public String toString() { + return "JRecordItem{" + + "channelId=" + channelId + + ", startTime='" + startTime + '\'' + + ", endTime='" + endTime + '\'' + + ", warn=" + alarmSign + + ", mediaType=" + mediaType + + ", streamType=" + streamType + + ", storageType=" + storageType + + ", size=" + size + + '}'; + } + } + + @Override + public String toString() { + return "J1205{" + + "respNo=" + respNo + + ", recordList=" + recordList + + '}'; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1206.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1206.java new file mode 100644 index 0000000..efef164 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1206.java @@ -0,0 +1,63 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * 文件上传完成通知 + * + */ +@Setter +@Getter +@MsgId(id = "1206") +public class J1206 extends Re { + + Integer respNo; + + // 0:成功; 1:失败 + private int result; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + respNo = buf.readUnsignedShort(); + result = buf.readUnsignedByte(); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + + @Override + public String toString() { + return "J1206{" + + "respNo=" + respNo + + ", result=" + result + + '}'; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/Re.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/Re.java new file mode 100644 index 0000000..03a66f4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/Re.java @@ -0,0 +1,45 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:50 + * @email qingtaij@163.com + */ +@Slf4j +public abstract class Re { + + protected abstract Rs decode0(ByteBuf buf, Header header, Session session); + + protected abstract Rs handler(Header header, Session session, Ijt1078Service service); + + public Rs decode(ByteBuf buf, Header header, Session session, Ijt1078Service service) { + if (session != null && !StringUtils.hasLength(session.getPhoneNumber())) { + session.register(header.getPhoneNumber(), (int) header.getVersion(), header); + } + Rs rs = decode0(buf, header, session); + buf.release(); + Rs rsHand = handler(header, session, service); + if (rs == null && rsHand != null) { + rs = rsHand; + } else if (rs != null && rsHand != null) { + log.warn("decode0:{} 与 handler:{} 返回值冲突,采用decode0返回值", rs, rsHand); + } + if (rs != null) { + rs.setHeader(header); + } + return rs; + } + + public abstract ApplicationEvent getEvent(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8001.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8001.java new file mode 100644 index 0000000..07d3aba --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8001.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import lombok.Setter; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:48 + * @email qingtaij@163.com + */ +@Setter +@MsgId(id = "8001") +public class J8001 extends Rs { + + public static final Integer SUCCESS = 0; + + public static final Integer FAIL = 1; + + public static final Integer ERROR = 2; + public static final Integer NOT_SUPPORTED = 3; + public static final Integer ALARM_ACK = 3; + + Integer respNo; + String respId; + Integer result; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeShort(respNo); + buffer.writeBytes(ByteBufUtil.decodeHexDump(respId)); + buffer.writeByte(result); + + return buffer; + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8100.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8100.java new file mode 100644 index 0000000..af92c21 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8100.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import lombok.Setter; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:40 + * @email qingtaij@163.com + */ +@Setter +@MsgId(id = "8100") +public class J8100 extends Rs { + /** + * 0 成功 + * 1 车辆已被注册 + * 2 数据库中无该车辆 + * 3 终端已被注册 + * 4 数据库中无该终端 + */ + public static final Integer SUCCESS = 0; + public static final Integer FAIL = 4; + + Integer respNo; + Integer result; + String code; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeShort(respNo); + buffer.writeByte(result); + buffer.writeCharSequence(code, CharsetUtil.UTF_8); + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8103.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8103.java new file mode 100644 index 0000000..04db44b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8103.java @@ -0,0 +1,127 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTDeviceConfig; +import com.genersoft.iot.vmp.jt1078.bean.common.ConfigAttribute; +import com.genersoft.iot.vmp.jt1078.bean.config.*; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * 设置终端参数 + */ +@Getter +@MsgId(id = "8103") +public class J8103 extends Rs { + + private final static Logger log = LoggerFactory.getLogger(J8103.class); + + private JTDeviceConfig config; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + Class configClass = config.getClass(); + Field[] declaredFields = configClass.getDeclaredFields(); + Map fieldConfigAttributeMap = new HashMap<>(); + for (Field field : declaredFields) { + try{ + Method method = configClass.getDeclaredMethod("get" + StringUtils.capitalize(field.getName())); + Object invoke = method.invoke(config); + if (invoke == null) { + continue; + } + ConfigAttribute configAttribute = field.getAnnotation(ConfigAttribute.class); + if (configAttribute != null) { + fieldConfigAttributeMap.put(field, configAttribute); + } + }catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { + log.error("[设置终端参数 ] 编码失败", e ); + } + + } + buffer.writeByte(fieldConfigAttributeMap.size()); + + if (!fieldConfigAttributeMap.isEmpty()) { + for (Field field : fieldConfigAttributeMap.keySet()) { + try{ + ConfigAttribute configAttribute = fieldConfigAttributeMap.get(field); + buffer.writeInt((int) (configAttribute.id() & 0xffff)); + switch (configAttribute.type()) { + case "Long": + buffer.writeByte(4); + field.setAccessible(true); + long longVal = (long)field.get(config); + buffer.writeInt((int) (longVal & 0xffffffffL)); + continue; + case "String": + field.setAccessible(true); + String stringVal = (String)field.get(config); + buffer.writeByte(stringVal.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(stringVal, Charset.forName("GBK")); + continue; + case "Integer": + buffer.writeByte(2); + field.setAccessible(true); + Integer integerVal = (Integer)field.get(config); + buffer.writeShort((short)(integerVal & 0xffff)); + continue; + case "Short": + buffer.writeByte(1); + field.setAccessible(true); + Short shortVal = (Short)field.get(config); + buffer.writeByte((int) (shortVal & 0xff)); + continue; + case "IllegalDrivingPeriods": + case "CollisionAlarmParams": + case "CameraTimer": + case "GnssPositioningMode": + case "VideoParam": + case "ChannelListParam": + case "ChannelParam": + case "AlarmRecordingParam": + case "AlarmShielding": + case "VideoAlarmBit": + case "AnalyzeAlarmParam": + case "AwakenParam": + case "AlarmSign": + field.setAccessible(true); + JTDeviceSubConfig subConfig = (JTDeviceSubConfig)field.get(config); + ByteBuf byteBuf = subConfig.encode(); + buffer.writeByte(byteBuf.readableBytes()); + buffer.writeBytes(byteBuf); + continue; + } + }catch (Exception e) { + log.error("[设置终端参数 ] 编码失败", e ); + } + } + } + return buffer; + } + + public void setConfig(JTDeviceConfig config) { + this.config = config; + } + + @Override + public String toString() { + return "J8103{" + + "config=" + config + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8104.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8104.java new file mode 100644 index 0000000..c4d6012 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8104.java @@ -0,0 +1,20 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.util.Arrays; + +/** + * 查询终端参数 + */ +@MsgId(id = "8104") +public class J8104 extends Rs { + + @Override + public ByteBuf encode() { + return Unpooled.buffer(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8105.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8105.java new file mode 100644 index 0000000..5e59096 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8105.java @@ -0,0 +1,81 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTDeviceConnectionControl; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; + +import java.nio.charset.Charset; +import java.util.Arrays; + +/** + * 终端控制 + */ +@Getter +@MsgId(id = "8105") +public class J8105 extends Rs { + + private JTDeviceConnectionControl connectionControl; + + /** + * 终端复位 + */ + private Boolean reset; + + /** + * 终端恢复出厂设置 + */ + private Boolean factoryReset; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + if (reset != null) { + byteBuf.writeByte(4); + }else if (factoryReset != null) { + byteBuf.writeByte(5); + }else if (connectionControl != null) { + byteBuf.writeByte(2); + StringBuffer stringBuffer = new StringBuffer(); + if (connectionControl.getSwitchOn() != null) { + if (connectionControl.getSwitchOn()) { + stringBuffer.append("1"); + }else { + stringBuffer.append("0"); + stringBuffer.append(";" + connectionControl.getAuthentication()) + .append(";" + connectionControl.getName()) + .append(";" + connectionControl.getUsername()) + .append(";" + connectionControl.getPassword()) + .append(";" + connectionControl.getAddress()) + .append(";" + connectionControl.getTcpPort()) + .append(";" + connectionControl.getUdpPort()) + .append(";" + connectionControl.getTimeLimit()); + } + } + byteBuf.writeCharSequence(stringBuffer.toString(), Charset.forName("GBK")); + } + return byteBuf; + } + + public void setConnectionControl(JTDeviceConnectionControl connectionControl) { + this.connectionControl = connectionControl; + } + + public void setReset(Boolean reset) { + this.reset = reset; + } + + public void setFactoryReset(Boolean factoryReset) { + this.factoryReset = factoryReset; + } + + @Override + public String toString() { + return "J8105{" + + "connectionControl=" + connectionControl + + ", reset=" + reset + + ", factoryReset=" + factoryReset + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8106.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8106.java new file mode 100644 index 0000000..4c7fe9d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8106.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 查询指定终端参数 + */ +@Getter +@MsgId(id = "8106") +public class J8106 extends Rs { + + private long[] params; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(params.length); + for (long param : params) { + buffer.writeInt((int) param); + } + return buffer; + } + + public void setParams(long[] params) { + this.params = params; + } + + @Override + public String toString() { + return "J8106{" + + "params=" + Arrays.toString(params) + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8107.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8107.java new file mode 100644 index 0000000..a516d2c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8107.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +/** + * 查询终端属性 + */ +@MsgId(id = "8107") +public class J8107 extends Rs { + + @Override + public ByteBuf encode() { + return Unpooled.buffer(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8201.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8201.java new file mode 100644 index 0000000..1d360b0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8201.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +/** + * 位置信息查询 + */ +@MsgId(id = "8201") +public class J8201 extends Rs { + + @Override + public ByteBuf encode() { + return Unpooled.buffer(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8202.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8202.java new file mode 100644 index 0000000..1c6ff8a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8202.java @@ -0,0 +1,37 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 临时位置跟踪控制 + */ +@Setter +@Getter +@MsgId(id = "8202") +public class J8202 extends Rs { + + /** + * 时间间隔,单位为秒,时间间隔为0 时停止跟踪,停止跟踪无需带后继字段 + */ + private int timeInterval; + + /** + * 位置跟踪有效期, 单位为秒,终端在接收到位置跟踪控制消息后,在有效期截止时间之前依据消息中的时间间隔发送位置汇报 + */ + private long validityPeriod; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeShort((short)(timeInterval & 0xffff)); + if (timeInterval > 0) { + buffer.writeInt((int) (validityPeriod & 0xffffffffL)); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8203.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8203.java new file mode 100644 index 0000000..d537c4f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8203.java @@ -0,0 +1,37 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTConfirmationAlarmMessageType; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 人工确认报警消息 + */ +@Setter +@Getter +@MsgId(id = "8203") +public class J8203 extends Rs { + + /** + * 报警消息流水号 + */ + private int alarmPackageNo; + /** + * 人工确认报警类型 + */ + private JTConfirmationAlarmMessageType alarmMessageType; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeShort((short)(alarmPackageNo & 0xffff)); + if (alarmMessageType != null) { + buffer.writeInt((int) (alarmMessageType.encode() & 0xffffffffL)); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8204.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8204.java new file mode 100644 index 0000000..7455301 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8204.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +/** + * 链路检测 + */ +@MsgId(id = "8204") +public class J8204 extends Rs { + + @Override + public ByteBuf encode() { + return Unpooled.buffer(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8300.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8300.java new file mode 100644 index 0000000..59e3e6e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8300.java @@ -0,0 +1,43 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTTextSign; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.nio.charset.Charset; + +/** + * 文本信息下发 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@MsgId(id = "8300") +public class J8300 extends Rs { + + /** + * 标志 + */ + private JTTextSign sign; + + /** + * 文本类型1 = 通知 ,2 = 服务 + */ + private int textType; + + /** + * 文本信息 + */ + private String content; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(sign.encode()); + buffer.writeByte(textType); + buffer.writeCharSequence(content, Charset.forName("GBK")); + return buffer; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8400.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8400.java new file mode 100644 index 0000000..8779118 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8400.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTTextSign; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; + +/** + * 电话回拨 + */ +@Setter +@Getter +@MsgId(id = "8400") +public class J8400 extends Rs { + + /** + * 标志, 0'普通通话,1'监听 + */ + private int sign; + + /** + * 电话号码 + */ + private String phoneNumber; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(sign); + buffer.writeCharSequence(phoneNumber, Charset.forName("GBK")); + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8401.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8401.java new file mode 100644 index 0000000..960fe5e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8401.java @@ -0,0 +1,50 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTPhoneBookContact; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; +import java.util.List; + +/** + * 设置电话本 + */ +@Setter +@Getter +@MsgId(id = "8401") +public class J8401 extends Rs { + + /** + * 设置类型: + * 0: 删除终端上所有存储的联系人, + * 1: 表示更新电话本$ 删除终端中已有全部联系人并追加消 息中的联系人, + * 2: 表示追加电话本, + * 3: 表示修改电话本$以联系人为索引 + */ + private int type; + + /** + * 联系人 + */ + private List phoneBookContactList; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(type); + if (phoneBookContactList != null && !phoneBookContactList.isEmpty()) { + buffer.writeByte(phoneBookContactList.size()); + for (JTPhoneBookContact jtPhoneBookContact : phoneBookContactList) { + buffer.writeBytes(jtPhoneBookContact.encode()); + } + }else { + buffer.writeByte(0); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8500.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8500.java new file mode 100644 index 0000000..047ed8b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8500.java @@ -0,0 +1,67 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTPhoneBookContact; +import com.genersoft.iot.vmp.jt1078.bean.JTVehicleControl; +import com.genersoft.iot.vmp.jt1078.bean.common.ConfigAttribute; +import com.genersoft.iot.vmp.jt1078.bean.config.JTDeviceSubConfig; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.lang.reflect.Field; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 车辆控制 + */ +@Setter +@Getter +@MsgId(id = "8500") +public class J8500 extends Rs { + + /** + * 控制类型 + */ + private JTVehicleControl vehicleControl; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeShort((short)(vehicleControl.getLength() & 0xffff)); + + Field[] fields = vehicleControl.getClass().getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + Object value = null; + try { + value = field.get(vehicleControl); + if (value == null) { + continue; + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + + ConfigAttribute configAttribute = field.getAnnotation(ConfigAttribute.class); + if (configAttribute == null) { + continue; + } + buffer.writeShort((short)(configAttribute.id() & 0xffff)); + switch (configAttribute.type()) { + case "Byte": + field.setAccessible(true); + buffer.writeByte((int)value); + continue; + } + } + + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8600.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8600.java new file mode 100644 index 0000000..8fc9063 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8600.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTCircleArea; +import com.genersoft.iot.vmp.jt1078.bean.JTVehicleControl; +import com.genersoft.iot.vmp.jt1078.bean.common.ConfigAttribute; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.lang.reflect.Field; +import java.util.List; + +/** + * 设置圆形区域 + */ +@Setter +@Getter +@MsgId(id = "8600") +public class J8600 extends Rs { + + /** + * 设置属性, 0:更新区域; 1:追加区域; 2:修改区域 + */ + private int attribute; + + /** + * 区域项 + */ + private List circleAreaList; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(attribute); + buffer.writeByte(circleAreaList.size()); + if (circleAreaList.isEmpty()) { + return buffer; + } + for (JTCircleArea circleArea : circleAreaList) { + buffer.writeBytes(circleArea.encode()); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8601.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8601.java new file mode 100644 index 0000000..2d0a7f0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8601.java @@ -0,0 +1,42 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTCircleArea; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 删除圆形区域 + */ +@Setter +@Getter +@MsgId(id = "8601") +public class J8601 extends Rs { + + + /** + * 待删除的区域ID + */ + private List idList; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + if (idList == null || idList.isEmpty()) { + buffer.writeByte(0); + return buffer; + }else { + buffer.writeByte(idList.size()); + } + for (Long id : idList) { + buffer.writeInt((int) (id & 0xffffffffL)); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8602.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8602.java new file mode 100644 index 0000000..bda25b1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8602.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTCircleArea; +import com.genersoft.iot.vmp.jt1078.bean.JTRectangleArea; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 设置矩形区域 + */ +@Setter +@Getter +@MsgId(id = "8602") +public class J8602 extends Rs { + + /** + * 设置属性, 0:更新区域; 1:追加区域; 2:修改区域 + */ + private int attribute; + + /** + * 区域项 + */ + private List rectangleAreas; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(attribute); + buffer.writeByte(rectangleAreas.size()); + if (rectangleAreas.isEmpty()) { + return buffer; + } + for (JTRectangleArea area : rectangleAreas) { + buffer.writeBytes(area.encode()); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8603.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8603.java new file mode 100644 index 0000000..2b5dbbf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8603.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 删除矩形区域 + */ +@Setter +@Getter +@MsgId(id = "8603") +public class J8603 extends Rs { + + + /** + * 待删除的区域ID + */ + private List idList; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + if (idList == null || idList.isEmpty()) { + buffer.writeByte(0); + return buffer; + }else { + buffer.writeByte(idList.size()); + } + for (Long id : idList) { + buffer.writeInt((int) (id & 0xffffffffL)); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8604.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8604.java new file mode 100644 index 0000000..2e54ded --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8604.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTPolygonArea; +import com.genersoft.iot.vmp.jt1078.bean.JTRectangleArea; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 设置多边形区域 + */ +@Setter +@Getter +@MsgId(id = "8604") +public class J8604 extends Rs { + + /** + * 多边形区域 + */ + private JTPolygonArea polygonArea; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeBytes(polygonArea.encode()); + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8605.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8605.java new file mode 100644 index 0000000..492cf6a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8605.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 删除多边形区域 + */ +@Setter +@Getter +@MsgId(id = "8605") +public class J8605 extends Rs { + + + /** + * 待删除的区域ID + */ + private List idList; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + if (idList == null || idList.isEmpty()) { + buffer.writeByte(0); + return buffer; + }else { + buffer.writeByte(idList.size()); + } + for (Long id : idList) { + buffer.writeInt((int) (id & 0xffffffffL)); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8606.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8606.java new file mode 100644 index 0000000..cfcded1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8606.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTPolygonArea; +import com.genersoft.iot.vmp.jt1078.bean.JTRoute; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 设置路线 + */ +@Setter +@Getter +@MsgId(id = "8606") +public class J8606 extends Rs { + + /** + * 路线 + */ + private JTRoute route; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeBytes(route.encode()); + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8607.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8607.java new file mode 100644 index 0000000..1b31235 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8607.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 删除路线 + */ +@Setter +@Getter +@MsgId(id = "8607") +public class J8607 extends Rs { + + + /** + * 待删除的路线ID + */ + private List idList; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + if (idList == null || idList.isEmpty()) { + buffer.writeByte(0); + return buffer; + }else { + buffer.writeByte(idList.size()); + } + for (Long id : idList) { + buffer.writeInt((int) (id & 0xffffffffL)); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8608.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8608.java new file mode 100644 index 0000000..5a33918 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8608.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 查询区域或线路数据 + */ +@Setter +@Getter +@MsgId(id = "8608") +public class J8608 extends Rs { + + + /** + * 查询类型, 1 = 查询圆形区域数据 ,2 = 查询矩形区域数据 ,3 = 查询多 边形区域数据 ,4 = 查询线路数据 + */ + private int type; + + + /** + * 要查询的区域或线路的 ID + */ + private List idList; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(type); + if (idList == null || idList.isEmpty()) { + buffer.writeInt(0); + return buffer; + }else { + buffer.writeInt(idList.size()); + } + for (Long id : idList) { + buffer.writeInt((int) (id & 0xffffffffL)); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8702.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8702.java new file mode 100644 index 0000000..ba457cd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8702.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +/** + * 上报驾驶员身份信息请求 + */ +@MsgId(id = "8702") +public class J8702 extends Rs { + + @Override + public ByteBuf encode() { + return Unpooled.buffer(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8801.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8801.java new file mode 100644 index 0000000..83d7e4f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8801.java @@ -0,0 +1,28 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTShootingCommand; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 摄像头立即拍摄命令 + */ +@Setter +@Getter +@MsgId(id = "8801") +public class J8801 extends Rs { + + JTShootingCommand command; + + @Override + public ByteBuf encode() { + return command.decode(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8802.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8802.java new file mode 100644 index 0000000..f7492c1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8802.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTQueryMediaDataCommand; +import com.genersoft.iot.vmp.jt1078.bean.JTShootingCommand; +import io.netty.buffer.ByteBuf; +import lombok.Getter; +import lombok.Setter; + +/** + * 存储多媒体数据检索 + */ +@Setter +@Getter +@MsgId(id = "8802") +public class J8802 extends Rs { + + JTQueryMediaDataCommand command; + + @Override + public ByteBuf encode() { + return command.decode(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8803.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8803.java new file mode 100644 index 0000000..59cf091 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8803.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTQueryMediaDataCommand; +import io.netty.buffer.ByteBuf; +import lombok.Getter; +import lombok.Setter; + +/** + * 存储多媒体数据上传命令 + */ +@Setter +@Getter +@MsgId(id = "8803") +public class J8803 extends Rs { + + JTQueryMediaDataCommand command; + + @Override + public ByteBuf encode() { + return command.decode(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8804.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8804.java new file mode 100644 index 0000000..2414d68 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8804.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTQueryMediaDataCommand; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 录音开始/停止命令 + */ +@Setter +@Getter +@MsgId(id = "8804") +public class J8804 extends Rs { + + /** + * 录音命令, 0:停止录音;0X01:开始录音 + */ + private int commond; + + /** + * 录音时长,单位为秒(s) ,0 表示一直录音 + */ + private int duration; + + /** + * 保存标志, 0:实时上传;1:保存 + */ + private int save; + + /** + * 音频采样率, 0:8K;1:11K;2:23K;3:32K;其他保留 + */ + private int samplingRate; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(commond); + byteBuf.writeShort((short)(duration & 0xffff)); + byteBuf.writeByte(save); + byteBuf.writeByte(samplingRate); + return byteBuf; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8805.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8805.java new file mode 100644 index 0000000..c64db88 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8805.java @@ -0,0 +1,37 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTQueryMediaDataCommand; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * 单条存储多媒体数据检索上传命令 + */ +@Setter +@Getter +@MsgId(id = "8805") +public class J8805 extends Rs { + + /** + * 多媒体 ID + */ + private Long mediaId; + + /** + * 删除标志, 0:保留;1:删除, 存储多媒体数据上传命令中使用 + */ + private Integer delete; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeInt((int) (mediaId & 0xffffffffL)); + byteBuf.writeByte(delete); + return byteBuf; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8900.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8900.java new file mode 100644 index 0000000..b8ef60f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8900.java @@ -0,0 +1,35 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 数据下行透传 + */ +@Setter +@Getter +@MsgId(id = "8900") +public class J8900 extends Rs { + + /** + * 透传消息类型, 0x00: GNSS 模块详细定位数据, 0X0B: 道路运输证 IC卡信息, 0X41: 串口1 透传, 0X42: 串口2 透传, 0XF0 ~ 0XFF: 用户自定义透传 + */ + private Integer type; + + /** + * 透传消息内容 + */ + private byte[] content; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(type); + byteBuf.writeBytes(content); + return byteBuf; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8A00.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8A00.java new file mode 100644 index 0000000..51f3df5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8A00.java @@ -0,0 +1,35 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 平台 RSA公钥 + */ +@Setter +@Getter +@MsgId(id = "8A00") +public class J8A00 extends Rs { + + /** + * 平台 RSA公钥{e ,n}中的 e + */ + private Long e; + + /** + * RSA公钥{e ,n}中的 n + */ + private byte[] n; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeInt((int) (e & 0xffffffffL)); + byteBuf.writeBytes(n); + return byteBuf; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9003.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9003.java new file mode 100644 index 0000000..9bbd9d0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9003.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +/** + * 查询终端音视频属性 + */ +@MsgId(id = "9003") +public class J9003 extends Rs { + + @Override + public ByteBuf encode() { + return Unpooled.buffer(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9101.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9101.java new file mode 100644 index 0000000..935021a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9101.java @@ -0,0 +1,70 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; + +/** + * 实时音视频传输请求 + * + * @author QingtaiJiang + * @date 2023/4/27 18:25 + * @email qingtaij@163.com + */ +@Setter +@Getter +@MsgId(id = "9101") +public class J9101 extends Rs { + String ip; + + // TCP端口 + Integer tcpPort; + + // UDP端口 + Integer udpPort; + + // 逻辑通道号 + Integer channel; + + // 数据类型 + /** + * 0:音视频,1:视频,2:双向对讲,3:监听,4:中心广播,5:透传 + */ + Integer type; + + // 码流类型 + /** + * 0:主码流,1:子码流 + */ + Integer rate; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(ip.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(ip, Charset.forName("GBK")); + buffer.writeShort(tcpPort); + buffer.writeShort(udpPort); + buffer.writeByte(channel); + buffer.writeByte(type); + buffer.writeByte(rate); + return buffer; + } + + @Override + public String toString() { + return "J9101{" + + "ip='" + ip + '\'' + + ", tcpPort=" + tcpPort + + ", udpPort=" + udpPort + + ", channel=" + channel + + ", type=" + type + + ", rate=" + rate + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9102.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9102.java new file mode 100644 index 0000000..61d7f0c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9102.java @@ -0,0 +1,71 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 音视频实时传输控制 + * + * @author QingtaiJiang + * @date 2023/4/27 18:49 + * @email qingtaij@163.com + */ +@Setter +@Getter +@MsgId(id = "9102") +public class J9102 extends Rs { + + // 通道号 + Integer channel; + + // 控制指令 + /** + * 0:关闭音视频传输指令; + * 1:切换码流(增加暂停和继续); + * 2:暂停该通道所有流的发送; + * 3:恢复暂停前流的发送,与暂停前的流类型一致; + * 4:关闭双向对讲 + */ + Integer command; + + // 数据类型 + /** + * 0:关闭该通道有关的音视频数据; + * 1:只关闭该通道有关的音频,保留该通道 + * 有关的视频; + * 2:只关闭该通道有关的视频,保留该通道 + * 有关的音频 + */ + Integer closeType; + + // 数据类型 + /** + * 0:主码流; + * 1:子码流 + */ + Integer streamType; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(command); + buffer.writeByte(closeType); + buffer.writeByte(streamType); + return buffer; + } + + + @Override + public String toString() { + return "J9102{" + + "channel=" + channel + + ", command=" + command + + ", closeType=" + closeType + + ", streamType=" + streamType + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9201.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9201.java new file mode 100644 index 0000000..3de9711 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9201.java @@ -0,0 +1,91 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; + +/** + * 回放请求 + * + * @author QingtaiJiang + * @date 2023/4/28 10:37 + * @email qingtaij@163.com + */ +@Setter +@Getter +@MsgId(id = "9201") +public class J9201 extends Rs { + // 服务器IP地址 + private String ip; + + // 实时视频服务器TCP端口号 + private int tcpPort; + + // 实时视频服务器UDP端口号 + private int udpPort; + + // 逻辑通道号 + private int channel; + + // 音视频资源类型:0.音视频 1.音频 2.视频 3.视频或音视频 + private int type; + + // 码流类型:0.所有码流 1.主码流 2.子码流(如果此通道只传输音频,此字段置0) + private int rate; + + // 存储器类型:0.所有存储器 1.主存储器 2.灾备存储器" + private int storageType; + + // 回放方式:0.正常回放 1.快进回放 2.关键帧快退回放 3.关键帧播放 4.单帧上传 + private int playbackType; + + // 快进或快退倍数:0.无效 1.1倍 2.2倍 3.4倍 4.8倍 5.16倍 (回放控制为1和2时,此字段内容有效,否则置0) + private int playbackSpeed; + + // 开始时间YYMMDDHHMMSS,回放方式为4时,该字段表示单帧上传时间 + private String startTime; + + // 结束时间YYMMDDHHMMSS,回放方式为4时,该字段无效,为0表示一直回放 + private String endTime; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(ip.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(ip, Charset.forName("GBK")); + buffer.writeShort(tcpPort); + buffer.writeShort(udpPort); + buffer.writeByte(channel); + buffer.writeByte(type); + buffer.writeByte(rate); + buffer.writeByte(storageType); + buffer.writeByte(playbackType); + buffer.writeByte(playbackSpeed); + buffer.writeBytes(ByteBufUtil.decodeHexDump(startTime)); + buffer.writeBytes(ByteBufUtil.decodeHexDump(endTime)); + return buffer; + } + + @Override + public String toString() { + return "J9201{" + + "ip='" + ip + '\'' + + ", tcpPort=" + tcpPort + + ", udpPort=" + udpPort + + ", channel=" + channel + + ", type=" + type + + ", rate=" + rate + + ", storageType=" + storageType + + ", playbackType=" + playbackType + + ", playbackSpeed=" + playbackSpeed + + ", startTime='" + startTime + '\'' + + ", endTime='" + endTime + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9202.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9202.java new file mode 100644 index 0000000..805d2dd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9202.java @@ -0,0 +1,55 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 平台下发远程录像回放控制 + * + * @author QingtaiJiang + * @date 2023/4/28 10:37 + * @email qingtaij@163.com + */ +@Setter +@Getter +@MsgId(id = "9202") +public class J9202 extends Rs { + // 逻辑通道号 + private int channel; + + // 回放控制:0.开始回放 1.暂停回放 2.结束回放 3.快进回放 4.关键帧快退回放 5.拖动回放 6.关键帧播放 + private int playbackType; + + // 快进或快退倍数:0.无效 1.1倍 2.2倍 3.4倍 4.8倍 5.16倍 (回放控制为3和4时,此字段内容有效,否则置0) + private int playbackSpeed; + + // 拖动回放位置(YYMMDDHHMMSS,回放控制为5时,此字段有效) + private String playbackTime; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(playbackType); + buffer.writeByte(playbackSpeed); + if (playbackType == 5) { + buffer.writeBytes(ByteBufUtil.decodeHexDump(playbackTime)); + } + + return buffer; + } + + @Override + public String toString() { + return "J9202{" + + "channel=" + channel + + ", playbackType=" + playbackType + + ", playbackSpeed=" + playbackSpeed + + ", playbackTime='" + playbackTime + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9205.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9205.java new file mode 100644 index 0000000..36b858e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9205.java @@ -0,0 +1,94 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; + +/** + * 查询资源列表 + * + * @author QingtaiJiang + * @date 2023/4/28 10:36 + * @email qingtaij@163.com + */ +@MsgId(id = "9205") +public class J9205 extends Rs { + // 逻辑通道号 + private int channelId; + + // 开始时间YYMMDDHHMMSS,全0表示无起始时间 + private String startTime; + + // 结束时间YYMMDDHHMMSS,全0表示无终止时间 + private String endTime; + + // 报警标志 + private final int warnType = 0; + + // 音视频资源类型:0.音视频 1.音频 2.视频 3.视频或音视频 + private int mediaType; + + // 码流类型:0.所有码流 1.主码流 2.子码流 + private int streamType = 0; + + // 存储器类型:0.所有存储器 1.主存储器 2.灾备存储器 + private int storageType = 0; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + + buffer.writeByte(channelId); + buffer.writeBytes(ByteBufUtil.decodeHexDump(startTime)); + buffer.writeBytes(ByteBufUtil.decodeHexDump(endTime)); + buffer.writeLong(warnType); + buffer.writeByte(mediaType); + buffer.writeByte(streamType); + buffer.writeByte(storageType); + + return buffer; + } + + + public void setChannelId(int channelId) { + this.channelId = channelId; + } + + public void setStartTime(String startTime) { + this.startTime = startTime; + } + + public void setEndTime(String endTime) { + this.endTime = endTime; + } + + public void setMediaType(int mediaType) { + this.mediaType = mediaType; + } + + public void setStreamType(int streamType) { + this.streamType = streamType; + } + + public void setStorageType(int storageType) { + this.storageType = storageType; + } + + public int getWarnType() { + return warnType; + } + + @Override + public String toString() { + return "J9205{" + + "channelId=" + channelId + + ", startTime='" + startTime + '\'' + + ", endTime='" + endTime + '\'' + + ", warnType=" + warnType + + ", mediaType=" + mediaType + + ", streamType=" + streamType + + ", storageType=" + storageType + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9206.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9206.java new file mode 100644 index 0000000..fefa084 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9206.java @@ -0,0 +1,105 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; + +/** + * 文件上传指令 + * + */ +@Setter +@Getter +@MsgId(id = "9206") +public class J9206 extends Rs { + + // 服务器地址 + private String serverIp; + // 服务器端口 + private int port; + // 用户名 + private String username; + // 密码 + private String password; + // 文件上传路径 + private String path; + // 逻辑通道号 + private int channelId; + + // 开始时间YYMMDDHHMMSS,全0表示无起始时间 + private String startTime; + + // 结束时间YYMMDDHHMMSS,全0表示无终止时间 + private String endTime; + + // 报警标志 + private int alarmSign = 0; + + // 音视频资源类型:0.音视频 1.音频 2.视频 3.视频或音视频 + private int mediaType; + + // 码流类型:0.所有码流 1.主码流 2.子码流 + private int streamType = 0; + + // 存储器类型:0.所有存储器 1.主存储器 2.灾备存储器 + private int storageType = 0; + + // 任务执行条件, + // 1:仅WI-FI 下可下载, + // 2: 仅LAN 连接时可下载; + // 3: WI-FI + LAN 连接时可下载; + // 4: 仅3G/ 4G 连接时可下载 + // 5: WI-FI + 3G/ 4G 连接时可下载 + // 6: WI-FI + LAN 连接时可下载 + // 7: WI-FI + LAN + 3G/ 4G 连接时可下载 + private int taskConditions = 7; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + + buffer.writeByte(serverIp.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(serverIp, Charset.forName("GBK")); + buffer.writeShort(port); + buffer.writeByte(username.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(username, Charset.forName("GBK")); + buffer.writeByte(password.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(password, Charset.forName("GBK")); + buffer.writeByte(path.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(path, Charset.forName("GBK")); + buffer.writeByte(channelId); + buffer.writeBytes(ByteBufUtil.decodeHexDump(startTime)); + buffer.writeBytes(ByteBufUtil.decodeHexDump(endTime)); + buffer.writeLong(alarmSign); + buffer.writeByte(mediaType); + buffer.writeByte(streamType); + buffer.writeByte(storageType); + buffer.writeByte(taskConditions); + return buffer; + } + + + @Override + public String toString() { + return "J9206{" + + "serverIp='" + serverIp + '\'' + + ", port=" + port + + ", user='" + username + '\'' + + ", password='" + password + '\'' + + ", path='" + path + '\'' + + ", channelId=" + channelId + + ", startTime='" + startTime + '\'' + + ", endTime='" + endTime + '\'' + + ", warnType=" + alarmSign + + ", mediaType=" + mediaType + + ", streamType=" + streamType + + ", storageType=" + storageType + + ", taskConditions=" + taskConditions + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9207.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9207.java new file mode 100644 index 0000000..d7a3c17 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9207.java @@ -0,0 +1,42 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import lombok.Getter; +import lombok.Setter; + +/** + * 文件上传控制 + * + */ +@Setter +@Getter +@MsgId(id = "9207") +public class J9207 extends Rs { + + // 对应平台文件上传消息的流水号 + Integer respNo; + + // 控制: 0:暂停; 1:继续; 2:取消 + private int control; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeShort(respNo); + buffer.writeByte(control); + return buffer; + } + + + @Override + public String toString() { + return "J9207{" + + "respNo=" + respNo + + ", control=" + control + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9301.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9301.java new file mode 100644 index 0000000..b7852b8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9301.java @@ -0,0 +1,44 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 云台控制指令-云台旋转 + * + */ +@Setter +@Getter +@MsgId(id = "9301") +public class J9301 extends Rs { + // 逻辑通道号 + private int channel; + + // 方向: 0:停止; 1:上; 2:下; 3:左; 4:右 + private int direction; + + // 速度:0 ~ 255 + private int speed; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(direction); + buffer.writeByte(speed); + return buffer; + } + + @Override + public String toString() { + return "J9301{" + + "channel=" + channel + + ", direction=" + direction + + ", speed=" + speed + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9302.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9302.java new file mode 100644 index 0000000..457bbf7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9302.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 云台控制指令-焦距控制 + * + */ +@Setter +@Getter +@MsgId(id = "9302") +public class J9302 extends Rs { + // 逻辑通道号 + private int channel; + + // 方向: 0:焦距调大; 1:焦距调小 + private int focalDirection; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(focalDirection); + return buffer; + } + + @Override + public String toString() { + return "J9302{" + + "channel=" + channel + + ", zoomDirection=" + focalDirection + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9303.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9303.java new file mode 100644 index 0000000..662ea8c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9303.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 云台控制指令-光圈控制 + * + */ +@Setter +@Getter +@MsgId(id = "9303") +public class J9303 extends Rs { + // 逻辑通道号 + private int channel; + + // 调整方式: 0:调大; 1:调小 + private int iris; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(iris); + return buffer; + } + + @Override + public String toString() { + return "J9303{" + + "channel=" + channel + + ", iris=" + iris + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9304.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9304.java new file mode 100644 index 0000000..557a9b2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9304.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 云台控制指令-云台雨刷控制 + * + */ +@Setter +@Getter +@MsgId(id = "9304") +public class J9304 extends Rs { + // 逻辑通道号 + private int channel; + + // 启停标识: 0:停止; 1:启动 + private int on; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(on); + return buffer; + } + + @Override + public String toString() { + return "J9304{" + + "channel=" + channel + + ", on=" + on + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9305.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9305.java new file mode 100644 index 0000000..3b34920 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9305.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 云台控制指令-红外补光控制 + * + */ +@Setter +@Getter +@MsgId(id = "9305") +public class J9305 extends Rs { + // 逻辑通道号 + private int channel; + + // 启停标识: 0:停止; 1:启动 + private int on; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(on); + return buffer; + } + + @Override + public String toString() { + return "J9305{" + + "channel=" + channel + + ", on=" + on + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9306.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9306.java new file mode 100644 index 0000000..ff60132 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9306.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 云台控制指令-云台变倍控制 + * + */ +@Setter +@Getter +@MsgId(id = "9306") +public class J9306 extends Rs { + // 逻辑通道号 + private int channel; + + // 0:调大; 1:调小 + private int zoom; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(zoom); + return buffer; + } + + @Override + public String toString() { + return "J9306{" + + "channel=" + channel + + ", zoom=" + zoom + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/Rs.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/Rs.java new file mode 100644 index 0000000..243cd94 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/Rs.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + + +import com.genersoft.iot.vmp.jt1078.proc.Header; +import io.netty.buffer.ByteBuf; + + +/** + * @author QingtaiJiang + * @date 2021/8/30 18:54 + * @email qingtaij@163.com + */ + +public abstract class Rs { + private Header header; + + public abstract ByteBuf encode(); + + + public Header getHeader() { + return header; + } + + public void setHeader(Header header) { + this.header = header; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/service/Ijt1078PlayService.java b/src/main/java/com/genersoft/iot/vmp/jt1078/service/Ijt1078PlayService.java new file mode 100644 index 0000000..c0f8fd3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/service/Ijt1078PlayService.java @@ -0,0 +1,43 @@ +package com.genersoft.iot.vmp.jt1078.service; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.proc.request.J1205; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; + +import java.util.List; + +public interface Ijt1078PlayService { + + JTMediaStreamType checkStreamFromJt(String stream); + + void play(String phoneNumber, Integer channelId, int type, CommonCallback> callback); + + void playback(String phoneNumber, Integer channelId, String startTime, String endTime, Integer type, + Integer rate, Integer playbackType, Integer playbackSpeed, CommonCallback> callback); + + void stopPlay(String phoneNumber, Integer channelId); + + void pausePlay(String phoneNumber, Integer channelId); + + void continueLivePlay(String phoneNumber, Integer channelId); + + List getRecordList(String phoneNumber, Integer channelId, String startTime, String endTime); + + void stopPlayback(String phoneNumber, Integer channelId); + + StreamInfo startTalk(String phoneNumber, Integer channelId); + + void stopTalk(String phoneNumber, Integer channelId); + + void playbackControl(String phoneNumber, Integer channelId, Integer command, Integer playbackSpeed, String time); + + void start(Integer channelId, Boolean record, ErrorCallback callback); + + void stop(Integer channelId); + + void playBack(Integer channelId, Long startTime, Long stopTime, ErrorCallback callback); +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/service/Ijt1078Service.java b/src/main/java/com/genersoft/iot/vmp/jt1078/service/Ijt1078Service.java new file mode 100644 index 0000000..b6bbba4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/service/Ijt1078Service.java @@ -0,0 +1,130 @@ +package com.genersoft.iot.vmp.jt1078.service; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageInfo; +import jakarta.servlet.ServletOutputStream; + +import java.io.OutputStream; +import java.util.List; + +public interface Ijt1078Service { + + JTMediaStreamType checkStreamFromJt(String stream); + + JTDevice getDevice(String phoneNumber); + + JTChannel getChannel(Integer terminalDbId, Integer channelId); + + void updateDevice(JTDevice deviceInDb); + + PageInfo getDeviceList(int page, int count, String query, Boolean online); + + void addDevice(JTDevice device); + + void deleteDeviceByPhoneNumber(String phoneNumber); + + void updateDeviceStatus(boolean connected, String phoneNumber); + + + void ptzControl(String phoneNumber, Integer channelId, String command, int speed); + + void supplementaryLight(String phoneNumber, Integer channelId, String command); + + void wiper(String phoneNumber, Integer channelId, String command); + + JTDeviceConfig queryConfig(String phoneNumber, String[] params); + + void setConfig(String phoneNumber, JTDeviceConfig config); + + void connectionControl(String phoneNumber, JTDeviceConnectionControl control); + + void resetControl(String phoneNumber); + + void factoryResetControl(String phoneNumber); + + JTDeviceAttribute attribute(String phoneNumber); + + JTPositionBaseInfo queryPositionInfo(String phoneNumber); + + void tempPositionTrackingControl(String phoneNumber, Integer timeInterval, Long validityPeriod); + + void confirmationAlarmMessage(String phoneNumber, int alarmPackageNo, JTConfirmationAlarmMessageType alarmMessageType); + + int linkDetection(String phoneNumber); + + int textMessage(String phoneNumber,JTTextSign sign, int textType, String content); + + int telephoneCallback(String phoneNumber, Integer sign, String destPhoneNumber); + + int setPhoneBook(String phoneNumber, int type, List phoneBookContactList); + + JTPositionBaseInfo controlDoor(String phoneNumber, Boolean open); + + int setAreaForCircle(int attribute, String phoneNumber, List circleAreaList); + + int deleteAreaForCircle(String phoneNumber, List ids); + + List queryAreaForCircle(String phoneNumber, List ids); + + int setAreaForRectangle(int i, String phoneNumber, List rectangleAreas); + + int deleteAreaForRectangle(String phoneNumber, List ids); + + List queryAreaForRectangle(String phoneNumber, List ids); + + int setAreaForPolygon(String phoneNumber, JTPolygonArea polygonArea); + + int deleteAreaForPolygon(String phoneNumber, List ids); + + List queryAreaForPolygon(String phoneNumber, List ids); + + int setRoute(String phoneNumber, JTRoute route); + + int deleteRoute(String phoneNumber, List ids); + + List queryRoute(String phoneNumber, List ids); + + JTDriverInformation queryDriverInformation(String phoneNumber); + + List shooting(String phoneNumber, JTShootingCommand shootingCommand); + + List queryMediaData(String phoneNumber, JTQueryMediaDataCommand queryMediaDataCommand); + + void uploadMediaData(String phoneNumber, JTQueryMediaDataCommand queryMediaDataCommand); + + void record(String phoneNumber, int command, Integer time, Integer save, Integer samplingRate); + + void uploadMediaDataForSingle(String phoneNumber, Long mediaId, Integer delete); + + JTMediaAttribute queryMediaAttribute(String phoneNumber); + + void changeStreamType(String phoneNumber, Integer channelId, Integer streamType); + + void recordDownload(String phoneNumber, Integer channelId, String startTime, String endTime, Integer alarmSign, Integer mediaType, Integer streamType, Integer storageType, OutputStream outputStream, CommonCallback> fileCallback); + + PageInfo getChannelList(int page, int count, int deviceId, String query); + + void updateChannel(JTChannel channel); + + void addChannel(JTChannel channel); + + void deleteChannelById(Integer id); + + JTDevice getDeviceById(Integer deviceId); + + void updateDevicePosition(String phoneNumber, Double longitude, Double latitude); + + JTChannel getChannelByDbId(Integer id); + + String getRecordTempUrl(String phoneNumber, Integer channelId, String startTime, String endTime, Integer alarmSign, Integer mediaType, Integer streamType, Integer storageType); + + void recordDownload(String filePath, ServletOutputStream outputStream); + + byte[] snap(String phoneNumber, int channelId); + + void uploadOneMedia(String phoneNumber, Long mediaId, ServletOutputStream outputStream, boolean delete); + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePTZServiceForJTImpl.java b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePTZServiceForJTImpl.java new file mode 100644 index 0000000..f88a73d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePTZServiceForJTImpl.java @@ -0,0 +1,159 @@ +package com.genersoft.iot.vmp.jt1078.service.impl; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IPTZService; +import com.genersoft.iot.vmp.gb28181.service.ISourcePTZService; +import com.genersoft.iot.vmp.jt1078.bean.JTChannel; +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import com.genersoft.iot.vmp.jt1078.cmd.JT1078Template; +import com.genersoft.iot.vmp.jt1078.proc.response.*; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.units.qual.A; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import java.util.List; + +@Slf4j +@Service(ChannelDataType.PTZ_SERVICE + ChannelDataType.JT_1078) +public class SourcePTZServiceForJTImpl implements ISourcePTZService { + + @Autowired + private Ijt1078Service service; + + @Autowired + private JT1078Template jt1078Template; + + @Override + public void ptz(CommonGBChannel channel, FrontEndControlCodeForPTZ frontEndControlCode, ErrorCallback callback) { + JTChannel jtChannel = service.getChannelByDbId(channel.getDataDeviceId()); + if (jtChannel == null) { + callback.run(ErrorCode.ERROR404.getCode(), "通道不存在", null); + return; + } + JTDevice jtDevice = service.getDeviceById(jtChannel.getTerminalDbId()); + if (jtDevice == null) { + callback.run(ErrorCode.ERROR404.getCode(), "设备不存在", null); + return; + } + + if (frontEndControlCode.getPan() == null && frontEndControlCode.getTilt() == null && frontEndControlCode.getZoom() == null) { + J9301 j9301 = new J9301(); + j9301.setChannel(jtChannel.getChannelId()); + j9301.setDirection(0); + j9301.setSpeed(0); + jt1078Template.ptzRotate(jtDevice.getPhoneNumber(), j9301, 6); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + return; + } + + if (frontEndControlCode.getPan() != null || frontEndControlCode.getTilt() != null) { + J9301 j9301 = new J9301(); + j9301.setChannel(jtChannel.getChannelId()); + if (frontEndControlCode.getTilt() != null) { + if (frontEndControlCode.getTilt() == 0) { + j9301.setDirection(1); + }else if (frontEndControlCode.getTilt() == 1) { + j9301.setDirection(2); + } + j9301.setSpeed((int)(frontEndControlCode.getTilt()/100D * 255)); + } + + if (frontEndControlCode.getPan() != null) { + if (frontEndControlCode.getPan() == 0) { + j9301.setDirection(3); + }else if (frontEndControlCode.getPan() == 1) { + j9301.setDirection(4); + } + j9301.setSpeed((int)(frontEndControlCode.getPanSpeed()/100D * 255)); + } + jt1078Template.ptzRotate(jtDevice.getPhoneNumber(), j9301, 6); + } + if (frontEndControlCode.getZoom() != null) { + J9306 j9306 = new J9306(); + j9306.setChannel(jtChannel.getChannelId()); + j9306.setZoom(1 - frontEndControlCode.getZoom()); + jt1078Template.ptzZoom(jtDevice.getPhoneNumber(), j9306, 6); + } + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + } + + @Override + public void preset(CommonGBChannel channel, FrontEndControlCodeForPreset frontEndControlCode, ErrorCallback callback) { + callback.run(ErrorCode.ERROR486.getCode(), ErrorCode.ERROR486.getMsg(), null); + } + + @Override + public void fi(CommonGBChannel channel, FrontEndControlCodeForFI frontEndControlCode, ErrorCallback callback) { + JTChannel jtChannel = service.getChannelByDbId(channel.getDataDeviceId()); + if (jtChannel == null) { + callback.run(ErrorCode.ERROR404.getCode(), "通道不存在", null); + return; + } + JTDevice jtDevice = service.getDeviceById(jtChannel.getTerminalDbId()); + if (jtDevice == null) { + callback.run(ErrorCode.ERROR404.getCode(), "设备不存在", null); + return; + } + if (frontEndControlCode.getIris() != null) { + J9303 j9303 = new J9303(); + j9303.setChannel(jtChannel.getChannelId()); + j9303.setIris(1 - frontEndControlCode.getIris()); + jt1078Template.ptzIris(jtDevice.getPhoneNumber(), j9303, 6); + } + if (frontEndControlCode.getFocus() != null) { + J9302 j9302 = new J9302(); + j9302.setChannel(jtChannel.getChannelId()); + j9302.setFocalDirection(1 - frontEndControlCode.getFocus()); + jt1078Template.ptzFocal(jtDevice.getPhoneNumber(), j9302, 6); + } + } + + @Override + public void tour(CommonGBChannel channel, FrontEndControlCodeForTour frontEndControlCode, ErrorCallback callback) { + callback.run(ErrorCode.ERROR486.getCode(), ErrorCode.ERROR486.getMsg(), null); + } + + @Override + public void scan(CommonGBChannel channel, FrontEndControlCodeForScan frontEndControlCode, ErrorCallback callback) { + callback.run(ErrorCode.ERROR486.getCode(), ErrorCode.ERROR486.getMsg(), null); + } + + @Override + public void auxiliary(CommonGBChannel channel, FrontEndControlCodeForAuxiliary frontEndControlCode, ErrorCallback callback) { + callback.run(ErrorCode.ERROR486.getCode(), ErrorCode.ERROR486.getMsg(), null); + } + + @Override + public void wiper(CommonGBChannel channel, FrontEndControlCodeForWiper frontEndControlCode, ErrorCallback callback) { + JTChannel jtChannel = service.getChannelByDbId(channel.getDataDeviceId()); + if (jtChannel == null) { + callback.run(ErrorCode.ERROR404.getCode(), "通道不存在", null); + return; + } + JTDevice jtDevice = service.getDeviceById(jtChannel.getTerminalDbId()); + if (jtDevice == null) { + callback.run(ErrorCode.ERROR404.getCode(), "设备不存在", null); + return; + } + J9304 j9304 = new J9304(); + j9304.setChannel(jtChannel.getChannelId()); + if (frontEndControlCode.getCode() == 1) { + j9304.setOn(1); + }else if (frontEndControlCode.getCode() == 0){ + j9304.setOn(0); + } + jt1078Template.ptzWiper(jtDevice.getPhoneNumber(), j9304, 6); + } + + @Override + public void queryPreset(CommonGBChannel channel, ErrorCallback> callback) { + callback.run(ErrorCode.ERROR486.getCode(), ErrorCode.ERROR486.getMsg(), null); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePlayServiceForJTImpl.java b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePlayServiceForJTImpl.java new file mode 100644 index 0000000..8432a88 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePlayServiceForJTImpl.java @@ -0,0 +1,47 @@ +package com.genersoft.iot.vmp.jt1078.service.impl; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.PlayException; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.service.ISourcePlayService; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078PlayService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sip.message.Response; + +@Slf4j +@Service(ChannelDataType.PLAY_SERVICE + ChannelDataType.JT_1078) +public class SourcePlayServiceForJTImpl implements ISourcePlayService { + + @Autowired + private Ijt1078PlayService playService; + + @Override + public void play(CommonGBChannel channel, Platform platform, Boolean record, ErrorCallback callback) { + // 部标设备通道 + try { + playService.start(channel.getDataDeviceId(), record, callback); + }catch (Exception e) { + log.info("[通用通道] 部标设备点播异常 {}", e.getMessage()); + callback.run(Response.BUSY_HERE, "busy here", null); + } + } + + @Override + public void stopPlay(CommonGBChannel channel) { + // 推流 + try { + playService.stop(channel.getDataDeviceId()); + }catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePlaybackServiceForJTImpl.java b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePlaybackServiceForJTImpl.java new file mode 100644 index 0000000..13881ce --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePlaybackServiceForJTImpl.java @@ -0,0 +1,131 @@ +package com.genersoft.iot.vmp.jt1078.service.impl; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.CommonRecordInfo; +import com.genersoft.iot.vmp.gb28181.service.ISourcePlaybackService; +import com.genersoft.iot.vmp.jt1078.bean.JTChannel; +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import com.genersoft.iot.vmp.jt1078.cmd.JT1078Template; +import com.genersoft.iot.vmp.jt1078.dao.JTChannelMapper; +import com.genersoft.iot.vmp.jt1078.dao.JTTerminalMapper; +import com.genersoft.iot.vmp.jt1078.proc.request.J1205; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078PlayService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.units.qual.A; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service(ChannelDataType.PLAYBACK_SERVICE + ChannelDataType.JT_1078) +public class SourcePlaybackServiceForJTImpl implements ISourcePlaybackService { + + @Autowired + private JTTerminalMapper jtDeviceMapper; + + @Autowired + private JTChannelMapper jtChannelMapper; + + @Autowired + private JT1078Template jt1078Template; + + @Autowired + private Ijt1078PlayService playService; + + + @Override + public void playback(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback callback) { + JTChannel jtChannel = jtChannelMapper.selectChannelById(channel.getDataDeviceId()); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jtDeviceMapper.getDeviceById(jtChannel.getDataDeviceId()); + Assert.notNull(device, "设备不存在"); + jt1078Template.checkTerminalStatus(device.getPhoneNumber()); + + playService.playback(device.getPhoneNumber(), jtChannel.getChannelId(), DateUtil.timestampTo_yyyy_MM_dd_HH_mm_ss(startTime), + DateUtil.timestampTo_yyyy_MM_dd_HH_mm_ss(stopTime), 0, 0, 0, 0, result -> { + callback.run(result.getCode(), result.getMsg(), result.getData()); + }); + } + + @Override + public void stopPlayback(CommonGBChannel channel, String stream) { + JTChannel jtChannel = jtChannelMapper.selectChannelById(channel.getDataDeviceId()); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jtDeviceMapper.getDeviceById(jtChannel.getDataDeviceId()); + Assert.notNull(device, "设备不存在"); + jt1078Template.checkTerminalStatus(device.getPhoneNumber()); + + playService.stopPlayback(device.getPhoneNumber(), jtChannel.getChannelId()); + } + + @Override + public void playbackPause(CommonGBChannel channel, String stream) { + JTChannel jtChannel = jtChannelMapper.selectChannelById(channel.getDataDeviceId()); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jtDeviceMapper.getDeviceById(jtChannel.getDataDeviceId()); + Assert.notNull(device, "设备不存在"); + jt1078Template.checkTerminalStatus(device.getPhoneNumber()); + + playService.playbackControl(device.getPhoneNumber(), jtChannel.getChannelId(), 1, 0, null); + } + + @Override + public void playbackResume(CommonGBChannel channel, String stream) { + JTChannel jtChannel = jtChannelMapper.selectChannelById(channel.getDataDeviceId()); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jtDeviceMapper.getDeviceById(jtChannel.getDataDeviceId()); + Assert.notNull(device, "设备不存在"); + jt1078Template.checkTerminalStatus(device.getPhoneNumber()); + + playService.playbackControl(device.getPhoneNumber(), jtChannel.getChannelId(), 0, 0, null); + } + + @Override + public void playbackSeek(CommonGBChannel channel, String stream, long seekTime) { + // 因为seek是增量,比如15s处, 1078是具体的时间点,比如2025-10-10 23:21:12 这个一个绝对时间点。无法转换,故此处不做支持 + log.warn("[JT-通用通道] 回放seek, 尚不支持"); + } + + @Override + public void playbackSpeed(CommonGBChannel channel, String stream, Double speed) { + JTChannel jtChannel = jtChannelMapper.selectChannelById(channel.getDataDeviceId()); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jtDeviceMapper.getDeviceById(jtChannel.getDataDeviceId()); + Assert.notNull(device, "设备不存在"); + jt1078Template.checkTerminalStatus(device.getPhoneNumber()); + + playService.playbackControl(device.getPhoneNumber(), jtChannel.getChannelId(), 0, (int)Math.floor(speed), null); + } + + @Override + public void queryRecord(CommonGBChannel channel, String startTime, String endTime, ErrorCallback> callback) { + JTChannel jtChannel = jtChannelMapper.selectChannelById(channel.getDataDeviceId()); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jtDeviceMapper.getDeviceById(jtChannel.getDataDeviceId()); + Assert.notNull(device, "设备不存在"); + jt1078Template.checkTerminalStatus(device.getPhoneNumber()); + List recordList = playService.getRecordList(device.getPhoneNumber(), jtChannel.getChannelId(), startTime, endTime); + if (recordList.isEmpty()) { + callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null); + return; + } + + List recordInfoList = new ArrayList<>(); + for (J1205.JRecordItem jRecordItem : recordList) { + CommonRecordInfo commonRecordInfo = new CommonRecordInfo(); + commonRecordInfo.setStartTime(jRecordItem.getStartTime()); + commonRecordInfo.setEndTime(jRecordItem.getEndTime()); + commonRecordInfo.setFileSize(jRecordItem.getSize() + ""); + recordInfoList.add(commonRecordInfo); + } + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), recordInfoList); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/jt1078PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/jt1078PlayServiceImpl.java new file mode 100644 index 0000000..f909678 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/jt1078PlayServiceImpl.java @@ -0,0 +1,712 @@ +package com.genersoft.iot.vmp.jt1078.service.impl; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.ftpServer.FtpSetting; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.cmd.JT1078Template; +import com.genersoft.iot.vmp.jt1078.event.FtpUploadEvent; +import com.genersoft.iot.vmp.jt1078.proc.request.J1205; +import com.genersoft.iot.vmp.jt1078.proc.response.*; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078PlayService; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookData; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.media.event.media.MediaNotFoundEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaSendRtpStoppedEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.MediaServerUtils; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import javax.sip.message.Response; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Service +@Slf4j +public class jt1078PlayServiceImpl implements Ijt1078PlayService { + + public static final String talkApp = "jt_talk"; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private Ijt1078Service jt1078Service; + + @Autowired + private JT1078Template jt1078Template; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private HookSubscribe subscribe; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private UserSetting userSetting; + + /** + * 流到来的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaArrivalEvent event) { + if (event.getApp().equals(talkApp) && event.getStream().endsWith("_talk")) { + // 收到对JT讲的流 + if (event.getStream().indexOf("_") <= 0) { + log.info("[JT-对讲流到来] 流格式有误,stream应该为jt_[phoneNumber]_[channelId]_talk"); + return; + } + String[] streamArray = event.getStream().split("_"); + if (streamArray.length != 4) { + log.info("[JT-对讲流到来] 流格式有误,stream应该为jt_[phoneNumber]_[channelId]_talk"); + return; + } + String phoneNumber = streamArray[1]; + String channelId = streamArray[2]; + JTDevice device = jt1078Service.getDevice(phoneNumber); + if (device == null) { + log.info("[JT-对讲流到来] 未找到设备{}", phoneNumber); + return; + } + sendTalk(device, Integer.valueOf(channelId), event.getMediaServer(), event.getApp(), event.getStream()); + + } + } + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + + } + + /** + * 流未找到的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaNotFoundEvent event) { + if (!userSetting.getAutoApplyPlay()) { + return; + } + JTMediaStreamType jtMediaStreamType = checkStreamFromJt(event.getStream()); + if (jtMediaStreamType == null){ + return; + } + String[] streamParamArray = event.getStream().split("_"); + String phoneNumber = streamParamArray[1]; + int channelId = Integer.parseInt(streamParamArray[2]); + String params = event.getParams(); + Map paramMap = MediaServerUtils.urlParamToMap(params); + int type = 0; + try { + type = Integer.parseInt(paramMap.get("type")); + }catch (NumberFormatException ignored) {} + if (jtMediaStreamType.equals(JTMediaStreamType.PLAY)) { + play(phoneNumber, channelId, 0, null); + }else if (jtMediaStreamType.equals(JTMediaStreamType.PLAYBACK)) { + String startTimeParam = DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(streamParamArray[3]); + String endTimeParam = DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(streamParamArray[4]); + int rate = 0; + int playbackType = 0; + int playbackSpeed = 0; + try { + rate = Integer.parseInt(paramMap.get("rate")); + playbackType = Integer.parseInt(paramMap.get("playbackType")); + playbackSpeed = Integer.parseInt(paramMap.get("playbackSpeed")); + }catch (NumberFormatException ignored) {} + playback(phoneNumber, channelId, startTimeParam, endTimeParam, type, rate, playbackType, playbackSpeed, null); + } + } + + + /** + * 校验流是否是属于部标的 + */ + @Override + public JTMediaStreamType checkStreamFromJt(String stream) { + if (!stream.startsWith("jt_")) { + return null; + } + String[] streamParamArray = stream.split("_"); + if (streamParamArray.length == 3) { + return JTMediaStreamType.PLAY; + }else if (streamParamArray.length == 5) { + return JTMediaStreamType.PLAYBACK; + }else if (streamParamArray.length == 4) { + return JTMediaStreamType.TALK; + }else { + return null; + } + } + + private final Map>>> inviteErrorCallbackMap = new ConcurrentHashMap<>(); + + @Override + public void play(String phoneNumber, Integer channelId, int type, CommonCallback> callback) { + JTDevice device = jt1078Service.getDevice(phoneNumber); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备不存在"); + } + jt1078Template.checkTerminalStatus(phoneNumber); + JTChannel channel = jt1078Service.getChannel(device.getId(), channelId); + if (channel == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道不存在"); + } + play(device, channel, type, callback); + } + + private void play(JTDevice device, JTChannel channel, int type, CommonCallback> callback) { + String phoneNumber = device.getPhoneNumber(); + int channelId = channel.getChannelId(); + String app = "1078"; + String stream = phoneNumber + "_" + channelId; + // 检查流是否已经存在,存在则返回 + String playKey = VideoManagerConstants.INVITE_INFO_1078_PLAY + phoneNumber + ":" + channelId; + List>> errorCallbacks = inviteErrorCallbackMap.computeIfAbsent(playKey, k -> new ArrayList<>()); + errorCallbacks.add(callback); + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + if (streamInfo != null) { + MediaServer mediaServer = streamInfo.getMediaServer(); + if (mediaServer != null) { + // 查询流是否存在,不存在则删除缓存数据 + MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, app, streamInfo.getStream()); + if (mediaInfo != null) { + log.info("[JT-点播] 点播已经存在,直接返回, phoneNumber: {}, channelId: {}", phoneNumber, channelId); + for (CommonCallback> errorCallback : errorCallbacks) { + errorCallback.run(new WVPResult<>(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo)); + } + return; + } + } + // 清理数据 + redisTemplate.delete(playKey); + } + + MediaServer mediaServer; + if (org.springframework.util.ObjectUtils.isEmpty(device.getMediaServerId()) || "auto".equals(device.getMediaServerId())) { + mediaServer = mediaServerService.getMediaServerForMinimumLoad(null); + } else { + mediaServer = mediaServerService.getOne(device.getMediaServerId()); + } + if (mediaServer == null) { + for (CommonCallback> errorCallback : errorCallbacks) { + errorCallback.run(new WVPResult<>(InviteErrorCode.FAIL.getCode(), "未找到可用的媒体节点", streamInfo)); + } + return; + } + // 设置hook监听 + Hook hook = Hook.getInstance(HookType.on_media_arrival, app, stream, mediaServer.getId()); + subscribe.addSubscribe(hook, (hookData) -> { + dynamicTask.stop(playKey); + log.info("[JT-点播] 点播成功, 手机号: {}, 通道: {}", phoneNumber, channelId); + // TODO 发送9105 实时音视频传输状态通知, 通知丢包率 + StreamInfo info = onPublishHandler(mediaServer, hookData, phoneNumber, channelId); + + for (CommonCallback> errorCallback : errorCallbacks) { + if (errorCallback == null) { + continue; + } + errorCallback.run(new WVPResult<>(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), info)); + } + subscribe.removeSubscribe(hook); + redisTemplate.opsForValue().set(playKey, info); + // 截图 + String path = "snap"; + String fileName = phoneNumber + "_" + channelId + ".jpg"; + // 请求截图 + log.info("[请求截图]: " + fileName); + mediaServerService.getSnap(mediaServer, app, stream, 15, 1, path, fileName); + }); + // 开启收流端口 + SSRCInfo ssrcInfo = mediaServerService.openJTTServer(mediaServer, stream, null, false, !channel.isHasAudio(), 1); + if (ssrcInfo == null) { + stopPlay(phoneNumber, channelId); + return; + } + + // 设置超时监听 + dynamicTask.startDelay(playKey, () -> { + log.info("[JT-点播] 超时, phoneNumber: {}, channelId: {}", phoneNumber, channelId); + for (CommonCallback> errorCallback : errorCallbacks) { + errorCallback.run(new WVPResult<>(InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), + InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null)); + } + mediaServerService.closeJTTServer(mediaServer, stream, null); + subscribe.removeSubscribe(hook); + stopPlay(phoneNumber, channelId); + }, userSetting.getPlayTimeout()); + + log.info("[JT-点播] phoneNumber: {}, channelId: {},IP: {}, 端口: {}", phoneNumber, channelId, mediaServer.getSdpIp(), ssrcInfo.getPort()); + J9101 j9101 = new J9101(); + j9101.setChannel(channelId); + j9101.setIp(mediaServer.getSdpIp()); + j9101.setRate(1); + j9101.setTcpPort(ssrcInfo.getPort()); + j9101.setUdpPort(ssrcInfo.getPort()); + j9101.setType(type); + jt1078Template.startLive(phoneNumber, j9101, 6); + } + + public StreamInfo onPublishHandler(MediaServer mediaServerItem, HookData hookData, String phoneNumber, Integer channelId) { + StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServerItem, "1078", hookData.getStream(), hookData.getMediaInfo(), null); + streamInfo.setDeviceId(phoneNumber); + streamInfo.setChannelId(channelId); + return streamInfo; + } + + @Override + public void stopPlay(String phoneNumber, Integer channelId) { + String playKey = VideoManagerConstants.INVITE_INFO_1078_PLAY + phoneNumber + ":" + channelId; + dynamicTask.stop(playKey); + // 清理回调 + List>> generalCallbacks = inviteErrorCallbackMap.get(playKey); + if (generalCallbacks != null && !generalCallbacks.isEmpty()) { + for (CommonCallback> callback : generalCallbacks) { + callback.run(new WVPResult<>(InviteErrorCode.ERROR_FOR_FINISH.getCode(), InviteErrorCode.ERROR_FOR_FINISH.getMsg(), null)); + } + } + jt1078Template.checkTerminalStatus(phoneNumber); + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + // 发送停止命令 + J9102 j9102 = new J9102(); + j9102.setChannel(channelId); + j9102.setCommand(0); + j9102.setCloseType(0); + j9102.setStreamType(1); + jt1078Template.stopLive(phoneNumber, j9102, 6); + log.info("[JT-停止点播] phoneNumber: {}, channelId: {}", phoneNumber, channelId); + // 删除缓存数据 + if (streamInfo != null) { + // 关闭rtpServer + mediaServerService.closeJTTServer(streamInfo.getMediaServer(), streamInfo.getStream(), null); + redisTemplate.delete(playKey); + } + + } + + @Override + public void pausePlay(String phoneNumber, Integer channelId) { + String playKey = VideoManagerConstants.INVITE_INFO_1078_PLAY + phoneNumber + ":" + channelId; + dynamicTask.stop(playKey); + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + if (streamInfo == null) { + log.info("[JT-暂停点播] 未找到点播信息 phoneNumber: {}, channelId: {}", phoneNumber, channelId); + } + log.info("[JT-暂停点播] phoneNumber: {}, channelId: {}", phoneNumber, channelId); + // 发送暂停命令 + J9102 j9102 = new J9102(); + j9102.setChannel(channelId); + j9102.setCommand(2); + j9102.setCloseType(0); + j9102.setStreamType(1); + jt1078Template.stopLive(phoneNumber, j9102, 6); + } + + @Override + public void continueLivePlay(String phoneNumber, Integer channelId) { + String playKey = VideoManagerConstants.INVITE_INFO_1078_PLAY + phoneNumber + ":" + channelId; + dynamicTask.stop(playKey); + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + if (streamInfo == null) { + log.info("[JT-继续点播] 未找到点播信息 phoneNumber: {}, channelId: {}", phoneNumber, channelId); + } + log.info("[JT-继续点播] phoneNumber: {}, channelId: {}", phoneNumber, channelId); + // 发送暂停命令 + J9102 j9102 = new J9102(); + j9102.setChannel(channelId); + j9102.setCommand(2); + j9102.setCloseType(0); + j9102.setStreamType(1); + jt1078Template.stopLive(phoneNumber, j9102, 6); + } + + @Override + public List getRecordList(String phoneNumber, Integer channelId, String startTime, String endTime) { + log.info("[JT-查询录像列表] phoneNumber: {}, channelId: {}, startTime: {}, endTime: {}" + , phoneNumber, channelId, startTime, endTime); + // 发送请求录像列表命令 + J9205 j9205 = new J9205(); + j9205.setChannelId(channelId); + j9205.setStartTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime)); + j9205.setEndTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime)); + j9205.setMediaType(0); + j9205.setStreamType(0); + j9205.setStorageType(0); + List JRecordItemList = (List) jt1078Template.queryBackTime(phoneNumber, j9205, 20); + if (JRecordItemList == null || JRecordItemList.isEmpty()) { + return null; + } + log.info("[JT-查询录像列表] phoneNumber: {}, channelId: {}, startTime: {}, endTime: {}, 结果: {}条" + , phoneNumber, channelId, startTime, endTime, JRecordItemList.size()); + return JRecordItemList; + } + + + + @Override + public void playback(String phoneNumber, Integer channelId, String startTime, String endTime, Integer type, + Integer rate, Integer playbackType, Integer playbackSpeed, CommonCallback> callback) { + JTDevice device = jt1078Service.getDevice(phoneNumber); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备不存在"); + } + jt1078Template.checkTerminalStatus(phoneNumber); + JTChannel channel = jt1078Service.getChannel(device.getId(), channelId); + if (channel == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道不存在"); + } + playback(device, channel, startTime, endTime, type, rate, playbackType, playbackSpeed, callback); + + } + + /** + * 回放 + * @param device 设备 + * @param channel 通道 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param type 音视频资源类型:0.音视频 1.音频 2.视频 3.视频或音视频 + * @param rate 码流类型:0.所有码流 1.主码流 2.子码流(如果此通道只传输音频,此字段置0) + * @param playbackType 回放方式:0.正常回放 1.快进回放 2.关键帧快退回放 3.关键帧播放 4.单帧上传 + * @param playbackSpeed 快进或快退倍数:0.无效 1.1倍 2.2倍 3.4倍 4.8倍 5.16倍 (回放控制为1和2时,此字段内容有效,否则置0) + * @param callback 结束回调 + */ + private void playback(JTDevice device, JTChannel channel, String startTime, String endTime, Integer type, + Integer rate, Integer playbackType, Integer playbackSpeed, CommonCallback> callback) { + + String phoneNumber = device.getPhoneNumber(); + Integer channelId = channel.getChannelId(); + log.info("[JT-回放] 回放,设备:{}, 通道: {}, 开始时间: {}, 结束时间: {}, 音视频类型: {}, 码流类型: {}, " + + "回放方式: {}, 快进或快退倍数: {}", phoneNumber, channelId, startTime, endTime, type, rate, playbackType, playbackSpeed); + // 检查流是否已经存在,存在则返回 + String playbackKey = VideoManagerConstants.INVITE_INFO_1078_PLAYBACK + phoneNumber + ":" + channelId; + List>> errorCallbacks = inviteErrorCallbackMap.computeIfAbsent(playbackKey, k -> new ArrayList<>()); + errorCallbacks.add(callback); + String logInfo = String.format("phoneNumber:%s, channelId:%s, startTime:%s, endTime:%s", phoneNumber, channelId, startTime, endTime); + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playbackKey); + if (streamInfo != null) { + + mediaServerService.closeJTTServer(streamInfo.getMediaServer(), streamInfo.getStream(), null); + // 清理数据 + redisTemplate.delete(playbackKey); + } + + String app = "1078"; + String stream = String.format("%s_%s_%s_%s", phoneNumber, channelId, + DateUtil.yyyy_MM_dd_HH_mm_ssToUrl(startTime), DateUtil.yyyy_MM_dd_HH_mm_ssToUrl(endTime)); + MediaServer mediaServer; + if (org.springframework.util.ObjectUtils.isEmpty(device.getMediaServerId()) || "auto".equals(device.getMediaServerId())) { + mediaServer = mediaServerService.getMediaServerForMinimumLoad(null); + } else { + mediaServer = mediaServerService.getOne(device.getMediaServerId()); + } + if (mediaServer == null) { + for (CommonCallback> errorCallback : errorCallbacks) { + errorCallback.run(new WVPResult<>(InviteErrorCode.FAIL.getCode(), "未找到可用的媒体节点", streamInfo)); + } + return; + } + // 设置hook监听 + Hook hookSubscribe = Hook.getInstance(HookType.on_media_arrival, app, stream, mediaServer.getId()); + subscribe.addSubscribe(hookSubscribe, (hookData) -> { + dynamicTask.stop(playbackKey); + log.info("[JT-回放] 回放成功, logInfo: {}", logInfo); + StreamInfo info = onPublishHandler(mediaServer, hookData, phoneNumber, channelId); + + for (CommonCallback> errorCallback : errorCallbacks) { + if (errorCallback == null) { + continue; + } + errorCallback.run(new WVPResult<>(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), info)); + } + subscribe.removeSubscribe(hookSubscribe); + redisTemplate.opsForValue().set(playbackKey, info); + }); + // 设置超时监听 + dynamicTask.startDelay(playbackKey, () -> { + log.info("[JT-回放] 回放超时, logInfo: {}", logInfo); + for (CommonCallback> errorCallback : errorCallbacks) { + errorCallback.run(new WVPResult<>(InviteErrorCode.ERROR_FOR_SIGNALLING_TIMEOUT.getCode(), + InviteErrorCode.ERROR_FOR_SIGNALLING_TIMEOUT.getMsg(), null)); + } + mediaServerService.closeJTTServer(mediaServer, stream, null); + subscribe.removeSubscribe(hookSubscribe); + }, userSetting.getPlayTimeout()); + + // 开启收流端口 + SSRCInfo ssrcInfo = mediaServerService.openJTTServer(mediaServer, stream, null, false, !channel.isHasAudio(), 1); + log.info("[JT-回放] logInfo: {}, 端口: {}", logInfo, ssrcInfo.getPort()); + J9201 j9201 = new J9201(); + j9201.setChannel(channelId); + j9201.setIp(mediaServer.getSdpIp()); + if (rate != null) { + j9201.setRate(rate); + } + if (playbackType != null) { + j9201.setPlaybackType(playbackType); + } + if (playbackSpeed != null) { + j9201.setPlaybackSpeed(playbackSpeed); + } + + j9201.setTcpPort(ssrcInfo.getPort()); + j9201.setUdpPort(ssrcInfo.getPort()); + j9201.setType(type); + j9201.setStartTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime)); + j9201.setEndTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime)); + jt1078Template.startBackLive(phoneNumber, j9201, 20); + + } + + @Override + public void playbackControl(String phoneNumber, Integer channelId, Integer command, Integer playbackSpeed, String time) { + String playKey = VideoManagerConstants.INVITE_INFO_1078_PLAYBACK + phoneNumber + ":" + channelId; + dynamicTask.stop(playKey); + if (command == 2) { + log.info("[JT-停止回放] phoneNumber: {}, channelId: {}, command: {}, playbackSpeed: {}, time: {}", + phoneNumber, channelId, command, playbackSpeed, time); + // 结束回放 + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + // 删除缓存数据 + if (streamInfo != null) { + // 关闭rtpServer + mediaServerService.closeJTTServer(streamInfo.getMediaServer(), streamInfo.getStream(), null); + } + // 清理回调 + List>> generalCallbacks = inviteErrorCallbackMap.get(playKey); + if (generalCallbacks != null && !generalCallbacks.isEmpty()) { + for (CommonCallback> callback : generalCallbacks) { + if (callback == null) { + continue; + } + callback.run(new WVPResult<>(InviteErrorCode.ERROR_FOR_FINISH.getCode(), InviteErrorCode.ERROR_FOR_FINISH.getMsg(), null)); + } + } + }else { + log.info("[JT-回放控制] phoneNumber: {}, channelId: {}, command: {}, playbackSpeed: {}, time: {}", + phoneNumber, channelId, command, playbackSpeed, time); + } + // 发送停止命令 + J9202 j9202 = new J9202(); + j9202.setChannel(channelId); + j9202.setPlaybackType(command); + + if (playbackSpeed != null) { + j9202.setPlaybackSpeed(playbackSpeed); + + } + if (!ObjectUtils.isEmpty(time)) { + j9202.setPlaybackTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(time)); + } + jt1078Template.controlBackLive(phoneNumber, j9202, 4); + } + + @Override + public void stopPlayback(String phoneNumber, Integer channelId) { + playbackControl(phoneNumber, channelId, 2, null, null); + } + + /** + * 监听发流停止 + */ + @EventListener + public void onApplicationEvent(MediaSendRtpStoppedEvent event) { + + List sendRtpInfos = sendRtpServerService.queryByStream(event.getStream()); + if (sendRtpInfos.isEmpty()) { + return; + } + for (SendRtpInfo sendRtpInfo : sendRtpInfos) { + if (!sendRtpInfo.isOnlyAudio() || ObjectUtils.isEmpty(sendRtpInfo.getChannelId())) { + continue; + } + if (!sendRtpInfo.getSsrc().contains("_")) { + continue; + } + sendRtpServerService.delete(sendRtpInfo); + String playKey = VideoManagerConstants.INVITE_INFO_1078_TALK + sendRtpInfo.getApp() + ":" + sendRtpInfo.getStream(); + redisTemplate.delete(playKey); + } + } + + + @Override + public StreamInfo startTalk(String phoneNumber, Integer channelId) { + // 检查流是否已经存在,存在则返回 + String playKey = VideoManagerConstants.INVITE_INFO_1078_TALK + phoneNumber + ":" + channelId; + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + + if (streamInfo != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "对讲进行中"); + } + + JTDevice device = jt1078Service.getDevice(phoneNumber); + Assert.notNull(device, "部标设备不存在"); + + String stream = "jt_" + phoneNumber + "_" + channelId + "_talk"; + + MediaServer mediaServer; + if (org.springframework.util.ObjectUtils.isEmpty(device.getMediaServerId()) || "auto".equals(device.getMediaServerId())) { + mediaServer = mediaServerService.getMediaServerForMinimumLoad(null); + } else { + mediaServer = mediaServerService.getOne(device.getMediaServerId()); + } + + // 检查待发送的流是否存在, + MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, talkApp, stream); + Assert.isNull(mediaInfo, "对讲已经存在"); + return mediaServerService.getStreamInfoByAppAndStream(mediaServer, talkApp, stream, null, null, null, false); + + } + private void sendTalk(JTDevice device, Integer channelId, MediaServer mediaServer, String app, String stream) { + // 检查待发送的流是否存在, + MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, app, stream); + if (mediaInfo == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), app + "/" + stream + "流不存在"); + } + + String phoneNumber = device.getPhoneNumber(); + + // 开启收流端口, zlm发送1078的rtp流需要将ssrc字段设置为 imei_channel格式 + String ssrc = device.getPhoneNumber() + "_" + channelId; + SendRtpInfo sendRtpInfo = sendRtpServerService.createSendRtpInfo(mediaServer, null, null, ssrc, phoneNumber, talkApp, stream, channelId, true, false); + sendRtpInfo.setTcpActive(true); + sendRtpInfo.setUsePs(false); + sendRtpInfo.setOnlyAudio(true); + sendRtpInfo.setReceiveStream(stream + "_talk"); + + // 设置hook监听 + Hook hook = Hook.getInstance(HookType.on_media_arrival, "1078", sendRtpInfo.getReceiveStream(), mediaServer.getId()); + subscribe.addSubscribe(hook, (hookData) -> { + log.info("[JT-对讲] 对讲连接建立, phoneNumber: {}, channelId: {}", phoneNumber, channelId); + subscribe.removeSubscribe(hook); + // 存储发流信息 + sendRtpServerService.update(sendRtpInfo); + }); + Hook hookForDeparture = Hook.getInstance(HookType.on_media_departure, app, stream, mediaServer.getId()); + subscribe.addSubscribe(hookForDeparture, (hookData) -> { + log.info("[JT-对讲] 对讲时源流注销, app: {}. stream: {}, phoneNumber: {}, channelId: {}", app, stream, phoneNumber, channelId); + stopTalk(phoneNumber, channelId); + }); + + Integer localPort = mediaServerService.startSendRtpPassive(mediaServer, sendRtpInfo, userSetting.getPlayTimeout()); + + log.info("[JT-对讲] phoneNumber: {}, channelId: {}, 收发端口: {}, app: {}, stream: {}", + phoneNumber, channelId, localPort, app, stream); + J9101 j9101 = new J9101(); + j9101.setChannel(channelId); + j9101.setIp(mediaServer.getSdpIp()); + j9101.setRate(1); + j9101.setTcpPort(sendRtpInfo.getLocalPort()); + j9101.setUdpPort(sendRtpInfo.getLocalPort()); + j9101.setType(4); + jt1078Template.startLive(phoneNumber, j9101, 6); + + log.info("[JT-对讲] 对讲消息下发成功, phoneNumber: {}, channelId: {}", phoneNumber, channelId); + // 存储发流信息 +// sendRtpServerService.update(sendRtpInfo); + } + + @Override + public void stopTalk(String phoneNumber, Integer channelId) { + String playKey = VideoManagerConstants.INVITE_INFO_1078_TALK + phoneNumber + ":" + channelId; + dynamicTask.stop(playKey); + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + // 发送停止命令 + J9102 j9102 = new J9102(); + j9102.setChannel(channelId); + j9102.setCommand(4); + j9102.setCloseType(0); + j9102.setStreamType(1); + jt1078Template.stopLive(phoneNumber, j9102, 6); + log.info("[JT-停止对讲] phoneNumber: {}, channelId: {}", phoneNumber, channelId); + // 删除缓存数据 + if (streamInfo != null) { + redisTemplate.delete(playKey); + // 关闭rtpServer + mediaServerService.closeJTTServer(streamInfo.getMediaServer(), streamInfo.getStream(), null); + } + // 清理回调 + List>> generalCallbacks = inviteErrorCallbackMap.get(playKey); + if (generalCallbacks != null && !generalCallbacks.isEmpty()) { + for (CommonCallback> callback : generalCallbacks) { + callback.run(new WVPResult<>(InviteErrorCode.ERROR_FOR_FINISH.getCode(), InviteErrorCode.ERROR_FOR_FINISH.getMsg(), null)); + } + } + } + + @Override + public void start(Integer channelId, Boolean record, ErrorCallback callback) { + JTChannel channel = jt1078Service.getChannelByDbId(channelId); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jt1078Service.getDeviceById(channel.getTerminalDbId()); + Assert.notNull(device, "设备不存在"); + jt1078Template.checkTerminalStatus(device.getPhoneNumber()); + play(device, channel, 0, + result -> callback.run(result.getCode(), result.getMsg(), result.getData())); + } + + @Override + public void stop(Integer channelId) { + JTChannel channel = jt1078Service.getChannelByDbId(channelId); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jt1078Service.getDeviceById(channel.getTerminalDbId()); + Assert.notNull(device, "设备不存在"); + stopPlay(device.getPhoneNumber(), channel.getChannelId()); + } + + @Override + public void playBack(Integer channelId, Long startTime, Long stopTime, ErrorCallback callback) { + if (startTime == null || stopTime == null) { + throw new PlayException(Response.BAD_REQUEST, "bad request"); + } + JTChannel channel = jt1078Service.getChannelByDbId(channelId); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jt1078Service.getDeviceById(channel.getTerminalDbId()); + Assert.notNull(device, "设备不存在"); + jt1078Template.checkTerminalStatus(device.getPhoneNumber()); + String startTimeStr = DateUtil.timestampTo_yyyy_MM_dd_HH_mm_ss(startTime); + String stopTimeStr = DateUtil.timestampTo_yyyy_MM_dd_HH_mm_ss(stopTime); + playback(device, channel, startTimeStr, stopTimeStr, 0, 1, 0, 0, + result -> callback.run(result.getCode(), result.getMsg(), result.getData())); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/jt1078ServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/jt1078ServiceImpl.java new file mode 100644 index 0000000..6247af8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/jt1078ServiceImpl.java @@ -0,0 +1,922 @@ +package com.genersoft.iot.vmp.jt1078.service.impl; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.ftpServer.FtpFileSystemFactory; +import com.genersoft.iot.vmp.conf.ftpServer.FtpSetting; +import com.genersoft.iot.vmp.conf.ftpServer.UserManager; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.bean.common.ConfigAttribute; +import com.genersoft.iot.vmp.jt1078.cmd.JT1078Template; +import com.genersoft.iot.vmp.jt1078.dao.JTChannelMapper; +import com.genersoft.iot.vmp.jt1078.dao.JTTerminalMapper; +import com.genersoft.iot.vmp.jt1078.event.DeviceUpdateEvent; +import com.genersoft.iot.vmp.jt1078.event.JTPositionEvent; +import com.genersoft.iot.vmp.jt1078.proc.response.*; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.FtpDownloadManager; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.ServletOutputStream; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.ftpserver.usermanager.impl.BaseUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Service +@Slf4j +public class jt1078ServiceImpl implements Ijt1078Service { + + @Autowired + private JTTerminalMapper jtDeviceMapper; + + @Autowired + private JTChannelMapper jtChannelMapper; + + @Autowired + private JT1078Template jt1078Template; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private UserSetting userSetting; + + @Autowired + private FtpSetting ftpSetting; + + @Autowired + private UserManager ftpUserManager; + + @Autowired + private FtpFileSystemFactory fileSystemFactory; + + @Autowired + private FtpDownloadManager downloadManager; + + // 服务启动后五分钟内没有尽量连接的设备设置为离线 + @PostConstruct + public void init(){ + // 检查session与在线终端是是否对应 不对应则设置终端离线 + List deviceList = jtDeviceMapper.getDeviceList(null, true); + if (deviceList.isEmpty()) { + return; + } + for (JTDevice device : deviceList) { + Session session = SessionManager.INSTANCE.get(device.getPhoneNumber()); + if (session == null) { + device.setStatus(false); + // 通道发送状态变化通知 + List jtChannels = jtChannelMapper.selectAll(device.getId(), null); + List channelList = new ArrayList<>(); + for (JTChannel jtChannel : jtChannels) { + if (jtChannel.getGbId() > 0) { + jtChannel.setGbStatus("OFF"); + channelList.add(jtChannel); + } + } + channelService.updateStatus(channelList); + updateDevice(device); + } + } + } + + /** + * 流到来的处理 + */ + @Async("taskExecutor") + @org.springframework.context.event.EventListener + public void onApplicationEvent(MediaArrivalEvent event) { + + } + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + + } + + /** + * 设备更新的通知 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(DeviceUpdateEvent event) { + JTDevice device = event.getDevice(); + if (device == null || device.getPhoneNumber() == null) { + return; + } + JTDevice deviceInDb = getDevice(event.getDevice().getPhoneNumber()); + if (deviceInDb.isStatus() != device.isStatus()) { + // 通道发送状态变化通知 + List jtChannels = jtChannelMapper.selectAll(deviceInDb.getId(), null); + List channelList = new ArrayList<>(); + for (JTChannel jtChannel : jtChannels) { + if (jtChannel.getGbId() > 0) { + jtChannel.setGbStatus("OFF"); + channelList.add(jtChannel); + } + } + channelService.updateStatus(channelList); + } + updateDevice(event.getDevice()); + } + + /** + * 位置更新的通知 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(JTPositionEvent event) { + if (event.getPhoneNumber() == null || event.getPositionInfo() == null + || event.getPositionInfo().getLongitude() == null || event.getPositionInfo().getLatitude() == null) { + return; + } + JTDevice device = getDevice(event.getPhoneNumber()); + if (device == null) { + return; + } + device.setLongitude(event.getPositionInfo().getLongitude()); + device.setLatitude(event.getPositionInfo().getLatitude()); + updateDevice(device); + + // 通道发送状态变化通知 + List jtChannels = jtChannelMapper.selectAll(device.getId(), null); + List channelList = new ArrayList<>(); + for (JTChannel jtChannel : jtChannels) { + if (jtChannel.getGbId() > 0) { + jtChannel.setGbLongitude(event.getPositionInfo().getLongitude()); + jtChannel.setGbLatitude(event.getPositionInfo().getLatitude()); + if (event.getPositionInfo().getAltitude() != null) { + jtChannel.setGpsAltitude((double) event.getPositionInfo().getAltitude()); + }else { + jtChannel.setGpsAltitude(0d); + } + if (event.getPositionInfo().getDirection() != null) { + jtChannel.setGpsDirection((double) event.getPositionInfo().getDirection()); + }else { + jtChannel.setGpsDirection(0d); + } + if (event.getPositionInfo().getTime() != null) { + jtChannel.setGpsTime(event.getPositionInfo().getTime()); + }else { + jtChannel.setGpsTime(DateUtil.getNow()); + } + channelList.add(jtChannel); + } + } + channelService.updateGPS(channelList); + } + + + /** + * 校验流是否是属于部标的 + */ + @Override + public JTMediaStreamType checkStreamFromJt(String stream) { + String[] streamParamArray = stream.split("_"); + if (streamParamArray.length == 2) { + return JTMediaStreamType.PLAY; + }else if (streamParamArray.length == 4) { + return JTMediaStreamType.PLAYBACK; + }else if (streamParamArray.length == 5) { + return JTMediaStreamType.TALK; + }else { + return null; + } + } + + @Override + public JTDevice getDevice(String phoneNumber) { + return jtDeviceMapper.getDevice(phoneNumber); + } + + @Override + public JTChannel getChannel(Integer terminalDbId, Integer channelId) { + return jtChannelMapper.selectChannelByChannelId(terminalDbId, channelId); + } + + @Override + public void updateDevice(JTDevice device) { + device.setUpdateTime(DateUtil.getNow()); + jtDeviceMapper.updateDevice(device); + } + + @Override + public PageInfo getDeviceList(int page, int count, String query, Boolean online) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = jtDeviceMapper.getDeviceList(query, online); + return new PageInfo<>(all); + } + + @Override + public void addDevice(JTDevice device) { + JTDevice deviceInDb = jtDeviceMapper.getDevice(device.getPhoneNumber()); + if (deviceInDb != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备" + device.getPhoneNumber() + "已存在"); + } + device.setCreateTime(DateUtil.getNow()); + device.setUpdateTime(DateUtil.getNow()); + jtDeviceMapper.addDevice(device); + } + + @Override + public void deleteDeviceByPhoneNumber(String phoneNumber) { + jtDeviceMapper.deleteDeviceByPhoneNumber(phoneNumber); + } + + @Override + public void updateDeviceStatus(boolean connected, String phoneNumber) { + jtDeviceMapper.updateDeviceStatus(connected, phoneNumber); + } + + + @Override + public void recordDownload(String phoneNumber, Integer channelId, String startTime, String endTime, Integer alarmSign, + Integer mediaType, Integer streamType, Integer storageType, OutputStream outputStream, CommonCallback> fileCallback) { + String filePath = UUID.randomUUID().toString(); + fileSystemFactory.addOutputStream(filePath, outputStream); + dynamicTask.startDelay(filePath, ()->{ + fileSystemFactory.removeOutputStream(filePath); + }, 2*60*60*1000); + Session session = SessionManager.INSTANCE.get(phoneNumber); + Assert.notNull(session, "连接不存在"); + InetSocketAddress socketAddress = session.getLoadAddress(); + String hostName = socketAddress.getHostName(); + + BaseUser randomUser = ftpUserManager.getRandomUser(); + + log.info("[JT-录像] 下载,设备:{}, 通道: {}, 开始时间: {}, 结束时间: {} 上传IP: {} 等待上传文件路径: {} 用户名: {}, 密码: {} ", + phoneNumber, channelId, startTime, endTime, hostName, filePath, randomUser.getName(), randomUser.getPassword()); + // 发送停止命令 + J9206 j92026 = new J9206(); + j92026.setChannelId(channelId); + j92026.setStartTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime)); + j92026.setEndTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime)); + j92026.setServerIp(hostName); + j92026.setPort(ftpSetting.getPort()); + j92026.setUsername(randomUser.getName()); + j92026.setPassword(randomUser.getPassword()); + j92026.setPath(filePath); + + if (mediaType != null) { + j92026.setMediaType(mediaType); + } + if (streamType != null) { + j92026.setStreamType(streamType); + } + if (storageType != null) { + j92026.setStorageType(storageType); + } + if (alarmSign != null) { + j92026.setAlarmSign(alarmSign); + } + jt1078Template.fileUpload(phoneNumber, j92026, 7200); + } + + @Override + public void ptzControl(String phoneNumber, Integer channelId, String command, int speed) { + // 发送停止命令 + switch (command) { + case "left": + case "right": + case "up": + case "down": + case "stop": + J9301 j9301 = new J9301(); + j9301.setChannel(channelId); + switch (command) { + case "left": + j9301.setDirection(3); + j9301.setSpeed(speed); + break; + case "right": + j9301.setDirection(4); + j9301.setSpeed(speed); + break; + case "up": + j9301.setDirection(1); + j9301.setSpeed(speed); + break; + case "down": + j9301.setDirection(2); + j9301.setSpeed(speed); + break; + case "stop": + j9301.setDirection(0); + j9301.setSpeed(0); + break; + } + jt1078Template.ptzRotate(phoneNumber, j9301, 6); + break; + + case "zoomin": + case "zoomout": + J9306 j9306 = new J9306(); + j9306.setChannel(channelId); + if (command.equals("zoomin")) { + j9306.setZoom(0); + } else { + j9306.setZoom(1); + } + jt1078Template.ptzZoom(phoneNumber, j9306, 6); + break; + case "irisin": + case "irisout": + J9303 j9303 = new J9303(); + j9303.setChannel(channelId); + if (command.equals("irisin")) { + j9303.setIris(0); + } else { + j9303.setIris(1); + } + jt1078Template.ptzIris(phoneNumber, j9303, 6); + break; + case "focusnear": + case "focusfar": + J9302 j9302 = new J9302(); + j9302.setChannel(channelId); + if (command.equals("focusfar")) { + j9302.setFocalDirection(0); + } else { + j9302.setFocalDirection(1); + } + jt1078Template.ptzFocal(phoneNumber, j9302, 6); + break; + + } + } + + @Override + public void supplementaryLight(String phoneNumber, Integer channelId, String command) { + J9305 j9305 = new J9305(); + j9305.setChannel(channelId); + if (command.equalsIgnoreCase("on")) { + j9305.setOn(1); + } else { + j9305.setOn(0); + } + jt1078Template.ptzSupplementaryLight(phoneNumber, j9305, 6); + } + + @Override + public void wiper(String phoneNumber, Integer channelId, String command) { + J9304 j9304 = new J9304(); + j9304.setChannel(channelId); + if (command.equalsIgnoreCase("on")) { + j9304.setOn(1); + } else { + j9304.setOn(0); + } + jt1078Template.ptzWiper(phoneNumber, j9304, 6); + } + + @Override + public JTDeviceConfig queryConfig(String phoneNumber, String[] params) { + if (phoneNumber == null) { + return null; + } + if (params == null || params.length == 0) { + J8104 j8104 = new J8104(); + return (JTDeviceConfig) jt1078Template.getDeviceConfig(phoneNumber, j8104, 20); + } else { + long[] paramBytes = new long[params.length]; + for (int i = 0; i < params.length; i++) { + try { + Field field = JTDeviceConfig.class.getDeclaredField(params[i]); + if (field.isAnnotationPresent(ConfigAttribute.class)) { + ConfigAttribute configAttribute = field.getAnnotation(ConfigAttribute.class); + if (configAttribute == null) { + log.warn("[查询设备配置] 获取 ConfigAttribute 失败"); + continue; + } + paramBytes[i] = configAttribute.id(); + } + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + J8106 j8106 = new J8106(); + j8106.setParams(paramBytes); + return (JTDeviceConfig) jt1078Template.getDeviceSpecifyConfig(phoneNumber, j8106, 20); + } + } + + @Override + public void setConfig(String phoneNumber, JTDeviceConfig config) { + J8103 j8103 = new J8103(); + j8103.setConfig(config); + jt1078Template.setDeviceSpecifyConfig(phoneNumber, j8103, 6); + } + + @Override + public void connectionControl(String phoneNumber, JTDeviceConnectionControl control) { + J8105 j8105 = new J8105(); + j8105.setConnectionControl(control); + jt1078Template.deviceControl(phoneNumber, j8105, 6); + } + + @Override + public void resetControl(String phoneNumber) { + J8105 j8105 = new J8105(); + j8105.setReset(true); + jt1078Template.deviceControl(phoneNumber, j8105, 6); + } + + @Override + public void factoryResetControl(String phoneNumber) { + J8105 j8105 = new J8105(); + j8105.setFactoryReset(true); + jt1078Template.deviceControl(phoneNumber, j8105, 6); + } + + @Override + public JTDeviceAttribute attribute(String phoneNumber) { + J8107 j8107 = new J8107(); + return (JTDeviceAttribute) jt1078Template.deviceAttribute(phoneNumber, j8107, 20); + } + + @Override + public JTPositionBaseInfo queryPositionInfo(String phoneNumber) { + J8201 j8201 = new J8201(); + return (JTPositionBaseInfo) jt1078Template.queryPositionInfo(phoneNumber, j8201, 20); + } + + @Override + public void tempPositionTrackingControl(String phoneNumber, Integer timeInterval, Long validityPeriod) { + J8202 j8202 = new J8202(); + j8202.setTimeInterval(timeInterval); + j8202.setValidityPeriod(validityPeriod); + jt1078Template.tempPositionTrackingControl(phoneNumber, j8202, 20); + } + + @Override + public void confirmationAlarmMessage(String phoneNumber, int alarmPackageNo, JTConfirmationAlarmMessageType alarmMessageType) { + J8203 j8203 = new J8203(); + j8203.setAlarmMessageType(alarmMessageType); + j8203.setAlarmPackageNo(alarmPackageNo); + jt1078Template.confirmationAlarmMessage(phoneNumber, j8203, 6); + } + + @Override + public int linkDetection(String phoneNumber) { + J8204 j8204 = new J8204(); + Object result = jt1078Template.linkDetection(phoneNumber, j8204, 6); + if (result == null) { + return 1; + }else { + return (int) result; + } + } + + @Override + public int textMessage(String phoneNumber, JTTextSign sign, int textType, String content) { + J8300 j8300 = new J8300(); + j8300.setSign(sign); + j8300.setTextType(textType); + j8300.setContent(content); + return (int) jt1078Template.textMessage(phoneNumber, j8300, 6); + } + + @Override + public int telephoneCallback(String phoneNumber, Integer sign, String destPhoneNumber) { + J8400 j8400 = new J8400(); + j8400.setSign(sign); + j8400.setPhoneNumber(destPhoneNumber); + return (int) jt1078Template.telephoneCallback(phoneNumber, j8400, 6); + } + + @Override + public int setPhoneBook(String phoneNumber, int type, List phoneBookContactList) { + J8401 j8401 = new J8401(); + j8401.setType(type); + if (phoneBookContactList != null) { + j8401.setPhoneBookContactList(phoneBookContactList); + } + return (int) jt1078Template.setPhoneBook(phoneNumber, j8401, 6); + } + + @Override + public JTPositionBaseInfo controlDoor(String phoneNumber, Boolean open) { + J8500 j8500 = new J8500(); + JTVehicleControl jtVehicleControl = new JTVehicleControl(); + jtVehicleControl.setControlCarDoor(open ? 1 : 0); + j8500.setVehicleControl(jtVehicleControl); + return (JTPositionBaseInfo) jt1078Template.vehicleControl(phoneNumber, j8500, 20); + } + + @Override + public int setAreaForCircle(int attribute, String phoneNumber, List circleAreaList) { + J8600 j8600 = new J8600(); + j8600.setAttribute(attribute); + j8600.setCircleAreaList(circleAreaList); + return (int) jt1078Template.setAreaForCircle(phoneNumber, j8600, 20); + } + + @Override + public int deleteAreaForCircle(String phoneNumber, List ids) { + J8601 j8601 = new J8601(); + j8601.setIdList(ids); + return (int) jt1078Template.deleteAreaForCircle(phoneNumber, j8601, 20); + } + + @Override + public List queryAreaForCircle(String phoneNumber, List ids) { + J8608 j8608 = new J8608(); + j8608.setType(1); + j8608.setIdList(ids); + return (List) jt1078Template.queryAreaOrRoute(phoneNumber, j8608, 20); + } + + @Override + public int setAreaForRectangle(int attribute, String phoneNumber, List rectangleAreas) { + J8602 j8602 = new J8602(); + j8602.setAttribute(attribute); + j8602.setRectangleAreas(rectangleAreas); + return (int) jt1078Template.setAreaForRectangle(phoneNumber, j8602, 20); + } + + @Override + public int deleteAreaForRectangle(String phoneNumber, List ids) { + J8603 j8603 = new J8603(); + j8603.setIdList(ids); + return (int) jt1078Template.deleteAreaForRectangle(phoneNumber, j8603, 20); + } + + @Override + public List queryAreaForRectangle(String phoneNumber, List ids) { + J8608 j8608 = new J8608(); + j8608.setType(2); + j8608.setIdList(ids); + return (List) jt1078Template.queryAreaOrRoute(phoneNumber, j8608, 20); + } + + @Override + public int setAreaForPolygon(String phoneNumber, JTPolygonArea polygonArea) { + J8604 j8604 = new J8604(); + j8604.setPolygonArea(polygonArea); + return (int) jt1078Template.setAreaForPolygon(phoneNumber, j8604, 20); + } + + @Override + public int deleteAreaForPolygon(String phoneNumber, List ids) { + J8605 j8605 = new J8605(); + j8605.setIdList(ids); + return (int) jt1078Template.deleteAreaForPolygon(phoneNumber, j8605, 20); + } + + @Override + public List queryAreaForPolygon(String phoneNumber, List ids) { + J8608 j8608 = new J8608(); + j8608.setType(3); + j8608.setIdList(ids); + return (List) jt1078Template.queryAreaOrRoute(phoneNumber, j8608, 20); + } + + @Override + public int setRoute(String phoneNumber, JTRoute route) { + J8606 j8606 = new J8606(); + j8606.setRoute(route); + return (int) jt1078Template.setRoute(phoneNumber, j8606, 20); + } + + @Override + public int deleteRoute(String phoneNumber, List ids) { + J8607 j8607 = new J8607(); + j8607.setIdList(ids); + return (int) jt1078Template.deleteRoute(phoneNumber, j8607, 20); + } + + @Override + public List queryRoute(String phoneNumber, List ids) { + J8608 j8608 = new J8608(); + j8608.setType(4); + j8608.setIdList(ids); + return (List) jt1078Template.queryAreaOrRoute(phoneNumber, j8608, 20); + } + + @Override + public JTDriverInformation queryDriverInformation(String phoneNumber) { + J8702 j8702 = new J8702(); + return (JTDriverInformation) jt1078Template.queryDriverInformation(phoneNumber, j8702, 20); + } + + @Override + public List shooting(String phoneNumber, JTShootingCommand shootingCommand) { + J8801 j8801 = new J8801(); + j8801.setCommand(shootingCommand); + return (List) jt1078Template.shooting(phoneNumber, j8801, 300); + } + + @Override + public List queryMediaData(String phoneNumber, JTQueryMediaDataCommand queryMediaDataCommand) { + J8802 j8802 = new J8802(); + j8802.setCommand(queryMediaDataCommand); + return (List) jt1078Template.queryMediaData(phoneNumber, j8802, 300); + } + + @Override + public void uploadMediaData(String phoneNumber, JTQueryMediaDataCommand queryMediaDataCommand) { + J8803 j8803 = new J8803(); + j8803.setCommand(queryMediaDataCommand); + jt1078Template.uploadMediaData(phoneNumber, j8803, 10); + } + + @Override + public void record(String phoneNumber, int command, Integer time, Integer save, Integer samplingRate) { + J8804 j8804 = new J8804(); + j8804.setCommond(command); + j8804.setDuration(time); + j8804.setSave(save); + j8804.setSamplingRate(samplingRate); + jt1078Template.record(phoneNumber, j8804, 10); + } + + @Override + public void uploadMediaDataForSingle(String phoneNumber, Long mediaId, Integer delete) { + J8805 j8805 = new J8805(); + j8805.setMediaId(mediaId); + j8805.setDelete(delete); + jt1078Template.uploadMediaDataForSingle(phoneNumber, j8805, 10); + } + + @Override + public JTMediaAttribute queryMediaAttribute(String phoneNumber) { + J9003 j9003 = new J9003(); + return (JTMediaAttribute) jt1078Template.queryMediaAttribute(phoneNumber, j9003, 300); + } + + @Override + public void changeStreamType(String phoneNumber, Integer channelId, Integer streamType) { + String playKey = VideoManagerConstants.INVITE_INFO_1078_PLAY + phoneNumber + ":" + channelId; + dynamicTask.stop(playKey); + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + if (streamInfo == null) { + log.info("[JT-切换码流类型] 未找到点播信息 phoneNumber: {}, channelId: {}, streamType: {}", phoneNumber, channelId, streamType); + } + log.info("[JT-切换码流类型] phoneNumber: {}, channelId: {}, streamType: {}", phoneNumber, channelId, streamType); + // 发送暂停命令 + J9102 j9102 = new J9102(); + j9102.setChannel(channelId); + j9102.setCommand(1); + j9102.setCloseType(0); + j9102.setStreamType(streamType); + jt1078Template.stopLive(phoneNumber, j9102, 6); + } + + @Override + public PageInfo getChannelList(int page, int count, int deviceId, String query) { + + JTDevice device = getDeviceById(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备不存在"); + } + PageHelper.startPage(page, count); + List all = jtChannelMapper.selectAll(deviceId, query); + PageInfo jtChannelPageInfo = new PageInfo<>(all); + for (JTChannel jtChannel : jtChannelPageInfo.getList()) { + String playKey = VideoManagerConstants.INVITE_INFO_1078_PLAY + device.getPhoneNumber() + ":" + jtChannel.getChannelId(); + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + if (streamInfo != null) { + jtChannel.setStream(streamInfo.getStream()); + } + } + return new PageInfo<>(all); + } + + @Override + @Transactional + public void updateChannel(JTChannel channel) { + channel.setUpdateTime(DateUtil.getNow()); + jtChannelMapper.update(channel); + if (!ObjectUtils.isEmpty(channel.getGbDeviceId())) { + if (channel.getGbId() > 0) { + channelService.update(channel.buildCommonGBChannel()); + }else { + channelService.add(channel.buildCommonGBChannel()); + } + } + } + + @Override + @Transactional + public void addChannel(JTChannel channel) { + JTChannel channelInDb = jtChannelMapper.selectChannelByChannelId(channel.getTerminalDbId(), channel.getChannelId()); + if (channelInDb != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道已存在"); + } + channel.setCreateTime(DateUtil.getNow()); + channel.setUpdateTime(DateUtil.getNow()); + jtChannelMapper.add(channel); + if (!ObjectUtils.isEmpty(channel.getGbDeviceId())) { + channelService.add(channel.buildCommonGBChannel()); + } + } + + @Override + @Transactional + public void deleteChannelById(Integer id) { + JTChannel jtChannel = jtChannelMapper.selectChannelById(id); + if (jtChannel == null) { + return; + } + if (jtChannel.getGbId() > 0) { + channelService.delete(jtChannel.getGbId()); + } + jtChannelMapper.delete(id); + } + + @Override + public JTDevice getDeviceById(Integer deviceId) { + return jtDeviceMapper.getDeviceById(deviceId); + } + + @Override + public void updateDevicePosition(String phoneNumber, Double longitude, Double latitude) { + JTDevice device = new JTDevice(); + device.setPhoneNumber(phoneNumber); + device.setLongitude(longitude); + device.setLatitude(latitude); + device.setUpdateTime(DateUtil.getNow()); + String key = VideoManagerConstants.INVITE_INFO_1078_POSITION + userSetting.getServerId(); + redisTemplate.opsForList().leftPush(key, device); + } + + @Scheduled(fixedDelay = 1000) + public void positionTask(){ + String key = VideoManagerConstants.INVITE_INFO_1078_POSITION + userSetting.getServerId(); + int count = 1000; + List devices = new ArrayList<>(count); + Long size = redisTemplate.opsForList().size(key); + if (size == null || size == 0) { + return; + } + long readCount = Math.min(count, size); + for (long i = 0L; i < readCount; i++) { + devices.add((JTDevice)redisTemplate.opsForList().rightPop(key)); + } + jtDeviceMapper.batchUpdateDevicePosition(devices); + } + + @Override + public JTChannel getChannelByDbId(Integer id) { + return jtChannelMapper.selectChannelById(id); + } + + + + @Override + public String getRecordTempUrl(String phoneNumber, Integer channelId, String startTime, String endTime, Integer alarmSign, Integer mediaType, Integer streamType, Integer storageType) { + String filePath = UUID.randomUUID().toString(); + + Session session = SessionManager.INSTANCE.get(phoneNumber); + Assert.notNull(session, "连接不存在"); + InetSocketAddress socketAddress = session.getLoadAddress(); + String hostName = socketAddress.getHostName(); + + BaseUser randomUser = ftpUserManager.getRandomUser(); + + log.info("[JT-录像] 下载,设备:{}, 通道: {}, 开始时间: {}, 结束时间: {} 上传IP: {} 等待上传文件路径: {} 用户名: {}, 密码: {} ", + phoneNumber, channelId, startTime, endTime, hostName, filePath, randomUser.getName(), randomUser.getPassword()); + // 文件上传指令 + J9206 j9206 = new J9206(); + j9206.setChannelId(channelId); + j9206.setStartTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime)); + j9206.setEndTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime)); + j9206.setServerIp(hostName); + j9206.setPort(ftpSetting.getPort()); + j9206.setUsername(randomUser.getName()); + j9206.setPassword(randomUser.getPassword()); + j9206.setPath(filePath); + + if (mediaType != null) { + j9206.setMediaType(mediaType); + } + if (streamType != null) { + j9206.setStreamType(streamType); + } + if (storageType != null) { + j9206.setStorageType(storageType); + } + if (alarmSign != null) { + j9206.setAlarmSign(alarmSign); + } + downloadManager.addCatch(filePath, phoneNumber, j9206); + return filePath; + } + + + @Override + public void recordDownload(String filePath, ServletOutputStream outputStream) { + JTRecordDownloadCatch downloadCatch = downloadManager.getCatch(filePath); + Assert.notNull(downloadCatch, "地址不存在"); + fileSystemFactory.addOutputStream(filePath, outputStream); + jt1078Template.fileUpload(downloadCatch.getPhoneNumber(), downloadCatch.getJ9206(), 7200); + downloadManager.runDownload(filePath, 2 * 60 * 60); + fileSystemFactory.removeOutputStream(filePath); + + } + + + @Override + public byte[] snap(String phoneNumber, int channelId) { + J8801 j8801 = new J8801(); + + // 设置抓图默认参数 + JTShootingCommand shootingCommand = new JTShootingCommand(); + shootingCommand.setChanelId(channelId); + shootingCommand.setCommand(1); + shootingCommand.setTime(0); + shootingCommand.setSave(0); + shootingCommand.setResolvingPower(0xFF); + shootingCommand.setQuality(1); + shootingCommand.setBrightness(125); + shootingCommand.setContrastRatio(60); + shootingCommand.setSaturation(60); + shootingCommand.setChroma(125); + + j8801.setCommand(shootingCommand); + log.info("[JT-抓图] 设备编号: {}, 通道编号: {}", phoneNumber, channelId); + // 监听文件上传, 存在设备不回复抓图请求或者回复通用回复,导致缺少抓图编号,但是直接上传文件的,此处通过监听文件上传直接获取文件 + + @SuppressWarnings("unchecked") + List ids = (List) jt1078Template.shooting(phoneNumber, j8801, 300); + log.info("[JT-抓图] 抓图编号: {}, 设备编号: {}, 通道编号: {}", ids.get(0), phoneNumber, channelId); + + log.info("[JT-抓图] 请求上传图片,抓图编号: {}, 设备编号: {}, 通道编号: {}", ids.get(0), phoneNumber, channelId); + J8805 j8805 = new J8805(); + j8805.setMediaId(ids.get(0)); + j8805.setDelete(1); + JTMediaEventInfo mediaEventInfo = (JTMediaEventInfo)jt1078Template.uploadMediaDataForSingle(phoneNumber, j8805, 600); + if (mediaEventInfo == null) { + log.info("[]"); + throw new ControllerException(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg()); + } + log.info("[JT-抓图] 图片上传完成,抓图编号: {}, 设备编号: {}, 通道编号: {}", ids.get(0), phoneNumber, channelId); + return mediaEventInfo.getMediaData(); + } + + @Override + public void uploadOneMedia(String phoneNumber, Long mediaId, ServletOutputStream outputStream, boolean delete) { + log.info("[JT-单条存储多媒体数据上传] 媒体编号: {}, 设备编号: {}", mediaId, phoneNumber); + J8805 j8805 = new J8805(); + j8805.setMediaId(mediaId); + j8805.setDelete(delete ? 1 : 0); + log.info("[JT-单条存储多媒体数据上传] 请求上传,媒体编号: {}, 设备编号: {}", mediaId, phoneNumber); + JTMediaEventInfo mediaEventInfo = (JTMediaEventInfo)jt1078Template.uploadMediaDataForSingle(phoneNumber, j8805, 600); + if (mediaEventInfo == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg()); + } + log.info("[JT-单条存储多媒体数据上传] 图片上传完成,媒体编号: {}, 设备编号: {}", mediaId, phoneNumber); + try { + outputStream.write(mediaEventInfo.getMediaData()); + outputStream.flush(); + } catch (IOException e) { + log.info("[JT-单条存储多媒体数据上传] 数据写入异常,抓图编号: {}, 设备编号: {}", mediaId, phoneNumber, e); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "数据写入异常"); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/session/FtpDownloadManager.java b/src/main/java/com/genersoft/iot/vmp/jt1078/session/FtpDownloadManager.java new file mode 100644 index 0000000..01ce90b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/session/FtpDownloadManager.java @@ -0,0 +1,105 @@ +package com.genersoft.iot.vmp.jt1078.session; + +import com.genersoft.iot.vmp.jt1078.bean.JTRecordDownloadCatch; +import com.genersoft.iot.vmp.jt1078.event.FtpUploadEvent; +import com.genersoft.iot.vmp.jt1078.proc.response.J9206; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class FtpDownloadManager { + + private final Map downloadCatchMap = new ConcurrentHashMap<>(); + private final DelayQueue downloadCatchQueue = new DelayQueue<>(); + + private final Map> topicSubscribers = new ConcurrentHashMap<>(); + + // 下载过期检查 + @Scheduled(fixedDelay = 1, timeUnit = TimeUnit.SECONDS) + public void downloadCatchCheck(){ + while (!downloadCatchQueue.isEmpty()) { + try { + JTRecordDownloadCatch take = downloadCatchQueue.take(); + downloadCatchMap.remove(take.getPath()); + } catch (InterruptedException e) { + log.error("[下载过期] ", e); + } + } + } + + public void addCatch(String path, String phoneNumber, J9206 j9206) { + JTRecordDownloadCatch downloadCatch = new JTRecordDownloadCatch(); + downloadCatch.setPhoneNumber(phoneNumber); + downloadCatch.setPath(path); + downloadCatch.setJ9206(j9206); + + // 10分钟临时地址无法访问则删除 + downloadCatch.setDelayTime(System.currentTimeMillis() + 10 * 60 * 1000L); + + downloadCatchMap.put(path, downloadCatch); + downloadCatchQueue.add(downloadCatch); + } + + public JTRecordDownloadCatch getCatch(String path) { + return downloadCatchMap.get(path); + } + + @EventListener + public void onApplicationEvent(FtpUploadEvent event) { + if (topicSubscribers.isEmpty()) { + return; + } + topicSubscribers.keySet().forEach(key -> { + if (!event.getFileName().contains(key)) { + return; + } + SynchronousQueue synchronousQueue = topicSubscribers.get(key); + if (synchronousQueue != null) { + synchronousQueue.offer(null); + } + }); + } + + + public Object runDownload(String path, long timeOut) { + SynchronousQueue subscribe = subscribe(path); + if (subscribe == null) { + log.error("[JT-下载] 暂停进程失败"); + return null; + } + try { + return subscribe.poll(timeOut, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.warn("[JT-下载] 暂停进程超时", e); + } finally { + this.unsubscribe(path); + JTRecordDownloadCatch downloadCatch = getCatch(path); + if (downloadCatch != null) { + downloadCatchMap.remove(path); + downloadCatchQueue.remove(downloadCatch); + } + } + return null; + } + + private SynchronousQueue subscribe(String key) { + SynchronousQueue queue = null; + if (!topicSubscribers.containsKey(key)) + topicSubscribers.put(key, queue = new SynchronousQueue<>()); + return queue; + } + + private void unsubscribe(String key) { + topicSubscribers.remove(key); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/session/Session.java b/src/main/java/com/genersoft/iot/vmp/jt1078/session/Session.java new file mode 100644 index 0000000..9c3d13a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/session/Session.java @@ -0,0 +1,111 @@ +package com.genersoft.iot.vmp.jt1078.session; + +import com.genersoft.iot.vmp.jt1078.proc.Header; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.util.AttributeKey; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:54 + * @email qingtaij@163.com + */ +@Slf4j +public class Session { + + public static final AttributeKey KEY = AttributeKey.newInstance(Session.class.getName()); + + // Netty的channel + protected final Channel channel; + + // 原子类的自增ID + private final AtomicInteger serialNo = new AtomicInteger(0); + + // 是否注册成功 + @Getter + private boolean registered = false; + + // 设备手机号 + @Getter + private String phoneNumber; + + // 设备手机号 + @Setter + @Getter + private String authenticationCode; + + // 创建时间 + @Getter + private final long creationTime; + + // 协议版本号 + @Getter + private Integer protocolVersion; + + @Getter + private Header header; + + protected Session(Channel channel) { + this.channel = channel; + this.creationTime = System.currentTimeMillis(); + } + + public void writeObject(Object message) { + log.info("<<<<<<<<<< cmd{},{}", this, message); + channel.writeAndFlush(message); + } + + /** + * 获得下一个流水号 + * + * @return 流水号 + */ + public int nextSerialNo() { + int current; + int next; + do { + current = serialNo.get(); + next = current > 0xffff ? 0 : current; + } while (!serialNo.compareAndSet(current, next + 1)); + return next; + } + + /** + * 注册session + * + * @param devId 设备ID + */ + public void register(String devId, Integer version, Header header) { + this.phoneNumber = devId; + this.registered = true; + this.protocolVersion = version; + this.header = header; + SessionManager.INSTANCE.put(devId, this); + } + + @Override + public String toString() { + return "[" + + "phoneNumber=" + phoneNumber + + ", reg=" + registered + + ", version=" + protocolVersion + + ",ip=" + channel.remoteAddress() + + ']'; + } + + public void unregister() { + channel.close(); + SessionManager.INSTANCE.remove(this.phoneNumber); + } + + public InetSocketAddress getLoadAddress() { + return (InetSocketAddress)channel.localAddress(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/session/SessionManager.java b/src/main/java/com/genersoft/iot/vmp/jt1078/session/SessionManager.java new file mode 100644 index 0000000..48979f8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/session/SessionManager.java @@ -0,0 +1,151 @@ +package com.genersoft.iot.vmp.jt1078.session; + +import com.genersoft.iot.vmp.jt1078.proc.entity.Cmd; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; + + +/** + * @author QingtaiJiang + * @date 2023/4/27 19:54 + * @email qingtaij@163.com + */ +@Slf4j +public enum SessionManager { + INSTANCE; + + // 用与消息的缓存 + private final Map> topicSubscribers = new ConcurrentHashMap<>(); + + // session的缓存 + private final Map sessionMap; + + SessionManager() { + this.sessionMap = new ConcurrentHashMap<>(); + } + + /** + * 创建新的Session + * + * @param channel netty通道 + * @return 创建的session对象 + */ + public Session newSession(Channel channel) { + return new Session(channel); + } + + + /** + * 获取指定设备的Session + * + * @param clientId 设备Id + * @return Session + */ + public Session get(Object clientId) { + return sessionMap.get(clientId); + } + + /** + * 放入新设备连接的session + * + * @param clientId 设备ID + * @param newSession session + */ + void put(Object clientId, Session newSession) { + sessionMap.put(clientId, newSession); + } + + + /** + * 发送同步消息,接收响应 + * 默认超时时间6秒 + */ + public Object request(Cmd cmd) { + // 默认6秒 + int timeOut = 6000; + return request(cmd, timeOut); + } + + public Object request(Cmd cmd, Integer timeOut) { + Session session = this.get(cmd.getPhoneNumber()); + if (session == null) { + log.error("DevId: {} not online!", cmd.getPhoneNumber()); + return null; + } + String requestKey = requestKey(cmd.getPhoneNumber(), cmd.getRespId(), cmd.getPackageNo()); + SynchronousQueue subscribe = subscribe(requestKey); + if (subscribe == null) { + log.error("DevId: {} key:{} send repaid", cmd.getPhoneNumber(), requestKey); + return null; + } + session.writeObject(cmd); + try { + return subscribe.poll(timeOut, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.warn("<<<<<<<<<< timeout" + session, e); + } finally { + this.unsubscribe(requestKey); + } + return null; + } + + public Boolean response(String devId, String respId, Long responseNo, Object data) { + String requestKey = requestKey(devId, respId, responseNo); + + boolean result = false; + if (responseNo == null) { + for (String key : topicSubscribers.keySet()) { + if (key.startsWith(requestKey)) { + SynchronousQueue queue = topicSubscribers.get(key); + if (queue != null) { + result = true; + try { + queue.offer(data, 2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error("{}", e.getMessage(), e); + } + } + } + } + }else { + SynchronousQueue queue = topicSubscribers.get(requestKey); + if (queue != null) { + result = true; + try { + queue.offer(data, 2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error("{}", e.getMessage(), e); + } + } + } + if (!result) { + log.warn("Not find response,key:{} data:{} ", requestKey, data); + } + return result; + } + + private void unsubscribe(String key) { + topicSubscribers.remove(key); + } + + private SynchronousQueue subscribe(String key) { + SynchronousQueue queue = null; + if (!topicSubscribers.containsKey(key)) + topicSubscribers.put(key, queue = new SynchronousQueue<>()); + return queue; + } + + private String requestKey(String devId, String respId, Long requestNo) { + return String.join("_", devId.replaceFirst("^0*", ""), respId, requestNo == null?"":requestNo.toString()); + + } + + public void remove(String devId) { + sessionMap.remove(devId); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/util/BCDUtil.java b/src/main/java/com/genersoft/iot/vmp/jt1078/util/BCDUtil.java new file mode 100644 index 0000000..b76786e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/util/BCDUtil.java @@ -0,0 +1,64 @@ +package com.genersoft.iot.vmp.jt1078.util; + +/** + * BCD码转换 + */ +public class BCDUtil { + + public static String transform(byte[] bytes) { + if (bytes.length == 0) { + return null; + } + // BCD + StringBuilder stringBuffer = new StringBuilder(bytes.length * 2); + for (int i = 0; i < bytes.length; i++) { + // 每次取出四位的值,一个byte是八位,第一取出高四位,第二次取出低四位, + // 这里也可以先 & 0xf0再右移4位,0xf0二进制为11110000,与运算后,可以得到高4位是值,低四位清零的结果 + stringBuffer.append((byte) ((bytes[i] >>> 4 & 0xf))); + stringBuffer.append((byte) (bytes[i] & 0x0f)); + } + return stringBuffer.toString(); + } + + /** + * 字符串转BCD码 + * 来自: https://www.cnblogs.com/ranandrun/p/BCD.html + * @param asc ASCII字符串 + * @return BCD + */ + public static byte[] strToBcd(String asc) { + int len = asc.length(); + int mod = len % 2; + if (mod != 0) { + asc = "0" + asc; + len = asc.length(); + } + byte abt[] = new byte[len]; + if (len >= 2) { + len >>= 1; + } + byte bbt[] = new byte[len]; + abt = asc.getBytes(); + int j, k; + for (int p = 0; p < asc.length() / 2; p++) { + if ((abt[2 * p] >= '0') && (abt[2 * p] <= '9')) { + j = abt[2 * p] - '0'; + } else if ((abt[2 * p] >= 'a') && (abt[2 * p] <= 'z')) { + j = abt[2 * p] - 'a' + 0x0a; + } else { + j = abt[2 * p] - 'A' + 0x0a; + } + if ((abt[2 * p + 1] >= '0') && (abt[2 * p + 1] <= '9')) { + k = abt[2 * p + 1] - '0'; + } else if ((abt[2 * p + 1] >= 'a') && (abt[2 * p + 1] <= 'z')) { + k = abt[2 * p + 1] - 'a' + 0x0a; + } else { + k = abt[2 * p + 1] - 'A' + 0x0a; + } + int a = (j << 4) + k; + byte b = (byte) a; + bbt[p] = b; + } + return bbt; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/util/Bin.java b/src/main/java/com/genersoft/iot/vmp/jt1078/util/Bin.java new file mode 100644 index 0000000..31f8b93 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/util/Bin.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.util; + +/** + * 32位整型的二进制读写 + */ +public class Bin { + + private static final int[] bits = new int[32]; + + static { + bits[0] = 1; + for (int i = 1; i < bits.length; i++) { + bits[i] = bits[i - 1] << 1; + } + } + + /** + * 读取n的第i位 + * + * @param n int32 + * @param i 取值范围0-31 + */ + public static boolean get(int n, int i) { + return (n & bits[i]) == bits[i]; + } + + /** + * 不足位数从左边加0 + */ + public static String strHexPaddingLeft(String data, int length) { + int dataLength = data.length(); + if (dataLength < length) { + StringBuilder dataBuilder = new StringBuilder(data); + for (int i = dataLength; i < length; i++) { + dataBuilder.insert(0, "0"); + } + data = dataBuilder.toString(); + } + return data; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/util/ClassUtil.java b/src/main/java/com/genersoft/iot/vmp/jt1078/util/ClassUtil.java new file mode 100644 index 0000000..def0c0e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/util/ClassUtil.java @@ -0,0 +1,116 @@ +package com.genersoft.iot.vmp.jt1078.util; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; + +import java.lang.annotation.Annotation; +import java.util.LinkedList; +import java.util.List; + +@Slf4j +public class ClassUtil { + + public static ConfigurableApplicationContext context; + + public static T getBean(String beanName, Class clazz) { + return context.getBean(beanName, clazz); + } + + public static Object getBean(Class clazz) { + if (clazz != null) { + try { + return clazz.getDeclaredConstructor().newInstance(); + } catch (Exception ex) { + log.error("ClassUtil:找不到指定的类", ex); + } + } + return null; + } + + + public static Object getBean(String className) { + Class clazz = null; + try { + clazz = Class.forName(className); + } catch (Exception ex) { + log.error("ClassUtil:找不到指定的类"); + } + if (clazz != null) { + try { + //获取声明的构造器--》创建实例 + return clazz.getDeclaredConstructor().newInstance(); + } catch (Exception ex) { + log.error("ClassUtil:找不到指定的类", ex); + } + } + return null; + } + + + /** + * 获取包下所有带注解的class + * + * @param packageName 包名 + * @param annotationClass 注解类型 + * @return list + */ + public static List> getClassList(String packageName, Class annotationClass) { + List> classList = getClassList(packageName); + classList.removeIf(next -> !next.isAnnotationPresent(annotationClass)); + return classList; + } + + public static List> getClassList(String... packageName) { + List> classList = new LinkedList<>(); + for (String s : packageName) { + List> c = getClassList(s); + classList.addAll(c); + } + return classList; + } + + public static List> getClassList(String packageName) { + List> classList = new LinkedList<>(); + try { + ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = resourcePatternResolver.getResources(packageName.replace(".", "/") + "/**/*.class"); + for (Resource resource : resources) { + String url = resource.getURL().toString(); + + String[] split = url.split(packageName.replace(".", "/")); + String s = split[split.length - 1]; + String className = s.replace("/", "."); + className = className.substring(0, className.lastIndexOf(".")); + doAddClass(classList, packageName + className); + } + + } catch (Exception e) { + throw new RuntimeException(e); + } + return classList; + } + + private static void doAddClass(List> classList, String className) { + Class cls = loadClass(className, false); + classList.add(cls); + } + + public static Class loadClass(String className, boolean isInitialized) { + Class cls; + try { + cls = Class.forName(className, isInitialized, getClassLoader()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + return cls; + } + + + public static ClassLoader getClassLoader() { + return Thread.currentThread().getContextClassLoader(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/MediaServerConfig.java b/src/main/java/com/genersoft/iot/vmp/media/MediaServerConfig.java new file mode 100755 index 0000000..ba27fb3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/MediaServerConfig.java @@ -0,0 +1,67 @@ +package com.genersoft.iot.vmp.media; + +import com.genersoft.iot.vmp.conf.MediaConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerChangeEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 启动是从配置文件加载节点信息,以及发送个节点状态管理去控制节点状态 + */ +@Slf4j +@Component +@Order(value=12) +public class MediaServerConfig implements CommandLineRunner { + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private MediaConfig mediaConfig; + + @Autowired + private UserSetting userSetting; + + + @Override + public void run(String... strings) throws Exception { + // 清理所有在线节点的缓存信息 + mediaServerService.clearMediaServerForOnline(); + MediaServer defaultMediaServer = mediaServerService.getDefaultMediaServer(); + MediaServer mediaSerItemInConfig = mediaConfig.getMediaSerItem(); + mediaSerItemInConfig.setServerId(userSetting.getServerId()); + if (defaultMediaServer != null && mediaSerItemInConfig.getId().equals(defaultMediaServer.getId())) { + mediaServerService.update(mediaSerItemInConfig); + }else { + if (defaultMediaServer != null) { + mediaServerService.delete(defaultMediaServer); + } + MediaServer mediaServerItem = mediaServerService.getOneFromDatabase(mediaSerItemInConfig.getId()); + if (mediaServerItem == null) { + mediaServerService.add(mediaSerItemInConfig); + }else { + mediaServerService.update(mediaSerItemInConfig); + } + } + // 发送媒体节点变化事件 + mediaServerService.syncCatchFromDatabase(); + // 获取所有的zlm, 并开启主动连接 + List all = mediaServerService.getAllFromDatabase(); + log.info("[媒体节点] 加载节点列表, 共{}个节点", all.size()); + MediaServerChangeEvent event = new MediaServerChangeEvent(this); + event.setMediaServerItemList(all); + applicationEventPublisher.publishEvent(event); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/ABLHttpHookListener.java b/src/main/java/com/genersoft/iot/vmp/media/abl/ABLHttpHookListener.java new file mode 100644 index 0000000..17a6c56 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/ABLHttpHookListener.java @@ -0,0 +1,387 @@ +package com.genersoft.iot.vmp.media.abl; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.session.AudioBroadcastManager; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.media.abl.bean.hook.*; +import com.genersoft.iot.vmp.media.abl.event.HookAblServerKeepaliveEvent; +import com.genersoft.iot.vmp.media.abl.event.HookAblServerStartEvent; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.ResultForOnPublish; +import com.genersoft.iot.vmp.media.event.media.*; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.hook.HookResult; +import com.genersoft.iot.vmp.media.zlm.dto.hook.HookResultForOnPublish; +import com.genersoft.iot.vmp.service.IMediaService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import io.swagger.v3.oas.annotations.Hidden; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +/** + * ABL 的hook事件监听 + */ +@RestController +@RequestMapping("/index/hook/abl") +@Hidden +public class ABLHttpHookListener { + + private final static Logger logger = LoggerFactory.getLogger(ABLHttpHookListener.class); + + @Autowired + private ABLRESTfulUtils ablresTfulUtils; + + @Autowired + private ISIPCommanderForPlatform commanderFroPlatform; + + @Autowired + private AudioBroadcastManager audioBroadcastManager; + + @Autowired + private IPlayService playService; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IMediaServerService mediaServerService; + + + @Autowired + private IMediaService mediaService; + + + @Autowired + private UserSetting userSetting; + + @Autowired + private SSRCFactory ssrcFactory; + + @Qualifier("taskExecutor") + @Autowired + private ThreadPoolTaskExecutor taskExecutor; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + /** + * 服务器定时上报时间,上报间隔可配置,默认10s上报一次 + */ + @ResponseBody + @PostMapping(value = "/on_server_keepalive", produces = "application/json;charset=UTF-8") + public HookResult onServerKeepalive(@RequestBody OnServerKeepaliveABLHookParam param) { + try { + HookAblServerKeepaliveEvent event = new HookAblServerKeepaliveEvent(this); + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + event.setMediaServerItem(mediaServerItem); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + logger.info("[ZLM-HOOK-心跳] 发送通知失败 ", e); + } + return HookResult.SUCCESS(); + } + + /** + * 播放器鉴权事件,rtsp/rtmp/http-flv/ws-flv/hls的播放都将触发此鉴权事件。 + */ + @ResponseBody + @PostMapping(value = "/on_play", produces = "application/json;charset=UTF-8") + public HookResult onPlay(@RequestBody OnPlayABLHookParam param) { + + MediaServer mediaServer = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServer == null) { + return new HookResultForOnPublish(0, "success"); + } + + Map paramMap = urlParamToMap(param.getParams()); + // 对于播放流进行鉴权 + boolean authenticateResult = mediaService.authenticatePlay(param.getApp(), param.getStream(), paramMap.get("callId")); + if (!authenticateResult) { + logger.info("[ABL HOOK] 播放鉴权 失败:{}->{}", param.getMediaServerId(), param); + ablresTfulUtils.closeStreams(mediaServer, param.getApp(), param.getStream()); + + } + logger.info("[ABL HOOK] 播放鉴权成功:{}->{}", param.getMediaServerId(), param); + return HookResult.SUCCESS(); + } + + /** + * rtsp/rtmp/rtp推流鉴权事件。 + */ + @ResponseBody + @PostMapping(value = "/on_publish", produces = "application/json;charset=UTF-8") + public HookResult onPublish(@RequestBody OnPublishABLHookParam param) { + + + logger.info("[ABL HOOK] 推流鉴权:{}->{}/{}?{}", param.getMediaServerId(), param.getApp(), param.getStream(), param.getParams()); + // TODO 加快处理速度 + + MediaServer mediaServer = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServer == null) { + return new HookResultForOnPublish(0, "success"); + } + + try { + ResultForOnPublish resultForOnPublish = mediaService.authenticatePublish(mediaServer, param.getApp(), param.getStream(), param.getParams()); + if (resultForOnPublish == null) { + logger.info("[ABL HOOK]推流鉴权 拒绝 响应:{}->{}", param.getMediaServerId(), param); + ablresTfulUtils.closeStreams(mediaServer, param.getApp(), param.getStream()); + } + }catch (ControllerException e) { + ablresTfulUtils.closeStreams(mediaServer, param.getApp(), param.getStream()); + } + return HookResult.SUCCESS(); + } + + /** + * 如果某一个码流进行MP4录像(enable_mp4=1),会触发录像进度通知事件 + */ + @ResponseBody + @PostMapping(value = "/on_record_progress", produces = "application/json;charset=UTF-8") + public HookResult onRecordProgress(@RequestBody OnRecordProgressABLHookParam param) { + + + logger.info("[ABL HOOK] 录像进度通知:{}->{}/{}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream(), param.getCurrentFileDuration(), param.getTotalVideoDuration()); + + try { + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + MediaRecordProcessEvent event = MediaRecordProcessEvent.getInstance(this, param, mediaServerItem); + event.setMediaServer(mediaServerItem); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + logger.info("[ZLM-HOOK-录像进度通知] 发送通知失败 ", e); + } + return HookResult.SUCCESS(); + } + + /** + * 当代理拉流、国标接入等等 码流不到达时会发出 码流不到达的事件通知 + */ + @ResponseBody + @PostMapping(value = "/on_stream_not_arrive", produces = "application/json;charset=UTF-8") + public HookResult onStreamNotArrive(@RequestBody ABLHookParam param) { + + + logger.info("[ABL HOOK] 码流不到达通知:{}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream()); + try { + if ("rtp".equals(param.getApp())) { + return HookResult.SUCCESS(); + } + MediaRtpServerTimeoutEvent event = new MediaRtpServerTimeoutEvent(this); + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + event.setMediaServer(mediaServerItem); + event.setApp("rtp"); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + logger.info("[ABL-HOOK-码流不到达通知] 发送通知失败 ", e); + } + + return HookResult.SUCCESS(); + } + + /** + * 如果某一个码流进行MP4录像(enable_mp4=1),当某个MP4文件被删除会触发该事件通知 + */ + @ResponseBody + @PostMapping(value = "/on_delete_record_mp4", produces = "application/json;charset=UTF-8") + public HookResult onDeleteRecordMp4(@RequestBody OnRecordMp4ABLHookParam param) { + + + logger.info("[ABL HOOK] MP4文件被删除通知:{}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream()); + + + return HookResult.SUCCESS(); + } + + + /** + * rtsp/rtmp流注册或注销时触发此事件;此事件对回复不敏感。 + */ + @ResponseBody + @PostMapping(value = "/on_stream_arrive", produces = "application/json;charset=UTF-8") + public HookResult onStreamArrive(@RequestBody OnStreamArriveABLHookParam param) { + + MediaServer mediaServer = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServer == null) { + return HookResult.SUCCESS(); + } + + logger.info("[ABL HOOK] 码流到达, {}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream()); + MediaArrivalEvent mediaArrivalEvent = MediaArrivalEvent.getInstance(this, param, mediaServer); + applicationEventPublisher.publishEvent(mediaArrivalEvent); + return HookResult.SUCCESS(); + } + + /** + * 流无人观看时事件,用户可以通过此事件选择是否关闭无人看的流。 + */ + @ResponseBody + @PostMapping(value = "/on_stream_none_reader", produces = "application/json;charset=UTF-8") + public JSONObject onStreamNoneReader(@RequestBody ABLHookParam param) { + + logger.info("[ABL HOOK]流无人观看:{}->{}/{}", param.getMediaServerId(), + param.getApp(), param.getStream()); + JSONObject ret = new JSONObject(); + + boolean close = mediaService.closeStreamOnNoneReader(param.getMediaServerId(), param.getApp(), param.getStream(), null); + ret.put("code", close); + return ret; + } + + /** + * 当播放一个url,如果不存在时,会发出一个消息通知 + */ + @ResponseBody + @PostMapping(value = "/on_stream_not_found", produces = "application/json;charset=UTF-8") + public HookResult onStreamNotFound(@RequestBody ABLHookParam param) { + + logger.info("[ABL HOOK] 流未找到:{}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream()); + MediaServer mediaServer = mediaServerService.getOne(param.getMediaServerId()); + if (!userSetting.getAutoApplyPlay() || mediaServer == null) { + return HookResult.SUCCESS(); + } + MediaNotFoundEvent mediaNotFoundEvent = MediaNotFoundEvent.getInstance(this, param, mediaServer); + applicationEventPublisher.publishEvent(mediaNotFoundEvent); + return HookResult.SUCCESS(); + } + + /** + * ABLMediaServer启动时会发送上线通知 + */ + @ResponseBody + @PostMapping(value = "/on_server_started", produces = "application/json;charset=UTF-8") + public HookResult onServerStarted(HttpServletRequest request, @RequestBody OnServerStaredABLHookParam param) { + + logger.info("[ABL HOOK] 启动 " + param.getMediaServerId()); + try { + HookAblServerStartEvent event = new HookAblServerStartEvent(this); + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + event.setMediaServerItem(mediaServerItem); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + logger.info("[ABL-HOOK-启动] 发送通知失败 ", e); + } + + return HookResult.SUCCESS(); + } + + /** + * TODO 发送rtp(startSendRtp)被动关闭时回调 + */ +// @ResponseBody +// @PostMapping(value = "/on_send_rtp_stopped", produces = "application/json;charset=UTF-8") +// public HookResult onSendRtpStopped(HttpServletRequest request, @RequestBody OnSendRtpStoppedHookParam param) { +// +// logger.info("[ZLM HOOK] rtp发送关闭:{}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream()); +// +// // 查找对应的上级推流,发送停止 +// if (!"rtp".equals(param.getApp())) { +// return HookResult.SUCCESS(); +// } +// try { +// MediaSendRtpStoppedEvent event = new MediaSendRtpStoppedEvent(this); +// MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); +// if (mediaServerItem != null) { +// event.setMediaServer(mediaServerItem); +// applicationEventPublisher.publishEvent(event); +// } +// }catch (Exception e) { +// logger.info("[ZLM-HOOK-rtp发送关闭] 发送通知失败 ", e); +// } +// +// return HookResult.SUCCESS(); +// } + + /** + * TODO 录像完成事件 + */ + @ResponseBody + @PostMapping(value = "/on_record_mp4", produces = "application/json;charset=UTF-8") + public HookResult onRecordMp4(HttpServletRequest request, @RequestBody OnRecordMp4ABLHookParam param) { + logger.info("[ABL HOOK] 录像完成事件:{}->{}", param.getMediaServerId(), param.getFileName()); + + try { + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + MediaRecordMp4Event event = MediaRecordMp4Event.getInstance(this, param, mediaServerItem); + event.setMediaServer(mediaServerItem); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + logger.info("[ZLM-HOOK-rtpServer收流超时] 发送通知失败 ", e); + } + + return HookResult.SUCCESS(); + } + + /** + * 当某一路码流断开时会发送通知 + */ + @ResponseBody + @PostMapping(value = "/on_stream_disconnect", produces = "application/json;charset=UTF-8") + public HookResult onRecordMp4(HttpServletRequest request, @RequestBody ABLHookParam param) { + logger.info("[ABL HOOK] 码流断开事件, {}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream()); + + MediaServer mediaServer = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServer == null) { + return HookResult.SUCCESS(); + } + + MediaDepartureEvent mediaDepartureEvent = MediaDepartureEvent.getInstance(this, param, mediaServer); + applicationEventPublisher.publishEvent(mediaDepartureEvent); + + return HookResult.SUCCESS(); + } + + private Map urlParamToMap(String params) { + HashMap map = new HashMap<>(); + if (ObjectUtils.isEmpty(params)) { + return map; + } + String[] paramsArray = params.split("&"); + if (paramsArray.length == 0) { + return map; + } + for (String param : paramsArray) { + String[] paramArray = param.split("="); + if (paramArray.length == 2) { + map.put(paramArray[0], paramArray[1]); + } + } + return map; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/ABLMediaNodeServerService.java b/src/main/java/com/genersoft/iot/vmp/media/abl/ABLMediaNodeServerService.java new file mode 100644 index 0000000..ab24c23 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/ABLMediaNodeServerService.java @@ -0,0 +1,539 @@ +package com.genersoft.iot.vmp.media.abl; + +import com.alibaba.fastjson2.JSONArray; +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.media.abl.bean.ABLMedia; +import com.genersoft.iot.vmp.media.abl.bean.ABLResult; +import com.genersoft.iot.vmp.media.abl.bean.AblServerConfig; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.media.event.media.MediaRecordMp4Event; +import com.genersoft.iot.vmp.media.service.IMediaNodeServerService; +import com.genersoft.iot.vmp.service.bean.CloudRecordItem; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.storager.dao.CloudRecordServiceMapper; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.springframework.util.ObjectUtils; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service("abl") +public class ABLMediaNodeServerService implements IMediaNodeServerService { + + private final static Logger logger = LoggerFactory.getLogger(ABLMediaNodeServerService.class); + + @Autowired + private ABLRESTfulUtils ablresTfulUtils; + + @Autowired + private SipConfig sipConfig; + + @Autowired + private UserSetting userSetting; + + @Autowired + private CloudRecordServiceMapper cloudRecordServiceMapper; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Override + public boolean initStopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc) { + return false; + } + + @Override + public int createRTPServer(MediaServer mediaServer, String stream, long ssrc, Integer port, Boolean onlyAuto, Boolean disableAudio, Boolean reUsePort, Integer tcpMode) { + Boolean recordSip = userSetting.getRecordSip(); + return ablresTfulUtils.openRtpServer(mediaServer, "rtp", stream, 96, port, tcpMode, disableAudio?1:0, recordSip, false); + } + + @Override + public void closeRtpServer(MediaServer mediaServer, String streamId, CommonCallback callback) { + if (mediaServer == null) { + return; + } + ABLResult result = ablresTfulUtils.closeStreams(mediaServer, "rtp", streamId); + logger.info("关闭RTP Server " + result); + if (result.getCode() != 0) { + logger.error("[closeRtpServer] 失败: {}", result.getMemo()); + } + } + + @Override + public int createJTTServer(MediaServer mediaServer, String stream, Integer port, Boolean disableVideo, Boolean disableAudio, Integer tcpMode) { + Boolean recordSip = userSetting.getRecordSip(); + return ablresTfulUtils.openRtpServer(mediaServer, "1078", stream, 96, port, tcpMode, disableAudio?1:0, recordSip, true); + } + + @Override + public void closeJTTServer(MediaServer mediaServer, String streamId, CommonCallback callback) { + if (mediaServer == null) { + return; + } + ABLResult result = ablresTfulUtils.closeStreams(mediaServer, "1078", streamId); + logger.info("关闭JT-RTP Server " + result); + if (result.getCode() != 0) { + logger.error("[JT-closeRtpServer] 失败: {}", result.getMemo()); + } + } + + @Override + public void closeStreams(MediaServer mediaServer, String app, String streamId) { + ABLResult result = ablresTfulUtils.closeStreams(mediaServer, app, streamId); + if (result.getCode() != 0) { + logger.error("[closeStreams] 失败: {}", result.getMemo()); + } + } + + @Override + public Boolean updateRtpServerSSRC(MediaServer mediaServerItem, String streamId, String ssrc) { + return null; + } + + @Override + public boolean checkNodeId(MediaServer mediaServerItem) { + logger.warn("[abl-checkNodeId] 未实现"); + return false; + } + + @Override + public void online(MediaServer mediaServerItem) { + logger.warn("[abl-online] 未实现"); + } + + @Override + public MediaServer checkMediaServer(String ip, int port, String secret) { + MediaServer mediaServer = new MediaServer(); + mediaServer.setIp(ip); + mediaServer.setHttpPort(port); + mediaServer.setSecret(secret); + ABLResult result = ablresTfulUtils.getServerConfig(mediaServer); + JSONArray data = result.getParams(); + if (data != null && !data.isEmpty()) { + AblServerConfig config = AblServerConfig.getInstance(data); + config.setServerIp(ip); + config.setHttpServerPort(port); + return new MediaServer(config, sipConfig.getIp()); + } + return null; + } + + @Override + public boolean stopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc) { + // TODO 需要记录开始发流返回的KEY,暂不做实现 + logger.warn("[abl-stopSendRtp] 未实现"); +// ablresTfulUtils.stopSendRtp() + return false; + } + + @Override + public boolean deleteRecordDirectory(MediaServer mediaServerItem, String app, String stream, String date, String fileName) { + logger.warn("[abl-deleteRecordDirectory] 未实现"); + return false; + } + + @Override + public List getMediaList(MediaServer mediaServer, String app, String stream, String callId) { + ABLResult result = ablresTfulUtils.getMediaList(mediaServer, app, stream); + if (result.getCode() != 0) { + return null; + } + if (result.getMediaList() == null || result.getMediaList().isEmpty()) { + return new ArrayList<>(); + } + List streamInfoList = new ArrayList<>(); + for (int i = 0; i < result.getMediaList().size(); i++) { + ABLMedia ablMedia = result.getMediaList().get(i); + MediaInfo mediaInfo = MediaInfo.getInstance(ablMedia, mediaServer); + StreamInfo streamInfo = getStreamInfoByAppAndStream(mediaServer, app, stream, mediaInfo, null, callId, true); + if (streamInfo != null) { + streamInfoList.add(streamInfo); + } + } + return streamInfoList; + } + + @Override + public StreamInfo getStreamInfoByAppAndStream(MediaServer mediaServer, String app, String stream, MediaInfo mediaInfo, + String addr, String callId, boolean isPlay) { + StreamInfo streamInfoResult = new StreamInfo(); + streamInfoResult.setStream(stream); + streamInfoResult.setApp(app); + if (addr == null) { + addr = mediaServer.getStreamIp(); + } + + streamInfoResult.setIp(addr); + if (mediaInfo != null) { + streamInfoResult.setServerId(mediaInfo.getServerId()); + }else { + streamInfoResult.setServerId(userSetting.getServerId()); + } + + streamInfoResult.setMediaServer(mediaServer); + Map param = new HashMap<>(); + if (!ObjectUtils.isEmpty(callId)) { + param.put("callId", callId); + } + if (mediaInfo != null && !ObjectUtils.isEmpty(mediaInfo.getOriginTypeStr())) { + param.put("originTypeStr", mediaInfo.getOriginTypeStr()); + } + StringBuilder callIdParamBuilder = new StringBuilder(); + if (!param.isEmpty()) { + callIdParamBuilder.append("?"); + for (Map.Entry entry : param.entrySet()) { + callIdParamBuilder.append(entry.getKey()).append("=").append(entry.getValue()); + callIdParamBuilder.append("&"); + } + callIdParamBuilder.deleteCharAt(callIdParamBuilder.length() - 1); + } + + String callIdParam = callIdParamBuilder.toString(); + + streamInfoResult.setRtmp(addr, mediaServer.getRtmpPort(),mediaServer.getRtmpSSlPort(), app, stream, callIdParam); + streamInfoResult.setRtsp(addr, mediaServer.getRtspPort(),mediaServer.getRtspSSLPort(), app, stream, callIdParam); + + String flvFile = String.format("%s/%s.flv%s", app, stream, callIdParam); + if ((mediaServer.getFlvPort() & 1) == 1) { + // 奇数端口 默认ssl端口 + streamInfoResult.setFlv(addr, null, mediaServer.getFlvPort(), flvFile); + }else { + streamInfoResult.setFlv(addr, mediaServer.getFlvPort(),null, flvFile); + } + if ((mediaServer.getWsFlvPort() & 1) == 1) { + // 奇数端口 默认ssl端口 + streamInfoResult.setWsFlv(addr, null, mediaServer.getWsFlvPort(), flvFile); + }else { + streamInfoResult.setWsFlv(addr, mediaServer.getWsFlvPort(),null, flvFile); + } + String mp4File = String.format("%s/%s.mp4%s", app, stream, callIdParam); + if ((mediaServer.getMp4Port() & 1) == 1) { + // 奇数端口 默认ssl端口 + streamInfoResult.setFmp4(addr, null, mediaServer.getMp4Port(), mp4File); + }else { + streamInfoResult.setFmp4(addr, mediaServer.getMp4Port(), null, mp4File); + } + + streamInfoResult.setHls(addr, mediaServer.getHttpPort(), mediaServer.getHttpSSlPort(), app, stream, callIdParam); + streamInfoResult.setTs(addr, mediaServer.getHttpPort(), mediaServer.getHttpSSlPort(), app, stream, callIdParam); + streamInfoResult.setRtc(addr, mediaServer.getHttpPort(), mediaServer.getHttpSSlPort(), app, stream, callIdParam, isPlay); + + streamInfoResult.setMediaInfo(mediaInfo); + + if (!"broadcast".equalsIgnoreCase(app) && !ObjectUtils.isEmpty(mediaServer.getTranscodeSuffix()) && !"null".equalsIgnoreCase(mediaServer.getTranscodeSuffix())) { + String newStream = stream + "_" + mediaServer.getTranscodeSuffix(); + mediaServer.setTranscodeSuffix(null); + StreamInfo transcodeStreamInfo = getStreamInfoByAppAndStream(mediaServer, app, newStream, null, addr, callId, isPlay); + streamInfoResult.setTranscodeStream(transcodeStreamInfo); + } + return streamInfoResult; + } + + @Override + public Boolean connectRtpServer(MediaServer mediaServerItem, String address, int port, String stream) { + logger.warn("[abl-connectRtpServer] 未实现"); + return null; + } + + @Override + public void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName) { + ablresTfulUtils.getSnap(mediaServer, app, stream, timeoutSec, path, fileName); + } + + @Override + public MediaInfo getMediaInfo(MediaServer mediaServer, String app, String stream) { + ABLResult ablResult = ablresTfulUtils.getMediaList(mediaServer, app, stream); + if (ablResult.getCode() != 0) { + return null; + } + if (ablResult.getMediaList() == null || ablResult.getMediaList().isEmpty()) { + return null; + } + return MediaInfo.getInstance(ablResult.getMediaList().get(0), mediaServer); + } + + @Override + public Boolean pauseRtpCheck(MediaServer mediaServer, String streamKey) { + ABLResult ablResult = ablresTfulUtils.pauseRtpServer(mediaServer, streamKey); + return ablResult.getCode() == 0; + } + + @Override + public Boolean resumeRtpCheck(MediaServer mediaServer, String streamKey) { + ABLResult ablResult = ablresTfulUtils.resumeRtpServer(mediaServer, streamKey); + return ablResult.getCode() == 0; + } + + @Override + public String getFfmpegCmd(MediaServer mediaServer, String cmdKey) { + return ""; + } + + @Override + public Boolean delFFmpegSource(MediaServer mediaServer, String streamKey) { + ABLResult ablResult = ablresTfulUtils.delFFmpegProxy(mediaServer, streamKey); + return ablResult.getCode() == 0; + } + + @Override + public Boolean delStreamProxy(MediaServer mediaServer, String streamKey) { + ABLResult ablResult = ablresTfulUtils.delStreamProxy(mediaServer, streamKey); + return ablResult.getCode() == 0; + } + + @Override + public Map getFFmpegCMDs(MediaServer mediaServer) { + return new HashMap<>(); + } + + // 接受进度通知 +// @EventListener +// public void onApplicationEvent(MediaRecordProcessEvent event) { +// CloudRecordItem cloudRecordItem = cloudRecordServiceMapper.getListByFileName(event.getApp(), event.getStream(), event.getFileName()); +// if (cloudRecordItem == null) { +// cloudRecordItem = CloudRecordItem.getInstance(event); +// cloudRecordItem.setStartTime(event.getStartTime()); +// cloudRecordItem.setEndTime(event.getEndTime()); +// cloudRecordServiceMapper.add(cloudRecordItem); +// }else { +// cloudRecordServiceMapper.updateTimeLen(cloudRecordItem.getId(), (long)event.getCurrentFileDuration() * 1000, System.currentTimeMillis()); +// } +// } + @EventListener + public void onApplicationEvent(MediaRecordMp4Event event) { + InviteInfo inviteInfo = inviteStreamService.getInviteInfo(InviteSessionType.DOWNLOAD, null, event.getStream()); + if (inviteInfo == null || inviteInfo.getStreamInfo() == null) { + return; + } + List cloudRecordItemList = cloudRecordServiceMapper.getList(null, event.getApp(), event.getStream(), + null, null, null, null, null, null); + if (cloudRecordItemList.isEmpty()) { + return; + } + long startTime = cloudRecordItemList.get(cloudRecordItemList.size() - 1).getStartTime(); + long endTime = cloudRecordItemList.get(0).getEndTime(); + ABLResult ablResult = ablresTfulUtils.queryRecordList(event.getMediaServer(), event.getApp(), event.getStream(), DateUtil.timestampMsToUrlToyyyy_MM_dd_HH_mm_ss(startTime), + DateUtil.timestampMsToUrlToyyyy_MM_dd_HH_mm_ss(endTime)); + if (ablResult.getCode() != 0) { + return; + } + if (ablResult.getUrl() == null) { + return; + } + String download = ablResult.getUrl().getDownload(); + DownloadFileInfo downloadFileInfo = new DownloadFileInfo(); + downloadFileInfo.setHttpPath(download); + downloadFileInfo.setHttpsPath(download); + inviteInfo.getStreamInfo().setDownLoadFilePath(downloadFileInfo); + inviteStreamService.updateInviteInfo(inviteInfo); + } + + @Override + public Long updateDownloadProcess(MediaServer mediaServer, String app, String stream) { + List list = cloudRecordServiceMapper.getList(null, app, stream, null, + null, null, null, null, null); + if (list.isEmpty()) { + return null; + } + long downloadProcess = 0L; + for (CloudRecordItem cloudRecordItem : list) { + downloadProcess += (long) cloudRecordItem.getTimeLen(); + } + return downloadProcess; + } + + @Override + public WVPResult addStreamProxy(MediaServer mediaServer, String app, String stream, String url, boolean enableAudio, boolean enableMp4, String rtpType, Integer timeout) { + + ABLResult result = ablresTfulUtils.addStreamProxy(mediaServer, app, stream, url, !enableAudio, enableMp4, rtpType, timeout); + if (result.getCode() != 0) { + return WVPResult.fail(ErrorCode.ERROR100.getCode(), result.getMemo()); + }else { + return WVPResult.success(result.getKey()); + } + } + + @Override + public Integer startSendRtpPassive(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout) { + logger.warn("[abl-startSendRtpPassive] 未实现"); + return 0; + } + + @Override + public Integer startSendRtpTalk(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout) { + logger.warn("[abl-startSendRtpTalk] 未实现"); + return 0; + } + + @Override + public void startSendRtpStream(MediaServer mediaServer, SendRtpInfo sendRtpItem) { + logger.warn("[abl-startSendRtpStream] 未实现"); + } + + @Override + public String startProxy(MediaServer mediaServer, StreamProxy streamProxy) { + + MediaInfo mediaInfo = getMediaInfo(mediaServer, streamProxy.getApp(), streamProxy.getStream()); + + if (mediaInfo != null) { + closeStreams(mediaServer, streamProxy.getApp(), streamProxy.getStream()); + } + + ABLResult ablResult = null; + if ("ffmpeg".equalsIgnoreCase(streamProxy.getType())){ + if (streamProxy.getTimeout() == 0) { + streamProxy.setTimeout(15); + } + ablResult = ablresTfulUtils.addFFmpegProxy(mediaServer, streamProxy.getApp(), streamProxy.getStream(), streamProxy.getSrcUrl().trim(), + !streamProxy.isEnableAudio(), streamProxy.isEnableMp4(), streamProxy.getRtspType(), streamProxy.getTimeout()); + }else { + ablResult = ablresTfulUtils.addStreamProxy(mediaServer, streamProxy.getApp(), streamProxy.getStream(), streamProxy.getSrcUrl().trim(), + streamProxy.isEnableAudio(), streamProxy.isEnableMp4(), streamProxy.getRtspType(), streamProxy.getTimeout()); + } + if (ablResult.getCode() != 0) { + throw new ControllerException(ablResult.getCode(), ablResult.getMemo()); + }else { + String key = ablResult.getKey(); + if (key == null) { + throw new ControllerException(ablResult.getCode(), "代理结果异常: " + ablResult); + }else { + return key; + } + } + } + + @Override + public void stopProxy(MediaServer mediaServer, String streamKey, String type) { + ABLResult ablResult = null; + if ("ffmpeg".equalsIgnoreCase(type)){ + ablResult = ablresTfulUtils.delFFmpegProxy(mediaServer, streamKey); + }else { + ablResult = ablresTfulUtils.delStreamProxy(mediaServer, streamKey); + } + if (ablResult.getCode() != 0) { + throw new ControllerException(ablResult.getCode(), ablResult.getMemo()); + } + } + + @Override + public List listRtpServer(MediaServer mediaServer) { + ABLResult ablResult = ablresTfulUtils.getMediaList(mediaServer, "rtp", null); + if (ablResult.getCode() != 0) { + return null; + } + if (ablResult.getMediaList() == null || ablResult.getMediaList().isEmpty()) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + for (int i = 0; i < ablResult.getMediaList().size(); i++) { + ABLMedia ablMedia = ablResult.getMediaList().get(i); + result.add(ablMedia.getStream()); + } + return result; + } + + @Override + public void loadMP4File(MediaServer mediaServer, String app, String stream, String filePath, String fileName, ErrorCallback callback) { + String buildStream = String.format("%s__ReplayFMP4RecordFile__%s", stream, fileName); + StreamInfo streamInfo = getStreamInfoByAppAndStream(mediaServer, app, buildStream, null, null, null, true); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo); + } + + @Override + public void loadMP4FileForDate(MediaServer mediaServer, String app, String stream, String date, String dateDir, ErrorCallback callback) { + // 解析为 LocalDate + LocalDate localDate = LocalDate.parse(date, DateUtil.DateFormatter); + LocalDateTime startOfDay = localDate.atStartOfDay(); + LocalDateTime endOfDay = localDate.atTime(23, 59,59, 999); + String startTime = DateUtil.urlFormatter.format(startOfDay); + String endTime = DateUtil.urlFormatter.format(endOfDay); + + ABLResult ablResult = ablresTfulUtils.queryRecordList(mediaServer, app, stream, startTime, endTime); + if (ablResult.getCode() != 0) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), ablResult.getMemo()); + } + String resultApp = ablResult.getApp(); + String resultStream = ablResult.getStream(); + StreamInfo streamInfo = getStreamInfoByAppAndStream(mediaServer, resultApp, resultStream, null, null,null, true); + streamInfo.setKey(ablResult.getKey()); + if (callback != null) { + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo); + } + } + + @Override + public void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema) { + ABLResult ablResult = ablresTfulUtils.controlRecordPlay(mediaServer, app, stream, "seek", stamp/1000 + ""); + if (ablResult.getCode() != 0) { + log.warn("[abl-seek] 失败:{}", ablResult.getMemo()); + } + } + + @Override + public void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema) { + ABLResult ablResult = ablresTfulUtils.controlRecordPlay(mediaServer, app, stream, "scale", speed + ""); + if (ablResult.getCode() != 0) { + log.warn("[abl-倍速] 失败:{}", ablResult.getMemo()); + } + } + + @Override + public DownloadFileInfo getDownloadFilePath(MediaServer mediaServer, RecordInfo recordInfo) { + // 将filePath作为独立参数传入,避免%符号解析问题 + String pathTemplate = "%s://%s:%s/%s/%s__ReplayFMP4RecordFile__%s?download_speed=16"; + + DownloadFileInfo info = new DownloadFileInfo(); + if ((mediaServer.getMp4Port() & 1) == 1) { + info.setHttpsPath( + String.format( + pathTemplate, + "https", + mediaServer.getStreamIp(), + mediaServer.getMp4Port(), + recordInfo.getApp(), + recordInfo.getStream(), + recordInfo.getFileName() + ) + ); + }else { + info.setHttpPath( + String.format( + pathTemplate, + "http", + mediaServer.getStreamIp(), + mediaServer.getMp4Port(), + recordInfo.getApp(), + recordInfo.getStream(), + recordInfo.getFileName() + ) + ); + } + return info; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/ABLMediaServerStatusManger.java b/src/main/java/com/genersoft/iot/vmp/media/abl/ABLMediaServerStatusManger.java new file mode 100644 index 0000000..a820851 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/ABLMediaServerStatusManger.java @@ -0,0 +1,358 @@ +package com.genersoft.iot.vmp.media.abl; + +import com.alibaba.fastjson2.JSONArray; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.media.abl.bean.ABLResult; +import com.genersoft.iot.vmp.media.abl.bean.AblServerConfig; +import com.genersoft.iot.vmp.media.abl.bean.ConfigKeyId; +import com.genersoft.iot.vmp.media.abl.event.HookAblServerKeepaliveEvent; +import com.genersoft.iot.vmp.media.abl.event.HookAblServerStartEvent; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerChangeEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerDeleteEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 管理zlm流媒体节点的状态 + */ +@Component +public class ABLMediaServerStatusManger { + + private final static Logger logger = LoggerFactory.getLogger(ABLMediaServerStatusManger.class); + + private final Map offlineABLPrimaryMap = new ConcurrentHashMap<>(); + private final Map offlineAblsecondaryMap = new ConcurrentHashMap<>(); + private final Map offlineAblTimeMap = new ConcurrentHashMap<>(); + + @Autowired + private ABLRESTfulUtils ablResTfulUtils; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private DynamicTask dynamicTask; + + @Value("${server.ssl.enabled:false}") + private boolean sslEnabled; + + @Value("${server.port}") + private Integer serverPort; + + @Autowired + private UserSetting userSetting; + + private final String type = "abl"; + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaServerChangeEvent event) { + if (event.getMediaServerItemList() == null + || event.getMediaServerItemList().isEmpty()) { + return; + } + for (MediaServer mediaServer : event.getMediaServerItemList()) { + if (!type.equals(mediaServer.getType())) { + continue; + } + logger.info("[ABL-添加待上线节点] ID:" + mediaServer.getId()); + offlineABLPrimaryMap.put(mediaServer.getId(), mediaServer); + offlineAblTimeMap.put(mediaServer.getId(), System.currentTimeMillis()); + } + execute(); + } + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(HookAblServerStartEvent event) { + if (event.getMediaServerItem() == null + || !type.equals(event.getMediaServerItem().getType()) + || event.getMediaServerItem().isStatus()) { + return; + } + MediaServer serverItem = mediaServerService.getOne(event.getMediaServerItem().getId()); + if (serverItem == null) { + return; + } + logger.info("[ABL-HOOK事件-服务启动] ID:" + event.getMediaServerItem().getId()); + online(serverItem, null); + } + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(HookAblServerKeepaliveEvent event) { + if (event.getMediaServerItem() == null) { + return; + } + MediaServer serverItem = mediaServerService.getOne(event.getMediaServerItem().getId()); + if (serverItem == null) { + return; + } + logger.info("[ABL-HOOK事件-心跳] ID:" + event.getMediaServerItem().getId()); + online(serverItem, null); + } + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaServerDeleteEvent event) { + if (event.getMediaServer() == null) { + return; + } + logger.info("[ABL-节点被移除] ID:" + event.getMediaServer().getServerId()); + offlineABLPrimaryMap.remove(event.getMediaServer().getServerId()); + offlineAblsecondaryMap.remove(event.getMediaServer().getServerId()); + offlineAblTimeMap.remove(event.getMediaServer().getServerId()); + } + + @Scheduled(fixedDelay = 10*1000) //每隔10秒检查一次 + public void execute(){ + // 初次加入的离线节点会在30分钟内,每间隔十秒尝试一次,30分钟后如果仍然没有上线,则每隔30分钟尝试一次连接 + if (offlineABLPrimaryMap.isEmpty() && offlineAblsecondaryMap.isEmpty()) { + return; + } + if (!offlineABLPrimaryMap.isEmpty()) { + for (MediaServer mediaServerItem : offlineABLPrimaryMap.values()) { + if (offlineAblTimeMap.get(mediaServerItem.getId()) < System.currentTimeMillis() - 30*60*1000) { + offlineAblsecondaryMap.put(mediaServerItem.getId(), mediaServerItem); + offlineABLPrimaryMap.remove(mediaServerItem.getId()); + continue; + } + logger.info("[ABL-尝试连接] ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + ABLResult ablResult = ablResTfulUtils.getServerConfig(mediaServerItem); + AblServerConfig ablServerConfig = null; + if (ablResult.getCode() != 0) { + logger.info("[ABL-尝试连接]失败, ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + continue; + } + JSONArray params = ablResult.getParams(); + + if (params == null || params.isEmpty()) { + logger.info("[ABL-尝试连接]失败, ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + }else { + ablServerConfig = AblServerConfig.getInstance(params); + initPort(mediaServerItem, ablServerConfig); + online(mediaServerItem, ablServerConfig); + } + } + } + if (!offlineAblsecondaryMap.isEmpty()) { + for (MediaServer mediaServerItem : offlineAblsecondaryMap.values()) { + if (offlineAblTimeMap.get(mediaServerItem.getId()) < System.currentTimeMillis() - 30*60*1000) { + continue; + } + logger.info("[ABL-尝试连接] ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + ABLResult ablResult = ablResTfulUtils.getServerConfig(mediaServerItem); + AblServerConfig ablServerConfig = null; + if (ablResult.getCode() != 0) { + logger.info("[ABL-尝试连接]失败, ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + offlineAblTimeMap.put(mediaServerItem.getId(), System.currentTimeMillis()); + continue; + } + JSONArray params = ablResult.getParams(); + if (params == null || params.isEmpty()) { + logger.info("[ABL-尝试连接]失败, ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + offlineAblTimeMap.put(mediaServerItem.getId(), System.currentTimeMillis()); + }else { + ablServerConfig = AblServerConfig.getInstance(params); + initPort(mediaServerItem, ablServerConfig); + online(mediaServerItem, ablServerConfig); + } + } + } + } + + private void online(MediaServer mediaServer, AblServerConfig config) { + offlineABLPrimaryMap.remove(mediaServer.getId()); + offlineAblsecondaryMap.remove(mediaServer.getId()); + offlineAblTimeMap.remove(mediaServer.getId()); + if (!mediaServer.isStatus()) { + logger.info("[ABL-连接成功] ID:{}, 地址: {}:{}", mediaServer.getId(), mediaServer.getIp(), mediaServer.getHttpPort()); + mediaServer.setStatus(true); + mediaServer.setHookAliveInterval(10F); + mediaServerService.update(mediaServer); + if(mediaServer.isAutoConfig()) { + if (config == null) { + ABLResult ablResult = ablResTfulUtils.getServerConfig(mediaServer); + JSONArray data = ablResult.getParams(); + if (data != null && !data.isEmpty()) { + config = AblServerConfig.getInstance(data); + } + } + if (config != null) { + initPort(mediaServer, config); + setAblConfig(mediaServer, false, config); + } + } + mediaServerService.update(mediaServer); + } + // 设置两次心跳未收到则认为zlm离线 + String key = "ABL-keepalive-" + mediaServer.getId(); + dynamicTask.startDelay(key, ()->{ + logger.warn("[ABL-心跳超时] ID:{}", mediaServer.getId()); + mediaServer.setStatus(false); + offlineABLPrimaryMap.put(mediaServer.getId(), mediaServer); + offlineAblTimeMap.put(mediaServer.getId(), System.currentTimeMillis()); + // TODO 发送离线通知 + mediaServerService.update(mediaServer); + }, (int)(mediaServer.getHookAliveInterval() * 2 * 1000)); + } + private void initPort(MediaServer mediaServer, AblServerConfig ablServerConfig) { + // 端口只会从配置中读取一次,一旦自己配置或者读取过了将不在配置 + if (ablServerConfig.getRtmpPort() != null && mediaServer.getRtmpPort() != ablServerConfig.getRtmpPort()) { + mediaServer.setRtmpPort(ablServerConfig.getRtmpPort()); + } + if (ablServerConfig.getRtspPort() != null && mediaServer.getRtspPort() != ablServerConfig.getRtspPort()) { + mediaServer.setRtspPort(ablServerConfig.getRtspPort()); + } + if (ablServerConfig.getHttpFlvPort() != null && mediaServer.getFlvPort() != ablServerConfig.getHttpFlvPort()) { + mediaServer.setFlvPort(ablServerConfig.getHttpFlvPort()); + } + if (ablServerConfig.getHttpMp4Port() != null && mediaServer.getMp4Port() != ablServerConfig.getHttpMp4Port()) { + mediaServer.setMp4Port(ablServerConfig.getHttpMp4Port()); + } + if (ablServerConfig.getWsFlvPort() != null && mediaServer.getWsFlvPort() != ablServerConfig.getWsFlvPort()) { + mediaServer.setWsFlvPort(ablServerConfig.getWsFlvPort()); + } + if (ablServerConfig.getPsTsRecvPort() != null && mediaServer.getRtpProxyPort() != ablServerConfig.getPsTsRecvPort()) { + mediaServer.setRtpProxyPort(ablServerConfig.getPsTsRecvPort()); + } + if (ablServerConfig.getJtt1078RecvPort() != null && mediaServer.getRtpProxyPort() != ablServerConfig.getJtt1078RecvPort()) { + mediaServer.setJttProxyPort(ablServerConfig.getJtt1078RecvPort()); + } + mediaServer.setHookAliveInterval(10F); + } + + public void setAblConfig(MediaServer mediaServerItem, boolean restart, AblServerConfig config) { + try { + if (config.getHookEnable() == 0) { + logger.info("[媒体服务节点-ABL] 开启HOOK功能 :{}", mediaServerItem.getId()); + ABLResult ablResult = ablResTfulUtils.setConfigParamValue(mediaServerItem, "hook_enable", "1"); + if (ablResult.getCode() == 0) { + logger.info("[媒体服务节点-ABL] 开启HOOK功能成功 :{}", mediaServerItem.getId()); + }else { + logger.info("[媒体服务节点-ABL] 开启HOOK功能失败 :{}->{}", mediaServerItem.getId(), ablResult.getMemo()); + } + } + }catch (Exception e) { + logger.info("[媒体服务节点-ABL] 开启HOOK功能失败 :{}", mediaServerItem.getId(), e); + } + // 设置相关的HOOK + String[] hookUrlArray = { + "on_stream_arrive", + "on_stream_none_reader", + "on_record_mp4", + "on_stream_disconnect", + "on_stream_not_found", + "on_server_started", + "on_publish", + "on_play", + "on_record_progress", + "on_server_keepalive", + "on_stream_not_arrive", + "on_delete_record_mp4", + }; + + String protocol = sslEnabled ? "https" : "http"; + String hookPrefix = String.format("%s://%s:%s/index/hook/abl", protocol, mediaServerItem.getHookIp(), serverPort); + Field[] fields = AblServerConfig.class.getDeclaredFields(); + for (Field field : fields) { + try { + if (field.isAnnotationPresent(ConfigKeyId.class)) { + ConfigKeyId configKeyId = field.getAnnotation(ConfigKeyId.class); + for (String hook : hookUrlArray) { + if (configKeyId.value().equals(hook)) { + String hookUrl = String.format("%s/%s", hookPrefix, hook); + field.setAccessible(true); + // 利用反射获取值后对比是否与配置中相同,不同则进行设置 + if (!hookUrl.equals(field.get(config))) { + ABLResult ablResult = ablResTfulUtils.setConfigParamValue(mediaServerItem, hook, hookUrl); + if (ablResult.getCode() == 0) { + logger.info("[媒体服务节点-ABL] 设置HOOK {} 成功 :{}", hook, mediaServerItem.getId()); + }else { + logger.info("[媒体服务节点-ABL] 设置HOOK {} 失败 :{}->{}", hook, mediaServerItem.getId(), ablResult.getMemo()); + } + } + } + } + } + }catch (Exception e) { + logger.info("[媒体服务节点-ABL] 设置HOOK 失败 :{}", mediaServerItem.getId(), e); + } + } + + + + +// Map param = new HashMap<>(); +// param.put("api.secret",mediaServerItem.getSecret()); // -profile:v Baseline +// if (mediaServerItem.getRtspPort() != 0) { +// param.put("ffmpeg.snap", "%s -rtsp_transport tcp -i %s -y -f mjpeg -frames:v 1 %s"); +// } +// param.put("hook.enable","1"); +// param.put("hook.on_flow_report",""); +// param.put("hook.on_play",String.format("%s/on_play", hookPrefix)); +// param.put("hook.on_http_access",""); +// param.put("hook.on_publish", String.format("%s/on_publish", hookPrefix)); +// param.put("hook.on_record_ts",""); +// param.put("hook.on_rtsp_auth",""); +// param.put("hook.on_rtsp_realm",""); +// param.put("hook.on_server_started",String.format("%s/on_server_started", hookPrefix)); +// param.put("hook.on_shell_login",""); +// param.put("hook.on_stream_changed",String.format("%s/on_stream_changed", hookPrefix)); +// param.put("hook.on_stream_none_reader",String.format("%s/on_stream_none_reader", hookPrefix)); +// param.put("hook.on_stream_not_found",String.format("%s/on_stream_not_found", hookPrefix)); +// param.put("hook.on_server_keepalive",String.format("%s/on_server_keepalive", hookPrefix)); +// param.put("hook.on_send_rtp_stopped",String.format("%s/on_send_rtp_stopped", hookPrefix)); +// param.put("hook.on_rtp_server_timeout",String.format("%s/on_rtp_server_timeout", hookPrefix)); +// param.put("hook.on_record_mp4",String.format("%s/on_record_mp4", hookPrefix)); +// param.put("hook.timeoutSec","30"); +// param.put("hook.alive_interval", mediaServerItem.getHookAliveInterval()); +// // 推流断开后可以在超时时间内重新连接上继续推流,这样播放器会接着播放。 +// // 置0关闭此特性(推流断开会导致立即断开播放器) +// // 此参数不应大于播放器超时时间 +// // 优化此消息以更快的收到流注销事件 +// param.put("protocol.continue_push_ms", "3000" ); +// // 最多等待未初始化的Track时间,单位毫秒,超时之后会忽略未初始化的Track, 设置此选项优化那些音频错误的不规范流, +// // 等zlm支持给每个rtpServer设置关闭音频的时候可以不设置此选项 +// if (mediaServerItem.isRtpEnable() && !ObjectUtils.isEmpty(mediaServerItem.getRtpPortRange())) { +// param.put("rtp_proxy.port_range", mediaServerItem.getRtpPortRange().replace(",", "-")); +// } +// +// if (!ObjectUtils.isEmpty(mediaServerItem.getRecordPath())) { +// File recordPathFile = new File(mediaServerItem.getRecordPath()); +// param.put("protocol.mp4_save_path", recordPathFile.getParentFile().getPath()); +// param.put("protocol.downloadRoot", recordPathFile.getParentFile().getPath()); +// param.put("record.appName", recordPathFile.getName()); +// } +// +// JSONObject responseJSON = ablResTfulUtils.setConfigParamValue(mediaServerItem, param); +// +// if (responseJSON != null && responseJSON.getInteger("code") == 0) { +// if (restart) { +// logger.info("[媒体服务节点] 设置成功,开始重启以保证配置生效 {} -> {}:{}", +// mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); +// ablResTfulUtils.restartServer(mediaServerItem); +// }else { +// logger.info("[媒体服务节点] 设置成功 {} -> {}:{}", +// mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); +// } +// }else { +// logger.info("[媒体服务节点] 设置媒体服务节点失败 {} -> {}:{}", +// mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); +// } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/ABLRESTfulUtils.java b/src/main/java/com/genersoft/iot/vmp/media/abl/ABLRESTfulUtils.java new file mode 100644 index 0000000..9c019bd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/ABLRESTfulUtils.java @@ -0,0 +1,540 @@ +package com.genersoft.iot.vmp.media.abl; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.media.abl.bean.ABLResult; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +@Component +public class ABLRESTfulUtils { + + private final static Logger logger = LoggerFactory.getLogger(ABLRESTfulUtils.class); + + private OkHttpClient client; + + public interface RequestCallback{ + void run(String response); + } + public interface ResultCallback{ + void run(ABLResult response); + } + + private OkHttpClient getClient(){ + return getClient(null); + } + + private OkHttpClient getClient(Integer readTimeOut){ + if (client == null) { + if (readTimeOut == null) { + readTimeOut = 10; + } + OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); + //todo 暂时写死超时时间 均为5s + // 设置连接超时时间 + httpClientBuilder.connectTimeout(8,TimeUnit.SECONDS); + // 设置读取超时时间 + httpClientBuilder.readTimeout(readTimeOut,TimeUnit.SECONDS); + // 设置连接池 + httpClientBuilder.connectionPool(new ConnectionPool(16, 5, TimeUnit.MINUTES)); + client = httpClientBuilder.build(); + } + return client; + + } + + public String sendPost(MediaServer mediaServerItem, String api, Map param, RequestCallback callback) { + return sendPost(mediaServerItem, api, param, callback, null); + } + + + public String sendPost(MediaServer mediaServerItem, String api, Map param, RequestCallback callback, Integer readTimeOut) { + OkHttpClient client = getClient(readTimeOut); + + if (mediaServerItem == null) { + return null; + } + String url = String.format("http://%s:%s/index/api/%s", mediaServerItem.getIp(), mediaServerItem.getHttpPort(), api); + String result = null; + + FormBody.Builder builder = new FormBody.Builder(); + builder.add("secret",mediaServerItem.getSecret()); + if (param != null && !param.isEmpty()) { + for (String key : param.keySet()){ + if (param.get(key) != null) { + builder.add(key, param.get(key).toString()); + } + } + } + + FormBody body = builder.build(); + + Request request = new Request.Builder() + .post(body) + .url(url) + .build(); + if (callback == null) { + try { + Response response = client.newCall(request).execute(); + + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + result = responseBody.string(); + } + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + }catch (IOException e) { + logger.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + + if(e instanceof SocketTimeoutException){ + //读取超时超时异常 + logger.error(String.format("读取ABL数据超时失败: %s, %s", url, e.getMessage())); + } + if(e instanceof ConnectException){ + //判断连接异常,我这里是报Failed to connect to 10.7.5.144 + logger.error(String.format("连接ABL连接失败: %s, %s", url, e.getMessage())); + } + + }catch (Exception e){ + logger.error(String.format("访问ABL失败: %s, %s", url, e.getMessage())); + } + }else { + client.newCall(request).enqueue(new Callback(){ + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response){ + if (response.isSuccessful()) { + try { + String responseStr = Objects.requireNonNull(response.body()).string(); + callback.run(responseStr); + } catch (IOException e) { + logger.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + logger.error(String.format("连接ABL失败: %s, %s", call.request().toString(), e.getMessage())); + + if(e instanceof SocketTimeoutException){ + //读取超时超时异常 + logger.error(String.format("读取ABL数据失败: %s, %s", call.request().toString(), e.getMessage())); + } + if(e instanceof ConnectException){ + //判断连接异常,我这里是报Failed to connect to 10.7.5.144 + logger.error(String.format("连接ABL失败: %s, %s", call.request().toString(), e.getMessage())); + } + } + }); + } + return result; + } + + public String sendGet(MediaServer mediaServerItem, String api, Map param) { + OkHttpClient client = getClient(); + + if (mediaServerItem == null) { + return null; + } + String result = null; + StringBuilder stringBuffer = new StringBuilder(); + stringBuffer.append(String.format("http://%s:%s/index/api/%s", mediaServerItem.getIp(), mediaServerItem.getHttpPort(), api)); + if (param != null && !param.keySet().isEmpty()) { + stringBuffer.append("?secret=").append(mediaServerItem.getSecret()).append("&"); + int index = 1; + for (String key : param.keySet()){ + if (param.get(key) != null) { + stringBuffer.append(key + "=" + param.get(key)); + if (index < param.size()) { + stringBuffer.append("&"); + } + } + index++; + } + } + String url = stringBuffer.toString(); + logger.info("[访问ABL]: {}", url); + Request request = new Request.Builder() + .get() + .url(url) + .build(); + try { + Response response = client.newCall(request).execute(); + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + result = responseBody.string(); + } + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + } catch (ConnectException e) { + logger.error(String.format("连接ABL失败: %s, %s", e.getCause().getMessage(), e.getMessage())); + logger.info("请检查media配置并确认ABL已启动..."); + }catch (IOException e) { + logger.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + return result; + } + + public void sendGetForImg(MediaServer mediaServerItem, String api, Map params, String targetPath, String fileName) { + String url = String.format("http://%s:%s/index/api/%s", mediaServerItem.getIp(), mediaServerItem.getHttpPort(), api); + HttpUrl parseUrl = HttpUrl.parse(url); + if (parseUrl == null) { + return; + } + HttpUrl.Builder httpBuilder = parseUrl.newBuilder(); + + httpBuilder.addQueryParameter("secret", mediaServerItem.getSecret()); + if (params != null) { + for (Map.Entry param : params.entrySet()) { + httpBuilder.addQueryParameter(param.getKey(), param.getValue().toString()); + } + } + + Request request = new Request.Builder() + .url(httpBuilder.build()) + .build(); + logger.info(request.toString()); + try { + OkHttpClient client = getClient(); + Response response = client.newCall(request).execute(); + if (response.isSuccessful()) { + if (targetPath != null) { + File snapFolder = new File(targetPath); + if (!snapFolder.exists()) { + if (!snapFolder.mkdirs()) { + logger.warn("{}路径创建失败", snapFolder.getAbsolutePath()); + } + } + File snapFile = new File(targetPath + File.separator + fileName); + FileOutputStream outStream = new FileOutputStream(snapFile); + + outStream.write(Objects.requireNonNull(response.body()).bytes()); + outStream.flush(); + outStream.close(); + } else { + logger.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message())); + } + } else { + logger.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message())); + } + Objects.requireNonNull(response.body()).close(); + } catch (ConnectException e) { + logger.error(String.format("连接ABL失败: %s, %s", e.getCause().getMessage(), e.getMessage())); + logger.info("请检查media配置并确认ABL已启动..."); + } catch (IOException e) { + logger.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + } + + public void sendGetForImgForUrl(String url, String targetPath, String fileName) { + HttpUrl parseUrl = HttpUrl.parse(url); + if (parseUrl == null) { + return; + } + HttpUrl.Builder httpBuilder = parseUrl.newBuilder(); + + Request request = new Request.Builder() + .url(httpBuilder.build()) + .build(); + logger.info(request.toString()); + try { + OkHttpClient client = getClient(); + Response response = client.newCall(request).execute(); + if (response.isSuccessful()) { + if (targetPath != null) { + File snapFolder = new File(targetPath); + if (!snapFolder.exists()) { + if (!snapFolder.mkdirs()) { + logger.warn("{}路径创建失败", snapFolder.getAbsolutePath()); + } + } + File snapFile = new File(targetPath + File.separator + fileName); + FileOutputStream outStream = new FileOutputStream(snapFile); + + outStream.write(Objects.requireNonNull(response.body()).bytes()); + outStream.flush(); + outStream.close(); + } else { + logger.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message())); + } + } else { + logger.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message())); + } + Objects.requireNonNull(response.body()).close(); + } catch (ConnectException e) { + logger.error(String.format("连接ABL失败: %s, %s", e.getCause().getMessage(), e.getMessage())); + logger.info("请检查media配置并确认ABL已启动..."); + } catch (IOException e) { + logger.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + } + + public Integer openRtpServer(MediaServer mediaServer, String app, String stream, int payload, Integer port, Integer tcpMode, Integer disableAudio, Boolean record, Boolean isJtt) { + Map param = new HashMap<>(); + param.put("vhost", "_defaultVhost_"); + param.put("app", app); + param.put("stream_id", stream); + param.put("payload", payload); + if (isJtt) { + // 1 PS 国标gb28181, 默认为1、 + // 2 ES 视频支持 H246\H265,音频只支持G711A、G711U 、AAC + // 3 XHB (一家公司的打包格式) 只支持视频,音频不能加入打包 + // 4 、Jt1078(2016版本)码流接入 + param.put("RtpPayloadDataType", 4); + param.put("jtt1078_version", "2019"); + } + if (port != null) { + param.put("port", port); + }else { + param.put("port", 0); + } + if (tcpMode != null) { + param.put("enable_tcp", tcpMode); + } + if (disableAudio != null) { + param.put("disableAudio", disableAudio); + } + if (record != null && record) { + param.put("enable_mp4", 1); + } + + String response = sendPost(mediaServer, "openRtpServer", param, null); + if (response == null) { + return 0; + }else { + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult.getCode() == 0) { + return ablResult.getPort(); + }else { + return 0; + } + } + } + + public ABLResult closeStreams(MediaServer mediaServerItem, String app, String stream) { + Map param = new HashMap<>(); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + param.put("force", 1); + String response = sendPost(mediaServerItem, "close_streams", param, null); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult getServerConfig(MediaServer mediaServerItem){ + String response = sendPost(mediaServerItem, "getServerConfig", null, null); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult setConfigParamValue(MediaServer mediaServerItem, String key, Object value){ + Map param = new HashMap<>(); + param.put("key", key); + param.put("value", value); + String response = sendGet(mediaServerItem, "setConfigParamValue", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public void stopSendRtp(MediaServer mediaServer,String key) { + Map param = new HashMap<>(); + param.put("key", key); + sendPost(mediaServer,"stopSendRtp", param, null); + } + + public ABLResult getMediaList(MediaServer mediaServer, String app, String stream) { + Map param = new HashMap<>(); + param.put("app", app); + if (stream != null) { + param.put("stream", stream); + } + + String response = sendGet(mediaServer, "getMediaList", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult queryRecordList(MediaServer mediaServer, String app, String stream, String startTime, String endTime) { + Map param = new HashMap<>(); + param.put("app", app); + param.put("stream", stream); + param.put("starttime", startTime); + param.put("endtime", endTime); + String response = sendGet(mediaServer, "queryRecordList", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, String path, String fileName) { + Map param = new HashMap<>(); + param.put("app", app); + param.put("stream", stream); + param.put("timeout_sec", timeoutSec); + param.put("vhost", "_defaultVhost_"); +// JSONObject jsonObject = sendPost(mediaServer, "getSnap", param, null); +// if (jsonObject != null && jsonObject.getInteger("code") == 0) { +// String url = jsonObject.getString("url"); +// sendGetForImgForUrl(url, path, fileName); +// } + sendGetForImg(mediaServer, "getSnap", param, path, fileName); + + } + + public ABLResult addStreamProxy(MediaServer mediaServer, String app, String stream, String url, boolean disableAudio, boolean enableMp4, String rtpType, Integer timeout) { + try { + url = URLEncoder.encode(url, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"url编码失败"); + } + + Map param = new HashMap<>(); + param.put("app", app); + param.put("stream", stream); + param.put("url", url); + param.put("disableAudio", disableAudio? "1" : "0"); + param.put("enable_mp4", enableMp4 ? "1" : "0"); + // TODO rtpType timeout 尚不支持 + String response = sendGet(mediaServer, "addStreamProxy", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult addFFmpegProxy(MediaServer mediaServer, String app, String stream, String url, boolean disableAudio, boolean enableMp4, String rtpType, Integer timeout) { + try { + url = URLEncoder.encode(url, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"url编码失败"); + } + Map param = new HashMap<>(); + param.put("app", app); + param.put("stream", stream); + param.put("url", url); + param.put("disableAudio", disableAudio); + param.put("enable_mp4", enableMp4); + // TODO rtpType timeout 尚不支持 + String response = sendGet(mediaServer, "addFFmpegProxy", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult delStreamProxy(MediaServer mediaServer, String streamKey) { + Map param = new HashMap<>(); + param.put("key", streamKey); + String response = sendGet(mediaServer, "delStreamProxy", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult delFFmpegProxy(MediaServer mediaServer, String streamKey) { + Map param = new HashMap<>(); + param.put("key", streamKey); + String response = sendGet(mediaServer, "delFFmpegProxy", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult pauseRtpServer(MediaServer mediaServer, String streamKey) { + Map param = new HashMap<>(); + param.put("key", streamKey); + String response = sendGet(mediaServer, "pauseRtpServer", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult resumeRtpServer(MediaServer mediaServer, String streamKey) { + Map param = new HashMap<>(); + param.put("key", streamKey); + String response = sendGet(mediaServer, "resumeRtpServer", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult controlRecordPlay(MediaServer mediaServer, String app, String stream, String command, String value) { + Map param = new HashMap<>(); + param.put("app", app); + param.put("stream", stream); + param.put("command", command); + param.put("value", value); + String response = sendGet(mediaServer, "controlRecordPlay", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLMedia.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLMedia.java new file mode 100644 index 0000000..66532c6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLMedia.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.media.abl.bean; + +import lombok.Data; + +@Data +public class ABLMedia { + private String key; + private String app; + private String stream; + private Integer sourceType; + private Long duration; + private String sim; + private Boolean status; + private Boolean enable_hls; + private Boolean transcodingStatus; + private String sourceURL; + private Integer networkType; + private Integer readerCount; + private String videoCodec; + private Integer width; + private Integer height; + private String audioCodec; + private Integer audioChannels; + private Integer audioSampleRate; +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLRecordFile.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLRecordFile.java new file mode 100644 index 0000000..4af189f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLRecordFile.java @@ -0,0 +1,10 @@ +package com.genersoft.iot.vmp.media.abl.bean; + +import lombok.Data; + +@Data +public class ABLRecordFile { + private String file; + private Long duration; + private ABLUrls url; +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLResult.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLResult.java new file mode 100644 index 0000000..523e8b1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLResult.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.media.abl.bean; + +import com.alibaba.fastjson2.JSONArray; +import lombok.Data; + +import java.util.List; + +@Data +public class ABLResult { + private int code; + private String memo; + + + private String key; + private Integer port; + private JSONArray params; + private List mediaList; + + private String app; + private String stream; + private String starttime; + private String endtime; + private Long duration; + private ABLUrls url; + private List recordFileList; + + public static ABLResult getFailForMediaServer() { + ABLResult zlmResult = new ABLResult(); + zlmResult.setCode(-2); + zlmResult.setMemo("流媒体调用失败"); + return zlmResult; + } + + public static ABLResult getMediaServer(int code, String msg) { + ABLResult zlmResult = new ABLResult(); + zlmResult.setCode(code); + zlmResult.setMemo(msg); + return zlmResult; + } + @Override + public String toString() { + return "ZLMResult{" + + "code=" + code + + ", memo='" + memo + '\'' + + (key != null ? (", key=" + key) : "") + + (port != null ? (", port=" + port) : "") + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLUrls.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLUrls.java new file mode 100644 index 0000000..a3b74c8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLUrls.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.media.abl.bean; + +import com.alibaba.fastjson2.annotation.JSONField; +import lombok.Data; + +@Data +public class ABLUrls { + private String rtsp; + private String rtmp; + + @JSONField(name = "http-flv") + private String httpFlv; + + @JSONField(name = "ws-flv") + private String wsFlv; + + @JSONField(name = "http-mp4") + private String httpMp4; + + @JSONField(name = "http-hls") + private String httpHls; + + private String download; +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/AblServerConfig.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/AblServerConfig.java new file mode 100644 index 0000000..af36faf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/AblServerConfig.java @@ -0,0 +1,257 @@ +package com.genersoft.iot.vmp.media.abl.bean; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import lombok.Data; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +@Data +public class AblServerConfig { + + @ConfigKeyId("secret") + private String secret; + + @ConfigKeyId("ServerIP") + private String serverIp; + + @ConfigKeyId("mediaServerID") + private String mediaServerId; + + @ConfigKeyId("hook_enable") + private Integer hookEnable; + + @ConfigKeyId("enable_audio") + private Integer enableAudio; + + @ConfigKeyId("httpServerPort") + private Integer httpServerPort; + + @ConfigKeyId("rtspPort") + private Integer rtspPort; + + @ConfigKeyId("rtmpPort") + private Integer rtmpPort; + + @ConfigKeyId("httpFlvPort") + private Integer httpFlvPort; + + @ConfigKeyId("hls_enable") + private Integer hlsEnable; + + @ConfigKeyId("hlsPort") + private Integer hlsPort; + + @ConfigKeyId("wsFlvPort") + private Integer wsFlvPort; + + @ConfigKeyId("httpMp4Port") + private Integer httpMp4Port; + + @ConfigKeyId("ps_tsRecvPort") + private Integer psTsRecvPort; + + @ConfigKeyId("1078Port") + private Integer jtt1078RecvPort; + + @ConfigKeyId("hlsCutType") + private Integer hlsCutType; + + @ConfigKeyId("h265CutType") + private Integer h265CutType; + + @ConfigKeyId("RecvThreadCount") + private Integer RecvThreadCount; + + @ConfigKeyId("SendThreadCount") + private Integer SendThreadCount; + + @ConfigKeyId("GB28181RtpTCPHeadType") + private Integer GB28181RtpTCPHeadType; + + @ConfigKeyId("ReConnectingCount") + private Integer ReConnectingCount; + + @ConfigKeyId("maxTimeNoOneWatch") + private Integer maxTimeNoOneWatch; + + @ConfigKeyId("pushEnable_mp4") + private Integer pushEnableMp4; + + @ConfigKeyId("fileSecond") + private Integer fileSecond; + + @ConfigKeyId("fileKeepMaxTime") + private Integer fileKeepMaxTime; + + @ConfigKeyId("httpDownloadSpeed") + private Integer httpDownloadSpeed; + + @ConfigKeyId("RecordReplayThread") + private Integer RecordReplayThread; + + @ConfigKeyId("convertMaxObject") + private Integer convertMaxObject; + + @ConfigKeyId("version") + private String version; + + @ConfigKeyId("recordPath") + private String recordPath; + + @ConfigKeyId("picturePath") + private String picturePath; + + @ConfigKeyId("noneReaderDuration") + private Integer noneReaderDuration; + + @ConfigKeyId("on_server_started") + private String onServerStarted; + + @ConfigKeyId("on_server_keepalive") + private String onServerKeepalive; + + @ConfigKeyId("on_play") + private String onPlay; + + @ConfigKeyId("on_publish") + private String onPublish; + + @ConfigKeyId("on_stream_arrive") + private String onStreamArrive; + + @ConfigKeyId("on_stream_not_arrive") + private String onStreamNotArrive; + + @ConfigKeyId("on_stream_none_reader") + private String onStreamNoneReader; + + @ConfigKeyId("on_stream_disconnect") + private String onStreamDisconnect; + + @ConfigKeyId("on_stream_not_found") + private String onStreamNotFound; + + @ConfigKeyId("on_record_mp4") + private String onRecordMp4; + + @ConfigKeyId("on_delete_record_mp4") + private String onDeleteRecordMp4; + + @ConfigKeyId("on_record_progress") + private String onRecordProgress; + + @ConfigKeyId("on_record_ts") + private String onRecordTs; + + @ConfigKeyId("enable_GetFileDuration") + private Integer enableGetFileDuration; + + @ConfigKeyId("keepaliveDuration") + private Integer keepaliveDuration; + + @ConfigKeyId("captureReplayType") + private Integer captureReplayType; + + @ConfigKeyId("pictureMaxCount") + private Integer pictureMaxCount; + + @ConfigKeyId("videoFileFormat") + private Integer videoFileFormat; + + @ConfigKeyId("MaxDiconnectTimeoutSecond") + private Integer maxDiconnectTimeoutSecond; + + @ConfigKeyId("G711ConvertAAC") + private Integer g711ConvertAAC; + + @ConfigKeyId("filterVideo_enable") + private Integer filterVideoEnable; + + @ConfigKeyId("filterVideo_text") + private String filterVideoText; + + @ConfigKeyId("FilterFontSize") + private Integer filterFontSize; + + @ConfigKeyId("FilterFontColor") + private String filterFontColor; + + @ConfigKeyId("FilterFontLeft") + private Integer filterFontLeft; + + @ConfigKeyId("FilterFontTop") + private Integer filterFontTop; + + @ConfigKeyId("FilterFontAlpha") + private Double filterFontAlpha; + + @ConfigKeyId("convertOutWidth") + private Integer convertOutWidth; + + @ConfigKeyId("convertOutHeight") + private Integer convertOutHeight; + + @ConfigKeyId("convertOutBitrate") + private Integer convertOutBitrate; + + @ConfigKeyId("flvPlayAddMute") + private Integer flvPlayAddMute; + + @ConfigKeyId("gb28181LibraryUse") + private Integer gb28181LibraryUse; + + @ConfigKeyId("rtc.listening-ip") + private String rtcListeningIp; + + @ConfigKeyId("rtc.listening-port") + private Integer rtcListeningIpPort; + + @ConfigKeyId("rtc.external-ip") + private String rtcExternalIp; + + @ConfigKeyId("rtc.realm") + private String rtcRealm; + + @ConfigKeyId("rtc.user") + private String rtcUser; + + @ConfigKeyId("rtc.min-port") + private Integer rtcMinPort; + + @ConfigKeyId("rtc.max-port") + private Integer rtcMaxPort; + + public static AblServerConfig getInstance(JSONArray jsonArray) { + if (jsonArray == null || jsonArray.isEmpty()) { + return null; + } + AblServerConfig ablServerConfig = new AblServerConfig(); + Field[] fields = AblServerConfig.class.getDeclaredFields(); + Map fieldMap = new HashMap<>(); + for (Field field : fields) { + if (field.isAnnotationPresent(ConfigKeyId.class)) { + ConfigKeyId configKeyId = field.getAnnotation(ConfigKeyId.class); + fieldMap.put(configKeyId.value(), field); + } + } + for (int i = 0; i < jsonArray.size(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + if (jsonObject == null) { + continue; + } + for (String key : fieldMap.keySet()) { + if (jsonObject.containsKey(key)) { + Field field = fieldMap.get(key); + field.setAccessible(true); + try { + field.set(ablServerConfig, jsonObject.getObject(key, fieldMap.get(key).getType())); + } catch (IllegalAccessException e) {} + } + } + } + return ablServerConfig; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ConfigKeyId.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ConfigKeyId.java new file mode 100644 index 0000000..244bb94 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ConfigKeyId.java @@ -0,0 +1,10 @@ +package com.genersoft.iot.vmp.media.abl.bean; + +import java.lang.annotation.*; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ConfigKeyId { + String value(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/ABLHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/ABLHookParam.java new file mode 100644 index 0000000..796935e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/ABLHookParam.java @@ -0,0 +1,65 @@ +package com.genersoft.iot.vmp.media.abl.bean.hook; + +public class ABLHookParam { + private String mediaServerId; + + /** + * 应用名 + */ + private String app; + + /** + * 流id + */ + private String stream; + + /** + * 媒体流来源编号,可以根据这个key进行关闭流媒体 可以调用delMediaStream或close_streams 函数进行关闭 + */ + private String key; + + /** + * 媒体流来源网络编号,可参考附表 + */ + private Integer networkType; + + public String getMediaServerId() { + return mediaServerId; + } + + public void setMediaServerId(String mediaServerId) { + this.mediaServerId = mediaServerId; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public Integer getNetworkType() { + return networkType; + } + + public void setNetworkType(Integer networkType) { + this.networkType = networkType; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnPlayABLHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnPlayABLHookParam.java new file mode 100644 index 0000000..77c180d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnPlayABLHookParam.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.media.abl.bean.hook; + + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class OnPlayABLHookParam extends ABLHookParam{ + + private String ip; + private Integer port; + private String params; + + @Override + public String toString() { + return "OnPlayABLHookParam{" + + "ip='" + ip + '\'' + + ", port=" + port + + ", params='" + params + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnPublishABLHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnPublishABLHookParam.java new file mode 100644 index 0000000..11da274 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnPublishABLHookParam.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.media.abl.bean.hook; + +public class OnPublishABLHookParam extends ABLHookParam{ + private String ip; + private Integer port; + private String params; + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getParams() { + return params; + } + + public void setParams(String params) { + this.params = params; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnRecordMp4ABLHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnRecordMp4ABLHookParam.java new file mode 100644 index 0000000..6aaddfe --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnRecordMp4ABLHookParam.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.media.abl.bean.hook; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class OnRecordMp4ABLHookParam extends ABLHookParam{ + private String fileName; + private String startTime; + private String endTime; + private long fileSize; +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnRecordProgressABLHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnRecordProgressABLHookParam.java new file mode 100644 index 0000000..93e6c09 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnRecordProgressABLHookParam.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.media.abl.bean.hook; + +public class OnRecordProgressABLHookParam extends OnRecordMp4ABLHookParam{ + private Integer currentFileDuration; + private Integer TotalVideoDuration; + private String startTime; + private String endTime; + + public Integer getCurrentFileDuration() { + return currentFileDuration; + } + + public void setCurrentFileDuration(Integer currentFileDuration) { + this.currentFileDuration = currentFileDuration; + } + + public Integer getTotalVideoDuration() { + return TotalVideoDuration; + } + + public void setTotalVideoDuration(Integer totalVideoDuration) { + TotalVideoDuration = totalVideoDuration; + } + + public String getStartTime() { + return startTime; + } + + public void setStartTime(String startTime) { + this.startTime = startTime; + } + + public String getEndTime() { + return endTime; + } + + public void setEndTime(String endTime) { + this.endTime = endTime; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnServerKeepaliveABLHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnServerKeepaliveABLHookParam.java new file mode 100644 index 0000000..ea1ac97 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnServerKeepaliveABLHookParam.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.media.abl.bean.hook; + +public class OnServerKeepaliveABLHookParam { + private String localipAddress; + private String mediaServerId; + private String datetime; + + + public String getLocalipAddress() { + return localipAddress; + } + + public void setLocalipAddress(String localipAddress) { + this.localipAddress = localipAddress; + } + + public String getMediaServerId() { + return mediaServerId; + } + + public void setMediaServerId(String mediaServerId) { + this.mediaServerId = mediaServerId; + } + + public String getDatetime() { + return datetime; + } + + public void setDatetime(String datetime) { + this.datetime = datetime; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnServerStaredABLHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnServerStaredABLHookParam.java new file mode 100644 index 0000000..a9ec44c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnServerStaredABLHookParam.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.media.abl.bean.hook; + +public class OnServerStaredABLHookParam { + private String localipAddress; + private String mediaServerId; + private String datetime; + + + public String getLocalipAddress() { + return localipAddress; + } + + public void setLocalipAddress(String localipAddress) { + this.localipAddress = localipAddress; + } + + public String getMediaServerId() { + return mediaServerId; + } + + public void setMediaServerId(String mediaServerId) { + this.mediaServerId = mediaServerId; + } + + public String getDatetime() { + return datetime; + } + + public void setDatetime(String datetime) { + this.datetime = datetime; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnStreamArriveABLHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnStreamArriveABLHookParam.java new file mode 100644 index 0000000..032a517 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnStreamArriveABLHookParam.java @@ -0,0 +1,112 @@ +package com.genersoft.iot.vmp.media.abl.bean.hook; + +import com.genersoft.iot.vmp.media.abl.bean.ABLUrls; +import lombok.Getter; +import lombok.Setter; + +/** + * 流到来的事件 + */ +@Getter +@Setter +public class OnStreamArriveABLHookParam extends ABLHookParam{ + + + + /** + * 推流鉴权Id + */ + private String callId; + + /** + * 状态 + */ + private Boolean status; + + + /** + * + */ + private Boolean enableHls; + + + /** + * + */ + private Boolean transcodingStatus; + + + /** + * + */ + private String sourceURL; + + + /** + * + */ + private Integer readerCount; + + + /** + * + */ + private Integer noneReaderDuration; + + + /** + * + */ + private String videoCodec; + + + /** + * + */ + private Integer videoFrameSpeed; + + + /** + * + */ + private Integer width; + + + /** + * + */ + private Integer height; + + + /** + * + */ + private Integer videoBitrate; + + + /** + * + */ + private String audioCodec; + + + /** + * + */ + private Integer audioChannels; + + + /** + * + */ + private Integer audioSampleRate; + + + /** + * + */ + private Integer audioBitrate; + + + private ABLUrls url; +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/event/HookAblServerKeepaliveEvent.java b/src/main/java/com/genersoft/iot/vmp/media/abl/event/HookAblServerKeepaliveEvent.java new file mode 100644 index 0000000..74465e4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/event/HookAblServerKeepaliveEvent.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.media.abl.event; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import org.springframework.context.ApplicationEvent; + +/** + * zlm 心跳事件 + */ +public class HookAblServerKeepaliveEvent extends ApplicationEvent { + + public HookAblServerKeepaliveEvent(Object source) { + super(source); + } + + private MediaServer mediaServerItem; + + public MediaServer getMediaServerItem() { + return mediaServerItem; + } + + public void setMediaServerItem(MediaServer mediaServerItem) { + this.mediaServerItem = mediaServerItem; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/event/HookAblServerStartEvent.java b/src/main/java/com/genersoft/iot/vmp/media/abl/event/HookAblServerStartEvent.java new file mode 100644 index 0000000..12bdac0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/event/HookAblServerStartEvent.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.media.abl.event; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import org.springframework.context.ApplicationEvent; + +/** + * zlm server_start事件 + */ +public class HookAblServerStartEvent extends ApplicationEvent { + + public HookAblServerStartEvent(Object source) { + super(source); + } + + private MediaServer mediaServerItem; + + public MediaServer getMediaServerItem() { + return mediaServerItem; + } + + public void setMediaServerItem(MediaServer mediaServerItem) { + this.mediaServerItem = mediaServerItem; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/bean/MediaInfo.java b/src/main/java/com/genersoft/iot/vmp/media/bean/MediaInfo.java new file mode 100644 index 0000000..54cf6eb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/bean/MediaInfo.java @@ -0,0 +1,308 @@ +package com.genersoft.iot.vmp.media.bean; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.media.abl.bean.ABLMedia; +import com.genersoft.iot.vmp.media.abl.bean.hook.OnStreamArriveABLHookParam; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamChangedHookParam; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OriginType; +import com.genersoft.iot.vmp.utils.MediaServerUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.util.ObjectUtils; + +import java.util.List; +import java.util.Map; + +/** + * 视频信息 + */ +@Data +@Schema(description = "视频信息") +public class MediaInfo { + @Schema(description = "应用名") + private String app; + @Schema(description = "流ID") + private String stream; + @Schema(description = "流媒体节点") + private MediaServer mediaServer; + @Schema(description = "协议") + private String schema; + + @Schema(description = "观看人数") + private Integer readerCount; + @Schema(description = "视频编码类型") + private String videoCodec; + @Schema(description = "视频宽度") + private Integer width; + @Schema(description = "视频高度") + private Integer height; + @Schema(description = "FPS") + private Integer fps; + @Schema(description = "丢包率") + private Integer loss; + @Schema(description = "音频编码类型") + private String audioCodec; + @Schema(description = "音频通道数") + private Integer audioChannels; + @Schema(description = "音频采样率") + private Integer audioSampleRate; + @Schema(description = "时长") + private Long duration; + @Schema(description = "在线") + private Boolean online; + @Schema(description = "unknown = 0,rtmp_push=1,rtsp_push=2,rtp_push=3,pull=4,ffmpeg_pull=5,mp4_vod=6,device_chn=7,rtc_push=8") + private Integer originType; + @Schema(description = "originType的文本描述") + private String originTypeStr; + @Schema(description = "产生流的源流地址") + private String originUrl; + @Schema(description = "存活时间,单位秒") + private Long aliveSecond; + @Schema(description = "数据产生速度,单位byte/s") + private Long bytesSpeed; + @Schema(description = "鉴权参数") + private String callId; + @Schema(description = "额外参数") + private Map paramMap; + @Schema(description = "服务ID") + private String serverId; + + + public static MediaInfo getInstance(JSONObject jsonObject, MediaServer mediaServer, String serverId) { + MediaInfo mediaInfo = new MediaInfo(); + mediaInfo.setMediaServer(mediaServer); + mediaInfo.setServerId(serverId); + String app = jsonObject.getString("app"); + mediaInfo.setApp(app); + String stream = jsonObject.getString("stream"); + mediaInfo.setStream(stream); + String schema = jsonObject.getString("schema"); + mediaInfo.setSchema(schema); + Integer totalReaderCount = jsonObject.getInteger("totalReaderCount"); + Boolean online = jsonObject.getBoolean("online"); + Integer originType = jsonObject.getInteger("originType"); + String originUrl = jsonObject.getString("originUrl"); + String originTypeStr = jsonObject.getString("originTypeStr"); + Long aliveSecond = jsonObject.getLong("aliveSecond"); + String params = jsonObject.getString("params"); + Long bytesSpeed = jsonObject.getLong("bytesSpeed"); + if (totalReaderCount != null) { + mediaInfo.setReaderCount(totalReaderCount); + } else { + mediaInfo.setReaderCount(0); + } + if (online != null) { + mediaInfo.setOnline(online); + } + if (originType != null) { + mediaInfo.setOriginType(originType); + } + if (originTypeStr != null) { + mediaInfo.setOriginTypeStr(originTypeStr); + } + + if (aliveSecond != null) { + mediaInfo.setAliveSecond(aliveSecond); + } + if (bytesSpeed != null) { + mediaInfo.setBytesSpeed(bytesSpeed); + } + if (params != null) { + mediaInfo.setParamMap(MediaServerUtils.urlParamToMap(params)); + if(mediaInfo.getCallId() == null) { + mediaInfo.setCallId(mediaInfo.getParamMap().get("callId")); + } + } + JSONArray jsonArray = jsonObject.getJSONArray("tracks"); + if (!ObjectUtils.isEmpty(jsonArray)) { + for (int i = 0; i < jsonArray.size(); i++) { + JSONObject trackJson = jsonArray.getJSONObject(i); + Integer channels = trackJson.getInteger("channels"); + Integer codecId = trackJson.getInteger("codec_id"); + Integer codecType = trackJson.getInteger("codec_type"); + Integer sampleRate = trackJson.getInteger("sample_rate"); + Integer height = trackJson.getInteger("height"); + Integer width = trackJson.getInteger("width"); + Integer fps = trackJson.getInteger("fps"); + Integer loss = trackJson.getInteger("loss"); + Integer frames = trackJson.getInteger("frames"); + Long keyFrames = trackJson.getLongValue("key_frames"); + Integer gop_interval_ms = trackJson.getInteger("gop_interval_ms"); + Long gop_size = trackJson.getLongValue("gop_size"); + + Long duration = trackJson.getLongValue("duration"); + if (channels != null) { + mediaInfo.setAudioChannels(channels); + } + if (sampleRate != null) { + mediaInfo.setAudioSampleRate(sampleRate); + } + if (height != null) { + mediaInfo.setHeight(height); + } + if (width != null) { + mediaInfo.setWidth(width); + } + if (fps != null) { + mediaInfo.setFps(fps); + } + if (loss != null) { + mediaInfo.setLoss(loss); + } + if (duration > 0L) { + mediaInfo.setDuration(duration); + } + if (codecId != null) { + switch (codecId) { + case 0: + mediaInfo.setVideoCodec("H264"); + break; + case 1: + mediaInfo.setVideoCodec("H265"); + break; + case 2: + mediaInfo.setAudioCodec("AAC"); + break; + case 3: + mediaInfo.setAudioCodec("G711A"); + break; + case 4: + mediaInfo.setAudioCodec("G711U"); + break; + } + } + } + } + return mediaInfo; + } + + public static MediaInfo getInstance(OnStreamChangedHookParam param, MediaServer mediaServer, String serverId) { + + MediaInfo mediaInfo = new MediaInfo(); + mediaInfo.setApp(param.getApp()); + mediaInfo.setStream(param.getStream()); + mediaInfo.setSchema(param.getSchema()); + mediaInfo.setMediaServer(mediaServer); + mediaInfo.setReaderCount(param.getTotalReaderCount()); + mediaInfo.setOnline(param.isRegist()); + mediaInfo.setOriginType(param.getOriginType()); + mediaInfo.setOriginTypeStr(param.getOriginTypeStr()); + mediaInfo.setOriginUrl(param.getOriginUrl()); + mediaInfo.setOriginUrl(param.getOriginUrl()); + mediaInfo.setAliveSecond(param.getAliveSecond()); + mediaInfo.setBytesSpeed(param.getBytesSpeed()); + mediaInfo.setParamMap(param.getParamMap()); + if(mediaInfo.getCallId() == null) { + mediaInfo.setCallId(param.getParamMap().get("callId")); + } + mediaInfo.setServerId(serverId); + List tracks = param.getTracks(); + if (tracks == null || tracks.isEmpty()) { + return mediaInfo; + } + for (OnStreamChangedHookParam.MediaTrack mediaTrack : tracks) { + switch (mediaTrack.getCodec_id()) { + case 0: + mediaInfo.setVideoCodec("H264"); + break; + case 1: + mediaInfo.setVideoCodec("H265"); + break; + case 2: + mediaInfo.setAudioCodec("AAC"); + break; + case 3: + mediaInfo.setAudioCodec("G711A"); + break; + case 4: + mediaInfo.setAudioCodec("G711U"); + break; + } + if (mediaTrack.getSample_rate() > 0) { + mediaInfo.setAudioSampleRate(mediaTrack.getSample_rate()); + } + if (mediaTrack.getChannels() > 0) { + mediaInfo.setAudioChannels(mediaTrack.getChannels()); + } + if (mediaTrack.getHeight() > 0) { + mediaInfo.setHeight(mediaTrack.getHeight()); + } + if (mediaTrack.getWidth() > 0) { + mediaInfo.setWidth(mediaTrack.getWidth()); + } + } + return mediaInfo; + } + + public static MediaInfo getInstance(OnStreamArriveABLHookParam param, MediaServer mediaServer) { + + MediaInfo mediaInfo = new MediaInfo(); + mediaInfo.setApp(param.getApp()); + mediaInfo.setStream(param.getStream()); + mediaInfo.setMediaServer(mediaServer); + mediaInfo.setReaderCount(param.getReaderCount()); + mediaInfo.setOnline(true); + mediaInfo.setVideoCodec(param.getVideoCodec()); + switch (param.getNetworkType()) { + case 21: + mediaInfo.setOriginType(OriginType.RTMP_PUSH.ordinal()); + break; + case 23: + mediaInfo.setOriginType(OriginType.RTSP_PUSH.ordinal()); + break; + case 30: + case 31: + case 32: + case 33: + mediaInfo.setOriginType(OriginType.PULL.ordinal()); + break; + default: + mediaInfo.setOriginType(OriginType.UNKNOWN.ordinal()); + break; + + } + mediaInfo.setWidth(param.getWidth()); + mediaInfo.setHeight(param.getHeight()); + mediaInfo.setAudioCodec(param.getAudioCodec()); + mediaInfo.setAudioChannels(param.getAudioChannels()); + mediaInfo.setAudioSampleRate(param.getAudioSampleRate()); + + return mediaInfo; + } + + public static MediaInfo getInstance(ABLMedia ablMedia, MediaServer mediaServer) { + MediaInfo mediaInfo = new MediaInfo(); + mediaInfo.setApp(ablMedia.getApp()); + mediaInfo.setStream(ablMedia.getStream()); + mediaInfo.setMediaServer(mediaServer); + mediaInfo.setReaderCount(ablMedia.getReaderCount()); + mediaInfo.setOnline(true); + mediaInfo.setVideoCodec(ablMedia.getVideoCodec()); + switch (ablMedia.getNetworkType()) { + case 21: + mediaInfo.setOriginType(OriginType.RTMP_PUSH.ordinal()); + break; + case 23: + mediaInfo.setOriginType(OriginType.RTSP_PUSH.ordinal()); + break; + case 30: + case 31: + case 32: + case 33: + mediaInfo.setOriginType(OriginType.PULL.ordinal()); + break; + default: + mediaInfo.setOriginType(OriginType.UNKNOWN.ordinal()); + break; + + } + mediaInfo.setWidth(ablMedia.getWidth()); + mediaInfo.setHeight(ablMedia.getHeight()); + mediaInfo.setAudioCodec(ablMedia.getAudioCodec()); + mediaInfo.setAudioChannels(ablMedia.getAudioChannels()); + mediaInfo.setAudioSampleRate(ablMedia.getAudioSampleRate()); + + return mediaInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/bean/MediaServer.java b/src/main/java/com/genersoft/iot/vmp/media/bean/MediaServer.java new file mode 100755 index 0000000..38de639 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/bean/MediaServer.java @@ -0,0 +1,163 @@ +package com.genersoft.iot.vmp.media.bean; + + +import com.genersoft.iot.vmp.media.abl.bean.AblServerConfig; +import com.genersoft.iot.vmp.media.zlm.dto.ZLMServerConfig; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.util.ObjectUtils; + +@Schema(description = "流媒体服务信息") +@Data +public class MediaServer { + + @Schema(description = "ID") + private String id; + + @Schema(description = "IP") + private String ip; + + @Schema(description = "hook使用的IP(zlm访问WVP使用的IP)") + private String hookIp = "127.0.0.1"; + + @Schema(description = "SDP IP") + private String sdpIp; + + @Schema(description = "流IP") + private String streamIp; + + @Schema(description = "HTTP端口") + private int httpPort; + + @Schema(description = "HTTPS端口") + private int httpSSlPort; + + @Schema(description = "RTMP端口") + private int rtmpPort; + + @Schema(description = "flv端口") + private int flvPort; + + @Schema(description = "https-flv端口") + private int flvSSLPort; + + @Schema(description = "mp4端口") + private int mp4Port; + + @Schema(description = "ws-flv端口") + private int wsFlvPort; + + @Schema(description = "wss-flv端口") + private int wsFlvSSLPort; + + @Schema(description = "RTMPS端口") + private int rtmpSSlPort; + + @Schema(description = "RTP收流端口(单端口模式有用)") + private int rtpProxyPort; + + @Schema(description = "1078收流端口(单端口模式有用)") + private int jttProxyPort; + + @Schema(description = "RTSP端口") + private int rtspPort; + + @Schema(description = "RTSPS端口") + private int rtspSSLPort; + + @Schema(description = "是否开启自动配置ZLM") + private boolean autoConfig; + + @Schema(description = "ZLM鉴权参数") + private String secret; + + @Schema(description = "keepalive hook触发间隔,单位秒") + private Float hookAliveInterval; + + @Schema(description = "是否使用多端口模式") + private boolean rtpEnable; + + @Schema(description = "状态") + private boolean status; + + @Schema(description = "多端口RTP收流端口范围") + private String rtpPortRange; + + @Schema(description = "RTP发流端口范围") + private String sendRtpPortRange; + + @Schema(description = "assist服务端口") + private int recordAssistPort; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "更新时间") + private String updateTime; + + @Schema(description = "上次心跳时间") + private String lastKeepaliveTime; + + @Schema(description = "是否是默认ZLM") + private boolean defaultServer; + + @Schema(description = "录像存储时长") + private int recordDay; + + @Schema(description = "录像存储路径") + private String recordPath; + @Schema(description = "类型: zlm/abl") + private String type; + + @Schema(description = "转码的前缀") + private String transcodeSuffix; + + @Schema(description = "服务Id") + private String serverId; + + public MediaServer() { + } + + public MediaServer(ZLMServerConfig zlmServerConfig, String sipIp) { + id = zlmServerConfig.getGeneralMediaServerId(); + ip = zlmServerConfig.getIp(); + hookIp = ObjectUtils.isEmpty(zlmServerConfig.getHookIp())? sipIp: zlmServerConfig.getHookIp(); + sdpIp = ObjectUtils.isEmpty(zlmServerConfig.getSdpIp())? zlmServerConfig.getIp(): zlmServerConfig.getSdpIp(); + streamIp = ObjectUtils.isEmpty(zlmServerConfig.getStreamIp())? zlmServerConfig.getIp(): zlmServerConfig.getStreamIp(); + httpPort = zlmServerConfig.getHttpPort(); + httpSSlPort = zlmServerConfig.getHttpSSLport(); + rtmpPort = zlmServerConfig.getRtmpPort(); + rtmpSSlPort = zlmServerConfig.getRtmpSslPort(); + rtpProxyPort = zlmServerConfig.getRtpProxyPort(); + rtspPort = zlmServerConfig.getRtspPort(); + rtspSSLPort = zlmServerConfig.getRtspSSlport(); + autoConfig = true; // 默认值true; + secret = zlmServerConfig.getApiSecret(); + hookAliveInterval = zlmServerConfig.getHookAliveInterval(); + rtpEnable = false; // 默认使用单端口;直到用户自己设置开启多端口 + rtpPortRange = zlmServerConfig.getPortRange().replace("_",","); // 默认使用30000,30500作为级联时发送流的端口号 + recordAssistPort = 0; // 默认关闭 + transcodeSuffix = zlmServerConfig.getTranscodeSuffix(); + + } + + public MediaServer(AblServerConfig config, String sipIp) { + id = config.getMediaServerId(); + ip = config.getServerIp(); + hookIp = sipIp; + sdpIp = config.getServerIp(); + streamIp = config.getServerIp(); + httpPort = config.getHttpServerPort(); + flvPort = config.getHttpFlvPort(); + mp4Port = config.getHttpMp4Port(); + wsFlvPort = config.getWsFlvPort(); + rtmpPort = config.getRtmpPort(); + rtpProxyPort = config.getJtt1078RecvPort(); + rtspPort = config.getRtspPort(); + autoConfig = true; // 默认值true; + secret = config.getSecret(); + rtpEnable = false; // 默认使用单端口;直到用户自己设置开启多端口 + rtpPortRange = "30000,30500"; // 默认使用30000,30500作为级联时发送流的端口号 + recordAssistPort = 0; // 默认关闭 + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/bean/RecordInfo.java b/src/main/java/com/genersoft/iot/vmp/media/bean/RecordInfo.java new file mode 100644 index 0000000..b903058 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/bean/RecordInfo.java @@ -0,0 +1,76 @@ +package com.genersoft.iot.vmp.media.bean; + +import com.genersoft.iot.vmp.media.abl.bean.hook.OnRecordMp4ABLHookParam; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnRecordMp4HookParam; +import com.genersoft.iot.vmp.service.bean.CloudRecordItem; +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.Data; + +@Data +public class RecordInfo { + private String app; + private String stream; + private String fileName; + private String filePath; + private long fileSize; + private String folder; + private String url; + /** + * 单位毫秒 + */ + private long startTime; + /** + * 单位毫秒 + */ + private double timeLen; + private String params; + + public static RecordInfo getInstance(OnRecordMp4HookParam hookParam) { + RecordInfo recordInfo = new RecordInfo(); + recordInfo.setApp(hookParam.getApp()); + recordInfo.setStream(hookParam.getStream()); + recordInfo.setFileName(hookParam.getFile_name()); + recordInfo.setUrl(hookParam.getUrl()); + recordInfo.setFolder(hookParam.getFolder()); + recordInfo.setFilePath(hookParam.getFile_path()); + recordInfo.setFileSize(hookParam.getFile_size()); + recordInfo.setStartTime(hookParam.getStart_time() * 1000); + recordInfo.setTimeLen(hookParam.getTime_len() * 1000); + return recordInfo; + } + + public static RecordInfo getInstance(OnRecordMp4ABLHookParam hookParam) { + RecordInfo recordInfo = new RecordInfo(); + recordInfo.setApp(hookParam.getApp()); + recordInfo.setStream(hookParam.getStream()); + recordInfo.setFileName(hookParam.getFileName()); + recordInfo.setStartTime(DateUtil.urlToTimestampMs(hookParam.getStartTime())); + recordInfo.setTimeLen(DateUtil.urlToTimestampMs(hookParam.getEndTime()) - recordInfo.getStartTime()); + recordInfo.setFileSize(hookParam.getFileSize()); + return recordInfo; + } + + public static RecordInfo getInstance(CloudRecordItem cloudRecordItem) { + RecordInfo recordInfo = new RecordInfo(); + recordInfo.setApp(cloudRecordItem.getApp()); + recordInfo.setStream(cloudRecordItem.getStream()); + recordInfo.setFileName(cloudRecordItem.getFileName()); + recordInfo.setStartTime(cloudRecordItem.getStartTime()); + recordInfo.setTimeLen(cloudRecordItem.getTimeLen()); + recordInfo.setFileSize(cloudRecordItem.getFileSize()); + recordInfo.setFilePath(cloudRecordItem.getFilePath()); + return recordInfo; + } + + @Override + public String toString() { + return "RecordInfo{" + + "文件名称='" + fileName + '\'' + + ", 文件路径='" + filePath + '\'' + + ", 文件大小=" + fileSize + + ", 开始时间=" + startTime + + ", 时长=" + timeLen + + ", params=" + params + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/bean/ResultForOnPublish.java b/src/main/java/com/genersoft/iot/vmp/media/bean/ResultForOnPublish.java new file mode 100644 index 0000000..88f7387 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/bean/ResultForOnPublish.java @@ -0,0 +1,59 @@ +package com.genersoft.iot.vmp.media.bean; + +public class ResultForOnPublish { + + private boolean enable_audio; + private boolean enable_mp4; + private int mp4_max_second; + private String mp4_save_path; + private String stream_replace; + private Integer modify_stamp; + + public boolean isEnable_audio() { + return enable_audio; + } + + public void setEnable_audio(boolean enable_audio) { + this.enable_audio = enable_audio; + } + + public boolean isEnable_mp4() { + return enable_mp4; + } + + public void setEnable_mp4(boolean enable_mp4) { + this.enable_mp4 = enable_mp4; + } + + public int getMp4_max_second() { + return mp4_max_second; + } + + public void setMp4_max_second(int mp4_max_second) { + this.mp4_max_second = mp4_max_second; + } + + public String getMp4_save_path() { + return mp4_save_path; + } + + public void setMp4_save_path(String mp4_save_path) { + this.mp4_save_path = mp4_save_path; + } + + public String getStream_replace() { + return stream_replace; + } + + public void setStream_replace(String stream_replace) { + this.stream_replace = stream_replace; + } + + public Integer getModify_stamp() { + return modify_stamp; + } + + public void setModify_stamp(Integer modify_stamp) { + this.modify_stamp = modify_stamp; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/hook/Hook.java b/src/main/java/com/genersoft/iot/vmp/media/event/hook/Hook.java new file mode 100755 index 0000000..0d1c55a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/hook/Hook.java @@ -0,0 +1,51 @@ +package com.genersoft.iot.vmp.media.event.hook; + +import lombok.Data; + +/** + * zlm hook事件的参数 + * @author lin + */ +@Data +public class Hook { + + private HookType hookType; + + private String app; + + private String stream; + + private Long expireTime; + + + public static Hook getInstance(HookType hookType, String app, String stream) { + Hook hookSubscribe = new Hook(); + hookSubscribe.setApp(app); + hookSubscribe.setStream(stream); + hookSubscribe.setHookType(hookType); + hookSubscribe.setExpireTime(System.currentTimeMillis() + 5 * 60 * 1000); + return hookSubscribe; + } + + public static Hook getInstance(HookType hookType, String app, String stream, String mediaServer) { + // TODO 后续修改所有方法 + return Hook.getInstance(hookType, app, stream); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Hook) { + Hook param = (Hook) obj; + return param.getHookType().equals(this.hookType) + && param.getApp().equals(this.app) + && param.getStream().equals(this.stream); + }else { + return false; + } + } + + @Override + public String toString() { + return this.getHookType() + this.getApp() + this.getStream(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookData.java b/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookData.java new file mode 100644 index 0000000..6aa3a67 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookData.java @@ -0,0 +1,78 @@ +package com.genersoft.iot.vmp.media.event.hook; + +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.media.event.media.MediaEvent; +import com.genersoft.iot.vmp.media.event.media.MediaPublishEvent; +import com.genersoft.iot.vmp.media.event.media.MediaRecordMp4Event; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * Hook返回的内容 + */ +@Data +public class HookData { + /** + * 应用名 + */ + private String app; + /** + * 流ID + */ + private String stream; + /** + * 流媒体节点 + */ + private MediaServer mediaServer; + /** + * 协议 + */ + private String schema; + + /** + * 流信息 + */ + private MediaInfo mediaInfo; + + /** + * 录像信息 + */ + private RecordInfo recordInfo; + + @Schema(description = "推流的额外参数") + private String params; + public static HookData getInstance(MediaEvent mediaEvent) { + HookData hookData = new HookData(); + if (mediaEvent instanceof MediaPublishEvent) { + MediaPublishEvent event = (MediaPublishEvent) mediaEvent; + hookData.setApp(event.getApp()); + hookData.setStream(event.getStream()); + hookData.setSchema(event.getSchema()); + hookData.setMediaServer(event.getMediaServer()); + hookData.setParams(event.getParams()); + }else if (mediaEvent instanceof MediaArrivalEvent) { + MediaArrivalEvent event = (MediaArrivalEvent) mediaEvent; + hookData.setApp(event.getApp()); + hookData.setStream(event.getStream()); + hookData.setSchema(event.getSchema()); + hookData.setMediaServer(event.getMediaServer()); + hookData.setMediaInfo(event.getMediaInfo()); + }else if (mediaEvent instanceof MediaRecordMp4Event) { + MediaRecordMp4Event event = (MediaRecordMp4Event) mediaEvent; + hookData.setApp(event.getApp()); + hookData.setStream(event.getStream()); + hookData.setSchema(event.getSchema()); + hookData.setMediaServer(event.getMediaServer()); + hookData.setRecordInfo(event.getRecordInfo()); + }else { + hookData.setApp(mediaEvent.getApp()); + hookData.setStream(mediaEvent.getStream()); + hookData.setSchema(mediaEvent.getSchema()); + hookData.setMediaServer(mediaEvent.getMediaServer()); + } + return hookData; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookSubscribe.java b/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookSubscribe.java new file mode 100755 index 0000000..c26f3dc --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookSubscribe.java @@ -0,0 +1,114 @@ +package com.genersoft.iot.vmp.media.event.hook; + +import com.genersoft.iot.vmp.media.event.media.*; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * zlm hook事件的参数 + * @author lin + */ +@Component +public class HookSubscribe { + + /** + * 订阅数据过期时间 + */ + private final long subscribeExpire = 5 * 60 * 1000; + + + @FunctionalInterface + public interface Event{ + void response(HookData data); + } + + /** + * 流到来的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaArrivalEvent event) { + if (event.getSchema() == null || "rtsp".equals(event.getSchema())) { + sendNotify(HookType.on_media_arrival, event); + } + + } + + /** + * 流结束事件 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + if (event.getSchema() == null || "rtsp".equals(event.getSchema())) { + sendNotify(HookType.on_media_departure, event); + } + + } + /** + * 推流鉴权事件 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaPublishEvent event) { + sendNotify(HookType.on_publish, event); + } + /** + * 生成录像文件事件 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaRecordMp4Event event) { + sendNotify(HookType.on_record_mp4, event); + } + + private final Map allSubscribes = new ConcurrentHashMap<>(); + private final Map allHook = new ConcurrentHashMap<>(); + + private void sendNotify(HookType hookType, MediaEvent event) { + Hook paramHook = Hook.getInstance(hookType, event.getApp(), event.getStream()); + Event hookSubscribeEvent = allSubscribes.get(paramHook.toString()); + if (hookSubscribeEvent != null) { + HookData data = HookData.getInstance(event); + hookSubscribeEvent.response(data); + } + } + + public void addSubscribe(Hook hook, HookSubscribe.Event event) { + if (hook.getExpireTime() == null) { + hook.setExpireTime(System.currentTimeMillis() + subscribeExpire); + } + allSubscribes.put(hook.toString(), event); + allHook.put(hook.toString(), hook); + } + + public void removeSubscribe(Hook hook) { + allSubscribes.remove(hook.toString()); + allHook.remove(hook.toString()); + } + + /** + * 对订阅数据进行过期清理 + */ + @Scheduled(fixedRate=subscribeExpire) //每5分钟执行一次 + public void execute(){ + long expireTime = System.currentTimeMillis(); + for (Hook hook : allHook.values()) { + if (hook.getExpireTime() < expireTime) { + allSubscribes.remove(hook.toString()); + allHook.remove(hook.toString()); + } + } + } + + public List getAll() { + return new ArrayList<>(allHook.values()); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookType.java b/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookType.java new file mode 100755 index 0000000..58bd656 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookType.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.media.event.hook; + +/** + * hook类型 + * @author lin + */ + +public enum HookType { + + on_publish, + on_record_mp4, + on_media_arrival, + on_media_departure, + on_rtp_server_timeout, +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaArrivalEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaArrivalEvent.java new file mode 100644 index 0000000..f26f50b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaArrivalEvent.java @@ -0,0 +1,64 @@ +package com.genersoft.iot.vmp.media.event.media; + +import com.genersoft.iot.vmp.media.abl.bean.hook.OnStreamArriveABLHookParam; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamChangedHookParam; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import lombok.Getter; +import lombok.Setter; + +import java.util.Map; + +/** + * 流到来事件 + */ + +public class MediaArrivalEvent extends MediaEvent { + public MediaArrivalEvent(Object source) { + super(source); + } + + public static MediaArrivalEvent getInstance(Object source, OnStreamChangedHookParam hookParam, MediaServer mediaServer, String serverId){ + MediaArrivalEvent mediaArrivalEvent = new MediaArrivalEvent(source); + mediaArrivalEvent.setMediaInfo(MediaInfo.getInstance(hookParam, mediaServer, serverId)); + mediaArrivalEvent.setApp(hookParam.getApp()); + mediaArrivalEvent.setStream(hookParam.getStream()); + mediaArrivalEvent.setMediaServer(mediaServer); + mediaArrivalEvent.setSchema(hookParam.getSchema()); + mediaArrivalEvent.setSchema(hookParam.getSchema()); + mediaArrivalEvent.setParamMap(hookParam.getParamMap()); + return mediaArrivalEvent; + } + public static MediaArrivalEvent getInstance(Object source, OnStreamArriveABLHookParam hookParam, MediaServer mediaServer){ + MediaArrivalEvent mediaArrivalEvent = new MediaArrivalEvent(source); + mediaArrivalEvent.setMediaInfo(MediaInfo.getInstance(hookParam, mediaServer)); + mediaArrivalEvent.setApp(hookParam.getApp()); + mediaArrivalEvent.setStream(hookParam.getStream()); + mediaArrivalEvent.setMediaServer(mediaServer); + mediaArrivalEvent.setCallId(hookParam.getCallId()); + return mediaArrivalEvent; + } + + @Getter + @Setter + private MediaInfo mediaInfo; + + @Getter + @Setter + private String callId; + + @Getter + @Setter + private StreamContent streamInfo; + + @Getter + @Setter + private Map paramMap; + + @Getter + @Setter + private String serverId; + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaDepartureEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaDepartureEvent.java new file mode 100644 index 0000000..02b99f3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaDepartureEvent.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.media.event.media; + +import com.genersoft.iot.vmp.media.abl.bean.hook.ABLHookParam; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamChangedHookParam; + +/** + * 流离开事件 + */ +public class MediaDepartureEvent extends MediaEvent { + public MediaDepartureEvent(Object source) { + super(source); + } + + public static MediaDepartureEvent getInstance(Object source, OnStreamChangedHookParam hookParam, MediaServer mediaServer){ + MediaDepartureEvent mediaDepartureEven = new MediaDepartureEvent(source); + mediaDepartureEven.setApp(hookParam.getApp()); + mediaDepartureEven.setStream(hookParam.getStream()); + mediaDepartureEven.setSchema(hookParam.getSchema()); + mediaDepartureEven.setMediaServer(mediaServer); + return mediaDepartureEven; + } + + public static MediaDepartureEvent getInstance(Object source, ABLHookParam hookParam, MediaServer mediaServer){ + MediaDepartureEvent mediaDepartureEven = new MediaDepartureEvent(source); + mediaDepartureEven.setApp(hookParam.getApp()); + mediaDepartureEven.setStream(hookParam.getStream()); + mediaDepartureEven.setMediaServer(mediaServer); + return mediaDepartureEven; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaEvent.java new file mode 100644 index 0000000..1ddcdd4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaEvent.java @@ -0,0 +1,66 @@ +package com.genersoft.iot.vmp.media.event.media; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import org.springframework.context.ApplicationEvent; + +/** + * 流到来事件 + */ +public class MediaEvent extends ApplicationEvent { + + public MediaEvent(Object source) { + super(source); + } + + private String app; + + private String stream; + + private MediaServer mediaServer; + + private String schema; + + private String params; + + public String getParams() { + return params; + } + + public void setParams(String params) { + this.params = params; + } + + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public MediaServer getMediaServer() { + return mediaServer; + } + + public void setMediaServer(MediaServer mediaServer) { + this.mediaServer = mediaServer; + } + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaNotFoundEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaNotFoundEvent.java new file mode 100644 index 0000000..d4152ee --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaNotFoundEvent.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.media.event.media; + +import com.genersoft.iot.vmp.media.abl.bean.hook.ABLHookParam; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamNotFoundHookParam; + +/** + * 流未找到 + */ +public class MediaNotFoundEvent extends MediaEvent { + public MediaNotFoundEvent(Object source) { + super(source); + } + + public static MediaNotFoundEvent getInstance(Object source, OnStreamNotFoundHookParam hookParam, MediaServer mediaServer){ + MediaNotFoundEvent mediaDepartureEven = new MediaNotFoundEvent(source); + mediaDepartureEven.setApp(hookParam.getApp()); + mediaDepartureEven.setStream(hookParam.getStream()); + mediaDepartureEven.setSchema(hookParam.getSchema()); + mediaDepartureEven.setMediaServer(mediaServer); + mediaDepartureEven.setParams(hookParam.getParams()); + return mediaDepartureEven; + } + + public static MediaNotFoundEvent getInstance(Object source, ABLHookParam hookParam, MediaServer mediaServer){ + MediaNotFoundEvent mediaDepartureEven = new MediaNotFoundEvent(source); + mediaDepartureEven.setApp(hookParam.getApp()); + mediaDepartureEven.setStream(hookParam.getStream()); + mediaDepartureEven.setMediaServer(mediaServer); + return mediaDepartureEven; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaPublishEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaPublishEvent.java new file mode 100644 index 0000000..4b5fa70 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaPublishEvent.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.media.event.media; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnPublishHookParam; + +/** + * 推流鉴权事件 + */ +public class MediaPublishEvent extends MediaEvent { + public MediaPublishEvent(Object source) { + super(source); + } + + public static MediaPublishEvent getInstance(Object source, OnPublishHookParam hookParam, MediaServer mediaServer){ + MediaPublishEvent mediaPublishEvent = new MediaPublishEvent(source); + mediaPublishEvent.setApp(hookParam.getApp()); + mediaPublishEvent.setStream(hookParam.getStream()); + mediaPublishEvent.setMediaServer(mediaServer); + mediaPublishEvent.setSchema(hookParam.getSchema()); + mediaPublishEvent.setParams(hookParam.getParams()); + return mediaPublishEvent; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRecordMp4Event.java b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRecordMp4Event.java new file mode 100644 index 0000000..9fd6def --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRecordMp4Event.java @@ -0,0 +1,47 @@ +package com.genersoft.iot.vmp.media.event.media; + +import com.genersoft.iot.vmp.media.abl.ABLHttpHookListener; +import com.genersoft.iot.vmp.media.abl.bean.hook.OnRecordMp4ABLHookParam; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnRecordMp4HookParam; + +/** + * 录像文件生成事件 + */ +public class MediaRecordMp4Event extends MediaEvent { + public MediaRecordMp4Event(Object source) { + super(source); + } + + private RecordInfo recordInfo; + + public static MediaRecordMp4Event getInstance(Object source, OnRecordMp4HookParam hookParam, MediaServer mediaServer){ + MediaRecordMp4Event mediaRecordMp4Event = new MediaRecordMp4Event(source); + mediaRecordMp4Event.setApp(hookParam.getApp()); + mediaRecordMp4Event.setStream(hookParam.getStream()); + RecordInfo recordInfo = RecordInfo.getInstance(hookParam); + mediaRecordMp4Event.setRecordInfo(recordInfo); + mediaRecordMp4Event.setMediaServer(mediaServer); + return mediaRecordMp4Event; + } + + public static MediaRecordMp4Event getInstance(ABLHttpHookListener source, OnRecordMp4ABLHookParam hookParam, MediaServer mediaServer) { + MediaRecordMp4Event mediaRecordMp4Event = new MediaRecordMp4Event(source); + mediaRecordMp4Event.setApp(hookParam.getApp()); + mediaRecordMp4Event.setStream(hookParam.getStream()); + RecordInfo recordInfo = RecordInfo.getInstance(hookParam); + mediaRecordMp4Event.setRecordInfo(recordInfo); + mediaRecordMp4Event.setMediaServer(mediaServer); + return mediaRecordMp4Event; + } + + public RecordInfo getRecordInfo() { + return recordInfo; + } + + public void setRecordInfo(RecordInfo recordInfo) { + this.recordInfo = recordInfo; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRecordProcessEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRecordProcessEvent.java new file mode 100644 index 0000000..e4d1a56 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRecordProcessEvent.java @@ -0,0 +1,77 @@ +package com.genersoft.iot.vmp.media.event.media; + +import com.genersoft.iot.vmp.media.abl.ABLHttpHookListener; +import com.genersoft.iot.vmp.media.abl.bean.hook.OnRecordProgressABLHookParam; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.utils.DateUtil; + +import java.util.Date; + +/** + * 录像文件进度通知事件 + */ +public class MediaRecordProcessEvent extends MediaEvent { + + private Integer currentFileDuration; + private Integer TotalVideoDuration; + private String fileName; + private long startTime; + private long endTime; + + public MediaRecordProcessEvent(Object source) { + super(source); + } + + public static MediaRecordProcessEvent getInstance(ABLHttpHookListener source, OnRecordProgressABLHookParam hookParam, MediaServer mediaServer) { + MediaRecordProcessEvent mediaRecordMp4Event = new MediaRecordProcessEvent(source); + mediaRecordMp4Event.setApp(hookParam.getApp()); + mediaRecordMp4Event.setStream(hookParam.getStream()); + mediaRecordMp4Event.setCurrentFileDuration(hookParam.getCurrentFileDuration()); + mediaRecordMp4Event.setTotalVideoDuration(hookParam.getTotalVideoDuration()); + mediaRecordMp4Event.setMediaServer(mediaServer); + mediaRecordMp4Event.setFileName(hookParam.getFileName()); + mediaRecordMp4Event.setStartTime(DateUtil.urlToTimestampMs(hookParam.getStartTime())); + mediaRecordMp4Event.setEndTime(DateUtil.urlToTimestampMs(hookParam.getEndTime())); + return mediaRecordMp4Event; + } + + public Integer getCurrentFileDuration() { + return currentFileDuration; + } + + public void setCurrentFileDuration(Integer currentFileDuration) { + this.currentFileDuration = currentFileDuration; + } + + public Integer getTotalVideoDuration() { + return TotalVideoDuration; + } + + public void setTotalVideoDuration(Integer totalVideoDuration) { + TotalVideoDuration = totalVideoDuration; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public long getStartTime() { + return startTime; + } + + public void setStartTime(long startTime) { + this.startTime = startTime; + } + + public long getEndTime() { + return endTime; + } + + public void setEndTime(long endTime) { + this.endTime = endTime; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRtpServerTimeoutEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRtpServerTimeoutEvent.java new file mode 100644 index 0000000..b700dd5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRtpServerTimeoutEvent.java @@ -0,0 +1,22 @@ +package com.genersoft.iot.vmp.media.event.media; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamChangedHookParam; + +/** + * RtpServer收流超时事件 + */ +public class MediaRtpServerTimeoutEvent extends MediaEvent { + public MediaRtpServerTimeoutEvent(Object source) { + super(source); + } + + public static MediaRtpServerTimeoutEvent getInstance(Object source, OnStreamChangedHookParam hookParam, MediaServer mediaServer){ + MediaRtpServerTimeoutEvent mediaDepartureEven = new MediaRtpServerTimeoutEvent(source); + mediaDepartureEven.setApp(hookParam.getApp()); + mediaDepartureEven.setStream(hookParam.getStream()); + mediaDepartureEven.setSchema(hookParam.getSchema()); + mediaDepartureEven.setMediaServer(mediaServer); + return mediaDepartureEven; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaSendRtpStoppedEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaSendRtpStoppedEvent.java new file mode 100644 index 0000000..d37a4c2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaSendRtpStoppedEvent.java @@ -0,0 +1,52 @@ +package com.genersoft.iot.vmp.media.event.mediaServer; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamNotFoundHookParam; +import org.springframework.context.ApplicationEvent; + +/** + * 发送流停止事件 + */ +public class MediaSendRtpStoppedEvent extends ApplicationEvent { + public MediaSendRtpStoppedEvent(Object source) { + super(source); + } + + private String app; + + private String stream; + + private MediaServer mediaServer; + + public static MediaSendRtpStoppedEvent getInstance(Object source, OnStreamNotFoundHookParam hookParam, MediaServer mediaServer){ + MediaSendRtpStoppedEvent mediaDepartureEven = new MediaSendRtpStoppedEvent(source); + mediaDepartureEven.setApp(hookParam.getApp()); + mediaDepartureEven.setStream(hookParam.getStream()); + mediaDepartureEven.setMediaServer(mediaServer); + return mediaDepartureEven; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public MediaServer getMediaServer() { + return mediaServer; + } + + public void setMediaServer(MediaServer mediaServer) { + this.mediaServer = mediaServer; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerChangeEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerChangeEvent.java new file mode 100644 index 0000000..ecbe332 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerChangeEvent.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.media.event.mediaServer; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import org.springframework.context.ApplicationEvent; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class MediaServerChangeEvent extends ApplicationEvent { + + public MediaServerChangeEvent(Object source) { + super(source); + } + + private List mediaServerItemList; + + public List getMediaServerItemList() { + return mediaServerItemList; + } + + public void setMediaServerItemList(List mediaServerItemList) { + this.mediaServerItemList = mediaServerItemList; + } + + public void setMediaServerItemList(MediaServer... mediaServerItemArray) { + this.mediaServerItemList = new ArrayList<>(); + this.mediaServerItemList.addAll(Arrays.asList(mediaServerItemArray)); + } + + public void setMediaServerItem(List mediaServerItemList) { + this.mediaServerItemList = mediaServerItemList; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerDeleteEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerDeleteEvent.java new file mode 100755 index 0000000..a716ff0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerDeleteEvent.java @@ -0,0 +1,11 @@ +package com.genersoft.iot.vmp.media.event.mediaServer; + +/** + * zlm在线事件 + */ +public class MediaServerDeleteEvent extends MediaServerEventAbstract { + + public MediaServerDeleteEvent(Object source) { + super(source); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerEventAbstract.java b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerEventAbstract.java new file mode 100755 index 0000000..8f80345 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerEventAbstract.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.media.event.mediaServer; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; + + +public abstract class MediaServerEventAbstract extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 1L; + + @Getter + @Setter + private MediaServer mediaServer; + + + public MediaServerEventAbstract(Object source) { + super(source); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerOfflineEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerOfflineEvent.java new file mode 100755 index 0000000..2f9ac23 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerOfflineEvent.java @@ -0,0 +1,11 @@ +package com.genersoft.iot.vmp.media.event.mediaServer; + +/** + * zlm离线事件类 + */ +public class MediaServerOfflineEvent extends MediaServerEventAbstract { + + public MediaServerOfflineEvent(Object source) { + super(source); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerOnlineEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerOnlineEvent.java new file mode 100755 index 0000000..673dce4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerOnlineEvent.java @@ -0,0 +1,11 @@ +package com.genersoft.iot.vmp.media.event.mediaServer; + +/** + * zlm在线事件 + */ +public class MediaServerOnlineEvent extends MediaServerEventAbstract { + + public MediaServerOnlineEvent(Object source) { + super(source); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerStatusEventListener.java b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerStatusEventListener.java new file mode 100755 index 0000000..2e5a6ea --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerStatusEventListener.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.media.event.mediaServer; + +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + + +/** + * @description: 在线事件监听器,监听到离线后,修改设备离在线状态。 设备在线有两个来源: + * 1、设备主动注销,发送注销指令 + * 2、设备未知原因离线,心跳超时 + * @author: swwheihei + * @date: 2020年5月6日 下午1:51:23 + */ +@Slf4j +@Component +public class MediaServerStatusEventListener { + + @Autowired + private IPlayService playService; + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaServerOnlineEvent event) { + log.info("[媒体节点] 上线 ID:" + event.getMediaServer().getId()); + playService.zlmServerOnline(event.getMediaServer()); + } + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaServerOfflineEvent event) { + + log.info("[媒体节点] 离线,ID:" + event.getMediaServer().getId()); + // 处理ZLM离线 + playService.zlmServerOffline(event.getMediaServer()); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/service/IMediaNodeServerService.java b/src/main/java/com/genersoft/iot/vmp/media/service/IMediaNodeServerService.java new file mode 100644 index 0000000..b0a768c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/service/IMediaNodeServerService.java @@ -0,0 +1,91 @@ +package com.genersoft.iot.vmp.media.service; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; + +import java.util.List; +import java.util.Map; + +public interface IMediaNodeServerService { + int createRTPServer(MediaServer mediaServer, String streamId, long ssrc, Integer port, Boolean onlyAuto, Boolean disableAudio, Boolean reUsePort, Integer tcpMode); + + void closeRtpServer(MediaServer mediaServer, String streamId, CommonCallback callback); + + + int createJTTServer(MediaServer mediaServer, String streamId, Integer port, Boolean disableVideo, Boolean disableAudio, Integer tcpMode); + + void closeJTTServer(MediaServer mediaServer, String streamId, CommonCallback callback); + + + void closeStreams(MediaServer mediaServer, String app, String stream); + + Boolean updateRtpServerSSRC(MediaServer mediaServer, String stream, String ssrc); + + boolean checkNodeId(MediaServer mediaServer); + + void online(MediaServer mediaServer); + + MediaServer checkMediaServer(String ip, int port, String secret); + + boolean stopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc); + + boolean initStopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc); + + boolean deleteRecordDirectory(MediaServer mediaServer, String app, String stream, String date, String fileName); + + List getMediaList(MediaServer mediaServer, String app, String stream, String callId); + + Boolean connectRtpServer(MediaServer mediaServer, String address, int port, String stream); + + void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName); + + MediaInfo getMediaInfo(MediaServer mediaServer, String app, String stream); + + Boolean pauseRtpCheck(MediaServer mediaServer, String streamKey); + + Boolean resumeRtpCheck(MediaServer mediaServer, String streamKey); + + String getFfmpegCmd(MediaServer mediaServer, String cmdKey); + + WVPResult addStreamProxy(MediaServer mediaServer, String app, String stream, String url, boolean enableAudio, boolean enableMp4, String rtpType, Integer timeout); + + Boolean delFFmpegSource(MediaServer mediaServer, String streamKey); + + Boolean delStreamProxy(MediaServer mediaServer, String streamKey); + + Map getFFmpegCMDs(MediaServer mediaServer); + + Integer startSendRtpPassive(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout); + + void startSendRtpStream(MediaServer mediaServer, SendRtpInfo sendRtpItem); + + Integer startSendRtpTalk(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout); + + Long updateDownloadProcess(MediaServer mediaServer, String app, String stream); + + String startProxy(MediaServer mediaServer, StreamProxy streamProxy); + + void stopProxy(MediaServer mediaServer, String streamKey, String type); + + List listRtpServer(MediaServer mediaServer); + + void loadMP4FileForDate(MediaServer mediaServer, String app, String stream, String datePath, String dateDir, ErrorCallback callback); + + void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema); + + void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema); + + DownloadFileInfo getDownloadFilePath(MediaServer mediaServer, RecordInfo recordInfo); + + StreamInfo getStreamInfoByAppAndStream(MediaServer mediaServer, String app, String stream, MediaInfo mediaInfo, String addr, String callId, boolean isPlay); + + void loadMP4File(MediaServer mediaServer, String app, String stream, String filePath, String fileName, ErrorCallback callback); +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/service/IMediaServerService.java b/src/main/java/com/genersoft/iot/vmp/media/service/IMediaServerService.java new file mode 100755 index 0000000..4246030 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/service/IMediaServerService.java @@ -0,0 +1,176 @@ +package com.genersoft.iot.vmp.media.service; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.service.bean.*; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; + +import java.util.List; +import java.util.Map; + +/** + * 媒体服务节点 + */ +public interface IMediaServerService { + + List getAllOnlineList(); + + List getAll(); + + List getAllFromDatabase(); + + List getAllOnline(); + + MediaServer getOne(String generalMediaServerId); + + void syncCatchFromDatabase(); + + MediaServer getMediaServerForMinimumLoad(Boolean hasAssist); + + void updateVmServer(List mediaServerItemList); + + SSRCInfo openRTPServer(MediaServer mediaServerItem, String streamId, String presetSsrc, boolean ssrcCheck, + boolean isPlayback, Integer port, Boolean onlyAuto, Boolean disableAudio, Boolean reUsePort, Integer tcpMode); + + void closeRTPServer(MediaServer mediaServerItem, String streamId); + + void closeRTPServer(MediaServer mediaServerItem, String streamId, CommonCallback callback); + + SSRCInfo openJTTServer(MediaServer mediaServerItem, String streamId, Integer port, Boolean disableVideo, Boolean disableAudio, Integer tcpMode); + + void closeJTTServer(MediaServer mediaServerItem, String streamId, CommonCallback callback); + + Boolean updateRtpServerSSRC(MediaServer mediaServerItem, String streamId, String ssrc); + + void closeRTPServer(String mediaServerId, String streamId); + + void clearRTPServer(MediaServer mediaServerItem); + + void update(MediaServer mediaSerItem); + + void addCount(String mediaServerId); + + void removeCount(String mediaServerId); + + void releaseSsrc(String mediaServerItemId, String ssrc); + + void clearMediaServerForOnline(); + + void add(MediaServer mediaSerItem); + + void resetOnlineServerItem(MediaServer serverItem); + + MediaServer checkMediaServer(String ip, int port, String secret, String type); + + boolean checkMediaRecordServer(String ip, int port); + + void delete(MediaServer mediaServer); + + MediaServer getDefaultMediaServer(); + + MediaServerLoad getLoad(MediaServer mediaServerItem); + + List getAllWithAssistPort(); + + MediaServer getOneFromDatabase(String id); + + boolean stopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc); + + boolean initStopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc); + + boolean deleteRecordDirectory(MediaServer mediaServerItem, String app, String stream, String date, String fileName); + + List getMediaList(MediaServer mediaInfo, String app, String stream, String callId); + + Boolean connectRtpServer(MediaServer mediaServerItem, String address, int port, String stream); + + void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName); + + MediaInfo getMediaInfo(MediaServer mediaServerItem, String app, String stream); + + Boolean pauseRtpCheck(MediaServer mediaServerItem, String streamKey); + + boolean resumeRtpCheck(MediaServer mediaServerItem, String streamKey); + + String getFfmpegCmd(MediaServer mediaServer, String cmdKey); + + void closeStreams(MediaServer mediaServerItem, String app, String stream); + + WVPResult addStreamProxy(MediaServer mediaServerItem, String app, String stream, String url, boolean enableAudio, boolean enableMp4, String rtpType, Integer timeout); + + Boolean delFFmpegSource(MediaServer mediaServerItem, String streamKey); + + Boolean delStreamProxy(MediaServer mediaServerItem, String streamKey); + + Map getFFmpegCMDs(MediaServer mediaServer); + + /** + * 根据应用名和流ID获取播放地址, 通过zlm接口检查是否存在 + * @param app + * @param stream + * @return + */ + StreamInfo getStreamInfoByAppAndStreamWithCheck(String app, String stream, String mediaServerId,String addr, boolean authority); + + + /** + * 根据应用名和流ID获取播放地址, 通过zlm接口检查是否存在, 返回的ip使用远程访问ip,适用与zlm与wvp在一台主机的情况 + * @param app + * @param stream + * @return + */ + StreamInfo getStreamInfoByAppAndStreamWithCheck(String app, String stream, String mediaServerId, boolean authority); + + /** + * 根据应用名和流ID获取播放地址, 只是地址拼接 + * @param app + * @param stream + * @return + */ + StreamInfo getStreamInfoByAppAndStream(MediaServer mediaServerItem, String app, String stream, MediaInfo mediaInfo, String callId); + + /** + * 根据应用名和流ID获取播放地址, 只是地址拼接,返回的ip使用远程访问ip,适用与zlm与wvp在一台主机的情况 + * @param app + * @param stream + * @return + */ + StreamInfo getStreamInfoByAppAndStream(MediaServer mediaServer, String app, String stream, MediaInfo mediaInfo, String addr, String callId, boolean isPlay); + + Boolean isStreamReady(MediaServer mediaServer, String rtp, String streamId); + + Integer startSendRtpPassive(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout); + + Integer startSendRtpTalk(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout); + + void startSendRtp(MediaServer mediaServer, SendRtpInfo sendRtpItem); + + MediaServer getMediaServerByAppAndStream(String app, String stream); + + Long updateDownloadProcess(MediaServer mediaServerItem, String app, String stream); + + String startProxy(MediaServer mediaServer, StreamProxy streamProxy); + + void stopProxy(MediaServer mediaServer, String streamKey, String type); + + StreamInfo getMediaByAppAndStream(String app, String stream); + + int createRTPServer(MediaServer mediaServerItem, String streamId, long ssrc, Integer port, boolean onlyAuto, boolean disableAudio, boolean reUsePort, Integer tcpMode); + + List listRtpServer(MediaServer mediaServer); + + void loadMP4FileForDate(MediaServer mediaServer, String app, String stream, String datePath, String dateDir, ErrorCallback callback); + + void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema); + + void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema); + + DownloadFileInfo getDownloadFilePath(MediaServer mediaServer, RecordInfo recordInfo); + + void loadMP4File(MediaServer mediaServer, String app, String stream, String filePath, String fileName, ErrorCallback callback); +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/service/impl/MediaServerServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/media/service/impl/MediaServerServiceImpl.java new file mode 100755 index 0000000..8b1ee8c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/service/impl/MediaServerServiceImpl.java @@ -0,0 +1,1007 @@ +package com.genersoft.iot.vmp.media.service.impl; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.MediaConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerDeleteEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerOfflineEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerOnlineEvent; +import com.genersoft.iot.vmp.media.service.IMediaNodeServerService; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OriginType; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.MediaServerLoad; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.storager.dao.MediaServerMapper; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.EventListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.ObjectUtils; + +import java.time.LocalDateTime; +import java.util.*; + +/** + * 媒体服务器节点管理 + */ +@Slf4j +@Service +public class MediaServerServiceImpl implements IMediaServerService { + + @Autowired + private SSRCFactory ssrcFactory; + + @Autowired + private UserSetting userSetting; + + @Autowired + private MediaServerMapper mediaServerMapper; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private Map nodeServerServiceMap; + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + private MediaConfig mediaConfig; + + + /** + * 流到来的处理 + */ + @Async("taskExecutor") + @org.springframework.context.event.EventListener + public void onApplicationEvent(MediaArrivalEvent event) { + if ("rtsp".equals(event.getSchema())) { + log.info("流变化:注册 app->{}, stream->{}", event.getApp(), event.getStream()); + addCount(event.getMediaServer().getId()); + String type = OriginType.values()[event.getMediaInfo().getOriginType()].getType(); + redisCatchStorage.addStream(event.getMediaServer(), type, event.getApp(), event.getStream(), event.getMediaInfo()); + } + } + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + if ("rtsp".equals(event.getSchema())) { + log.info("流变化:注销, app->{}, stream->{}", event.getApp(), event.getStream()); + removeCount(event.getMediaServer().getId()); + MediaInfo mediaInfo = redisCatchStorage.getStreamInfo( + event.getApp(), event.getStream(), event.getMediaServer().getId()); + if (mediaInfo == null) { + return; + } + String type = OriginType.values()[mediaInfo.getOriginType()].getType(); + redisCatchStorage.removeStream(mediaInfo.getMediaServer().getId(), type, event.getApp(), event.getStream()); + } + } + + /** + * 流媒体节点上线 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaServerOnlineEvent event) { + // 查看是否有未处理的RTP流 + + } + + /** + * 流媒体节点离线 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaServerOfflineEvent event) { + + } + + + /** + * 初始化 + */ + @Override + public void updateVmServer(List mediaServerList) { + log.info("[媒体服务节点] 缓存初始化 "); + for (MediaServer mediaServer : mediaServerList) { + if (ObjectUtils.isEmpty(mediaServer.getId())) { + continue; + } + // 更新 + if (!ssrcFactory.hasMediaServerSSRC(mediaServer.getId())) { + ssrcFactory.initMediaServerSSRC(mediaServer.getId(), null); + } + // 查询redis是否存在此mediaServer + String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId(); + Boolean hasKey = redisTemplate.hasKey(key); + if (hasKey != null && !hasKey) { + redisTemplate.opsForHash().put(key, mediaServer.getId(), mediaServer); + } + } + } + + + @Override + public SSRCInfo openRTPServer(MediaServer mediaServer, String streamId, String presetSsrc, boolean ssrcCheck, + boolean isPlayback, Integer port, Boolean onlyAuto, Boolean disableAudio, Boolean reUsePort, Integer tcpMode) { + if (mediaServer == null || mediaServer.getId() == null) { + log.info("[openRTPServer] 失败, mediaServer == null || mediaServer.getId() == null"); + return null; + } + // 获取mediaServer可用的ssrc + String ssrc; + if (presetSsrc != null) { + ssrc = presetSsrc; + }else { + if (isPlayback) { + ssrc = ssrcFactory.getPlayBackSsrc(mediaServer.getId()); + }else { + ssrc = ssrcFactory.getPlaySsrc(mediaServer.getId()); + } + } + + if (streamId == null) { + streamId = String.format("%08x", Long.parseLong(ssrc)).toUpperCase(); + } + if (ssrcCheck && tcpMode > 0) { + // 目前zlm不支持 tcp模式更新ssrc,暂时关闭ssrc校验 + log.warn("[openRTPServer] 平台对接时下级可能自定义ssrc,但是tcp模式zlm收流目前无法更新ssrc,可能收流超时,此时请使用udp收流或者关闭ssrc校验"); + } + int rtpServerPort; + if (mediaServer.isRtpEnable()) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[openRTPServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return null; + } + rtpServerPort = mediaNodeServerService.createRTPServer(mediaServer, streamId, ssrcCheck ? Long.parseLong(ssrc) : 0, port, onlyAuto, disableAudio, reUsePort, tcpMode); + } else { + rtpServerPort = mediaServer.getRtpProxyPort(); + } + return new SSRCInfo(rtpServerPort, ssrc, "rtp", streamId, null); + } + + @Override + public int createRTPServer(MediaServer mediaServer, String streamId, long ssrc, Integer port, boolean onlyAuto, boolean disableAudio, boolean reUsePort, Integer tcpMode) { + int rtpServerPort; + if (mediaServer.isRtpEnable()) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[openRTPServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return 0; + } + rtpServerPort = mediaNodeServerService.createRTPServer(mediaServer, streamId, ssrc, port, onlyAuto, disableAudio, reUsePort, tcpMode); + } else { + rtpServerPort = mediaServer.getRtpProxyPort(); + } + return rtpServerPort; + } + + @Override + public SSRCInfo openJTTServer(MediaServer mediaServer, @NotNull String streamId, Integer port, Boolean disableVideo, Boolean disableAudio, Integer tcpMode) { + if (mediaServer == null || mediaServer.getId() == null) { + log.info("[openJTTServer] 失败, mediaServer == null || mediaServer.getId() == null"); + return null; + } + + int rtpServerPort; + if (mediaServer.isRtpEnable()) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[openJTTServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return null; + } + rtpServerPort = mediaNodeServerService.createJTTServer(mediaServer, streamId, port, disableVideo, disableAudio, tcpMode); + } else { + rtpServerPort = mediaServer.getJttProxyPort(); + } + return new SSRCInfo(rtpServerPort, null, "1078", streamId, null); + } + + @Override + public List listRtpServer(MediaServer mediaServer) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[openRTPServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return new ArrayList<>(); + } + return mediaNodeServerService.listRtpServer(mediaServer); + } + + @Override + public void closeRTPServer(MediaServer mediaServer, String streamId) { + if (mediaServer == null) { + return; + } + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[closeRTPServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return; + } + mediaNodeServerService.closeRtpServer(mediaServer, streamId, null); + } + + @Override + public void closeRTPServer(MediaServer mediaServer, String streamId, CommonCallback callback) { + if (mediaServer == null) { + callback.run(false); + return; + } + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[closeRTPServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return; + } + mediaNodeServerService.closeRtpServer(mediaServer, streamId, callback); + } + + @Override + public void closeRTPServer(String mediaServerId, String streamId) { + MediaServer mediaServer = this.getOne(mediaServerId); + if (mediaServer == null) { + return; + } + if (mediaServer.isRtpEnable()) { + closeRTPServer(mediaServer, streamId); + } + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[closeRTPServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return; + } + mediaNodeServerService.closeStreams(mediaServer, "rtp", streamId); + } + + @Override + public void closeJTTServer(MediaServer mediaServer, String streamId, CommonCallback callback) { + if (mediaServer == null) { + callback.run(false); + return; + } + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[closeJTTServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return; + } + mediaNodeServerService.closeJTTServer(mediaServer, streamId, callback); + } + + @Override + public Boolean updateRtpServerSSRC(MediaServer mediaServer, String streamId, String ssrc) { + if (mediaServer == null) { + return false; + } + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[updateRtpServerSSRC] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return false; + } + return mediaNodeServerService.updateRtpServerSSRC(mediaServer, streamId, ssrc); + } + + @Override + public void releaseSsrc(String mediaServerId, String ssrc) { + MediaServer mediaServer = getOne(mediaServerId); + if (mediaServer == null || ssrc == null) { + return; + } + ssrcFactory.releaseSsrc(mediaServerId, ssrc); + } + + /** + * 媒体服务节点 重启后重置他的推流信息, TODO 给正在使用的设备发送停止命令 + */ + @Override + public void clearRTPServer(MediaServer mediaServer) { + ssrcFactory.reset(mediaServer.getId()); + } + + @Override + public void update(MediaServer mediaSerItem) { + mediaServerMapper.update(mediaSerItem); + MediaServer mediaServerInRedis = getOne(mediaSerItem.getId()); + // 获取完整数据 + MediaServer mediaServerInDataBase = mediaServerMapper.queryOne(mediaSerItem.getId(), userSetting.getServerId()); + if (mediaServerInDataBase == null) { + return; + } + mediaServerInDataBase.setStatus(mediaSerItem.isStatus()); + if (mediaServerInRedis == null || !ssrcFactory.hasMediaServerSSRC(mediaServerInDataBase.getId())) { + ssrcFactory.initMediaServerSSRC(mediaServerInDataBase.getId(),null); + } + if (mediaSerItem.getSecret() != null && !mediaServerInDataBase.getSecret().equals(mediaSerItem.getSecret())) { + mediaServerInDataBase.setSecret(mediaSerItem.getSecret()); + } + mediaServerInDataBase.setSecret(mediaSerItem.getSecret()); + String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId(); + redisTemplate.opsForHash().put(key, mediaServerInDataBase.getId(), mediaServerInDataBase); + if (mediaServerInDataBase.isStatus()) { + resetOnlineServerItem(mediaServerInDataBase); + } + } + + + @Override + public List getAllOnlineList() { + List result = new ArrayList<>(); + String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId(); + String onlineKey = VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(); + List values = redisTemplate.opsForHash().values(key); + for (Object value : values) { + if (Objects.isNull(value)) { + continue; + } + MediaServer mediaServer = (MediaServer) value; + // 检查状态 + Double aDouble = redisTemplate.opsForZSet().score(onlineKey, mediaServer.getId()); + if (aDouble != null) { + mediaServer.setStatus(true); + } + result.add(mediaServer); + } + result.sort((serverItem1, serverItem2)->{ + int sortResult = 0; + LocalDateTime localDateTime1 = LocalDateTime.parse(serverItem1.getCreateTime(), DateUtil.formatter); + LocalDateTime localDateTime2 = LocalDateTime.parse(serverItem2.getCreateTime(), DateUtil.formatter); + + sortResult = localDateTime1.compareTo(localDateTime2); + return sortResult; + }); + return result; + } + + @Override + public List getAll() { + List mediaServerList = mediaServerMapper.queryAll(userSetting.getServerId()); + if (mediaServerList.isEmpty()) { + return new ArrayList<>(); + } + for (MediaServer mediaServer : mediaServerList) { + MediaServer mediaServerInRedis = getOne(mediaServer.getId()); + if (mediaServerInRedis != null) { + mediaServer.setStatus(mediaServerInRedis.isStatus()); + } + } + return mediaServerList; + } + + + @Override + public List getAllFromDatabase() { + return mediaServerMapper.queryAll(userSetting.getServerId()); + } + + @Override + public List getAllOnline() { + String key = VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(); + Set mediaServerIdSet = redisTemplate.opsForZSet().reverseRange(key, 0, -1); + + List result = new ArrayList<>(); + if (mediaServerIdSet != null && !mediaServerIdSet.isEmpty()) { + for (Object mediaServerId : mediaServerIdSet) { + String mediaServerIdStr = (String) mediaServerId; + String serverKey = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId(); + result.add((MediaServer) redisTemplate.opsForHash().get(serverKey, mediaServerIdStr)); + } + } + Collections.reverse(result); + return result; + } + + /** + * 获取单个媒体服务节点服务器 + * @param mediaServerId 服务id + * @return mediaServer + */ + @Override + public MediaServer getOne(String mediaServerId) { + if (mediaServerId == null) { + return null; + } + String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId(); + return (MediaServer) redisTemplate.opsForHash().get(key, mediaServerId); + } + + + @Override + public MediaServer getDefaultMediaServer() { + return mediaServerMapper.queryDefault(userSetting.getServerId()); + } + + @Override + public void clearMediaServerForOnline() { + String key = VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(); + redisTemplate.delete(key); + } + + @Override + public void add(MediaServer mediaServer) { + mediaServer.setCreateTime(DateUtil.getNow()); + mediaServer.setUpdateTime(DateUtil.getNow()); + if (mediaServer.getHookAliveInterval() == null || mediaServer.getHookAliveInterval() == 0F) { + mediaServer.setHookAliveInterval(10F); + } + if (mediaServer.getType() == null) { + log.info("[添加媒体节点] 失败, mediaServer的类型:为空"); + return; + } + if (mediaServerMapper.queryOne(mediaServer.getId(), userSetting.getServerId()) != null) { + log.info("[添加媒体节点] 失败, 媒体服务ID已存在,请修改媒体服务器配置, {}", mediaServer.getId()); + throw new ControllerException(ErrorCode.ERROR100.getCode(),"保存失败,媒体服务ID [ " + mediaServer.getId() + " ] 已存在,请修改媒体服务器配置"); + } + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[添加媒体节点] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return; + } + + mediaServerMapper.add(mediaServer); + if (mediaServer.isStatus()) { + mediaNodeServerService.online(mediaServer); + } + } + + @Override + public void resetOnlineServerItem(MediaServer serverItem) { + // 更新缓存 + String key = VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(); + // 使用zset的分数作为当前并发量, 默认值设置为0 + if (redisTemplate.opsForZSet().score(key, serverItem.getId()) == null) { // 不存在则设置默认值 已存在则重置 + redisTemplate.opsForZSet().add(key, serverItem.getId(), 0L); + // 查询服务流数量 + int count = getMediaList(serverItem); + redisTemplate.opsForZSet().add(key, serverItem.getId(), count); + }else { + clearRTPServer(serverItem); + } + } + + private int getMediaList(MediaServer serverItem) { + + return 0; + } + + + @Override + public void addCount(String mediaServerId) { + if (mediaServerId == null) { + return; + } + String key = VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(); + redisTemplate.opsForZSet().incrementScore(key, mediaServerId, 1); + + } + + @Override + public void removeCount(String mediaServerId) { + String key = VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(); + redisTemplate.opsForZSet().incrementScore(key, mediaServerId, - 1); + } + + /** + * 获取负载最低的节点 + * @return mediaServer + */ + @Override + public MediaServer getMediaServerForMinimumLoad(Boolean hasAssist) { + String key = VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(); + Long size = redisTemplate.opsForZSet().zCard(key); + if (size == null || size == 0) { + log.info("获取负载最低的节点时无在线节点"); + return null; + } + + // 获取分数最低的,及并发最低的 + Set objects = redisTemplate.opsForZSet().range(key, 0, -1); + ArrayList mediaServerObjectS = new ArrayList<>(objects); + MediaServer mediaServer = null; + if (hasAssist == null) { + String mediaServerId = (String)mediaServerObjectS.get(0); + mediaServer = getOne(mediaServerId); + }else if (hasAssist) { + for (Object mediaServerObject : mediaServerObjectS) { + String mediaServerId = (String)mediaServerObject; + MediaServer serverItem = getOne(mediaServerId); + if (serverItem.getRecordAssistPort() > 0) { + mediaServer = serverItem; + break; + } + } + }else if (!hasAssist) { + for (Object mediaServerObject : mediaServerObjectS) { + String mediaServerId = (String)mediaServerObject; + MediaServer serverItem = getOne(mediaServerId); + if (serverItem.getRecordAssistPort() == 0) { + mediaServer = serverItem; + break; + } + } + } + + return mediaServer; + } + + @Override + public MediaServer checkMediaServer(String ip, int port, String secret, String type) { + if (mediaServerMapper.queryOneByHostAndPort(ip, port, userSetting.getServerId()) != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "此连接已存在"); + } + + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(type); + if (mediaNodeServerService == null) { + log.info("[closeRTPServer] 失败, mediaServer的类型: {},未找到对应的实现类", type); + return null; + } + MediaServer mediaServer = mediaNodeServerService.checkMediaServer(ip, port, secret); + if (mediaServer != null) { + if (mediaServerMapper.queryOne(mediaServer.getId(), userSetting.getServerId()) != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "媒体服务ID [" + mediaServer.getId() + " ] 已存在,请修改媒体服务器配置"); + } + } + return mediaServer; + } + + @Override + public boolean checkMediaRecordServer(String ip, int port) { + boolean result = false; + OkHttpClient client = new OkHttpClient(); + String url = String.format("http://%s:%s/index/api/record", ip, port); + Request request = new Request.Builder() + .get() + .url(url) + .build(); + try { + Response response = client.newCall(request).execute(); + if (response != null) { + result = true; + } + } catch (Exception e) {} + + return result; + } + + @Override + public void delete(MediaServer mediaServer) { + mediaServerMapper.delOne(mediaServer.getId(), userSetting.getServerId()); + redisTemplate.opsForZSet().remove(VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(), mediaServer.getId()); + String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId() + ":" + mediaServer.getId(); + redisTemplate.delete(key); + // 发送节点移除通知 + MediaServerDeleteEvent event = new MediaServerDeleteEvent(this); + event.setMediaServer(mediaServer); + applicationEventPublisher.publishEvent(event); + } + + @Override + public MediaServer getOneFromDatabase(String mediaServerId) { + return mediaServerMapper.queryOne(mediaServerId, userSetting.getServerId()); + } + + @Override + public void syncCatchFromDatabase() { + List allInCatch = getAllOnlineList(); + List allInDatabase = mediaServerMapper.queryAll(userSetting.getServerId()); + Map mediaServerMap = new HashMap<>(); + + for (MediaServer mediaServer : allInDatabase) { + mediaServerMap.put(mediaServer.getId(), mediaServer); + } + for (MediaServer mediaServer : allInCatch) { + // 清除数据中不存在但redis缓存数据 + if (!mediaServerMap.containsKey(mediaServer.getId())) { + delete(mediaServer); + } + } + } + + @Override + public MediaServerLoad getLoad(MediaServer mediaServer) { + MediaServerLoad result = new MediaServerLoad(); + result.setId(mediaServer.getId()); + result.setPush(redisCatchStorage.getPushStreamCount(mediaServer.getId())); + result.setProxy(redisCatchStorage.getProxyStreamCount(mediaServer.getId())); + + result.setGbReceive(inviteStreamService.getStreamInfoCount(mediaServer.getId())); + result.setGbSend(redisCatchStorage.getGbSendCount(mediaServer.getId())); + return result; + } + + @Override + public List getAllWithAssistPort() { + return mediaServerMapper.queryAllWithAssistPort(userSetting.getServerId()); + } + + + @Override + public boolean stopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaInfo.getType()); + if (mediaNodeServerService == null) { + log.info("[stopSendRtp] 失败, mediaServer的类型: {},未找到对应的实现类", mediaInfo.getType()); + return false; + } + return mediaNodeServerService.stopSendRtp(mediaInfo, app, stream, ssrc); + } + + @Override + public boolean initStopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaInfo.getType()); + if (mediaNodeServerService == null) { + log.info("[stopSendRtp] 失败, mediaServer的类型: {},未找到对应的实现类", mediaInfo.getType()); + return false; + } + return mediaNodeServerService.initStopSendRtp(mediaInfo, app, stream, ssrc); + } + + @Override + public boolean deleteRecordDirectory(MediaServer mediaServer, String app, String stream, String date, String fileName) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[stopSendRtp] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return false; + } + return mediaNodeServerService.deleteRecordDirectory(mediaServer, app, stream, date, fileName); + } + + @Override + public List getMediaList(MediaServer mediaServer, String app, String stream, String callId) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[getMediaList] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return new ArrayList<>(); + } + return mediaNodeServerService.getMediaList(mediaServer, app, stream, callId); + } + + @Override + public Boolean connectRtpServer(MediaServer mediaServer, String address, int port, String stream) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[connectRtpServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return false; + } + return mediaNodeServerService.connectRtpServer(mediaServer, address, port, stream); + } + + @Override + public void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[getSnap] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return; + } + mediaNodeServerService.getSnap(mediaServer, app, stream, timeoutSec, expireSec, path, fileName); + } + + @Override + public MediaInfo getMediaInfo(MediaServer mediaServer, String app, String stream) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[getMediaInfo] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return null; + } + return mediaNodeServerService.getMediaInfo(mediaServer, app, stream); + } + + @Override + public Boolean pauseRtpCheck(MediaServer mediaServer, String streamKey) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[pauseRtpCheck] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return false; + } + return mediaNodeServerService.pauseRtpCheck(mediaServer, streamKey); + } + + @Override + public boolean resumeRtpCheck(MediaServer mediaServer, String streamKey) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[pauseRtpCheck] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return false; + } + return mediaNodeServerService.resumeRtpCheck(mediaServer, streamKey); + } + + @Override + public String getFfmpegCmd(MediaServer mediaServer, String cmdKey) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[getFfmpegCmd] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return null; + } + return mediaNodeServerService.getFfmpegCmd(mediaServer, cmdKey); + } + + @Override + public void closeStreams(MediaServer mediaServer, String app, String stream) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[closeStreams] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return; + } + mediaNodeServerService.closeStreams(mediaServer, app, stream); + } + + @Override + public WVPResult addStreamProxy(MediaServer mediaServer, String app, String stream, String url, + boolean enableAudio, boolean enableMp4, String rtpType, Integer timeout) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[addStreamProxy] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return WVPResult.fail(ErrorCode.ERROR400); + } + return mediaNodeServerService.addStreamProxy(mediaServer, app, stream, url, enableAudio, enableMp4, rtpType, timeout); + } + + @Override + public Boolean delFFmpegSource(MediaServer mediaServer, String streamKey) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[delFFmpegSource] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return false; + } + return mediaNodeServerService.delFFmpegSource(mediaServer, streamKey); + } + + @Override + public Boolean delStreamProxy(MediaServer mediaServerItem, String streamKey) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServerItem.getType()); + if (mediaNodeServerService == null) { + log.info("[delStreamProxy] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServerItem.getType()); + return false; + } + return mediaNodeServerService.delStreamProxy(mediaServerItem, streamKey); + } + + @Override + public Map getFFmpegCMDs(MediaServer mediaServer) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[getFFmpegCMDs] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return new HashMap<>(); + } + return mediaNodeServerService.getFFmpegCMDs(mediaServer); + } + + @Override + public StreamInfo getStreamInfoByAppAndStream(MediaServer mediaServerItem, String app, String stream, MediaInfo mediaInfo, String callId) { + return getStreamInfoByAppAndStream(mediaServerItem, app, stream, mediaInfo, null, callId, true); + } + + @Override + public StreamInfo getStreamInfoByAppAndStreamWithCheck(String app, String stream, String mediaServerId, String addr, boolean authority) { + if (mediaServerId == null) { + mediaServerId = mediaConfig.getId(); + } + MediaServer mediaInfo = getOne(mediaServerId); + if (mediaInfo == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到使用的媒体节点"); + } + String calld = null; + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(app, stream); + if (streamAuthorityInfo != null) { + calld = streamAuthorityInfo.getCallId(); + } + List streamInfoList = getMediaList(mediaInfo, app, stream, calld); + if (streamInfoList == null || streamInfoList.isEmpty()) { + return null; + }else { + StreamInfo streamInfo = streamInfoList.get(0); + if (addr != null && !addr.isEmpty()) { + streamInfo.changeStreamIp(addr); + } + return streamInfo; + } + } + + + + @Override + public StreamInfo getStreamInfoByAppAndStreamWithCheck(String app, String stream, String mediaServerId, boolean authority) { + return getStreamInfoByAppAndStreamWithCheck(app, stream, mediaServerId, null, authority); + } + + @Override + public StreamInfo getStreamInfoByAppAndStream(MediaServer mediaServer, String app, String stream, MediaInfo mediaInfo, String addr, String callId, boolean isPlay) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[getStreamInfoByAppAndStream] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return null; + } + return mediaNodeServerService.getStreamInfoByAppAndStream(mediaServer, app, stream, mediaInfo, addr, callId, isPlay); + } + + @Override + public Boolean isStreamReady(MediaServer mediaServer, String app, String streamId) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[isStreamReady] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return false; + } + MediaInfo mediaInfo = mediaNodeServerService.getMediaInfo(mediaServer, app, streamId); + return mediaInfo != null; + } + + @Override + public Integer startSendRtpPassive(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[startSendRtpPassive] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + return mediaNodeServerService.startSendRtpPassive(mediaServer, sendRtpItem, timeout); + } + + @Override + public Integer startSendRtpTalk(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[startSendRtpPassive] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + return mediaNodeServerService.startSendRtpTalk(mediaServer, sendRtpItem, timeout); + } + + @Override + public void startSendRtp(MediaServer mediaServer, SendRtpInfo sendRtpItem) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[startSendRtpStream] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + sendRtpItem.setRtcp(true); + + log.info("[开始推流] {}/{}, 目标={}:{},SSRC={}, RTCP={}", sendRtpItem.getApp(), sendRtpItem.getStream(), + sendRtpItem.getIp(), sendRtpItem.getPort(), sendRtpItem.getSsrc(), sendRtpItem.isRtcp()); + mediaNodeServerService.startSendRtpStream(mediaServer, sendRtpItem); + } + + + + @Override + public MediaServer getMediaServerByAppAndStream(String app, String stream) { + List mediaServerList = getAll(); + for (MediaServer mediaServer : mediaServerList) { + MediaInfo mediaInfo = getMediaInfo(mediaServer, app, stream); + if (mediaInfo != null) { + return mediaServer; + } + } + return null; + } + + @Override + public StreamInfo getMediaByAppAndStream(String app, String stream) { + + List mediaServerList = getAll(); + for (MediaServer mediaServer : mediaServerList) { + MediaInfo mediaInfo = getMediaInfo(mediaServer, app, stream); + if (mediaInfo != null) { + return getStreamInfoByAppAndStream(mediaServer, app, stream, mediaInfo, mediaInfo.getCallId()); + } + } + return null; + } + + @Override + public Long updateDownloadProcess(MediaServer mediaServer, String app, String stream) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[updateDownloadProcess] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + return mediaNodeServerService.updateDownloadProcess(mediaServer, app, stream); + } + + @Override + public String startProxy(MediaServer mediaServer, StreamProxy streamProxy) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[startProxy] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + return mediaNodeServerService.startProxy(mediaServer, streamProxy); + } + + @Override + public void stopProxy(MediaServer mediaServer, String streamKey, String type) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[stopProxy] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + mediaNodeServerService.stopProxy(mediaServer, streamKey, type); + } + + @Override + public void loadMP4FileForDate(MediaServer mediaServer, String app, String stream, String date, String dateDir, ErrorCallback callback) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[loadMP4FileForDate] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + mediaNodeServerService.loadMP4FileForDate(mediaServer, app, stream, date, dateDir, callback); + + } + + @Override + public void loadMP4File(MediaServer mediaServer, String app, String stream, String filePath, String fileName, ErrorCallback callback) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[loadMP4File] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + mediaNodeServerService.loadMP4File(mediaServer, app, stream, filePath, fileName, callback); + } + + @Override + public void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[seekRecordStamp] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + mediaNodeServerService.seekRecordStamp(mediaServer, app, stream, stamp, schema); + } + + @Override + public void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[setRecordSpeed] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + mediaNodeServerService.setRecordSpeed(mediaServer, app, stream, speed, schema); + } + + @Override + public DownloadFileInfo getDownloadFilePath(MediaServer mediaServer, RecordInfo recordInfo) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[setRecordSpeed] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + return mediaNodeServerService.getDownloadFilePath(mediaServer, recordInfo); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/AssistRESTfulUtils.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/AssistRESTfulUtils.java new file mode 100644 index 0000000..23b8b37 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/AssistRESTfulUtils.java @@ -0,0 +1,288 @@ +package com.genersoft.iot.vmp.media.zlm; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.utils.SSLSocketClientUtil; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import okhttp3.logging.HttpLoggingInterceptor; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.net.ssl.X509TrustManager; +import java.io.IOException; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class AssistRESTfulUtils { + + private OkHttpClient client; + + + public interface RequestCallback{ + void run(JSONObject response); + } + + private OkHttpClient getClient(){ + return getClient(null); + } + + private OkHttpClient getClient(Integer readTimeOut){ + if (client == null) { + if (readTimeOut == null) { + readTimeOut = 10; + } + OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); + // 设置连接超时时间 + httpClientBuilder.connectTimeout(8, TimeUnit.SECONDS); + // 设置读取超时时间 + httpClientBuilder.readTimeout(readTimeOut,TimeUnit.SECONDS); + // 设置连接池 + httpClientBuilder.connectionPool(new ConnectionPool(16, 5, TimeUnit.MINUTES)); + if (log.isDebugEnabled()) { + HttpLoggingInterceptor logging = new HttpLoggingInterceptor(message -> { + log.debug("http请求参数:" + message); + }); + logging.setLevel(HttpLoggingInterceptor.Level.BASIC); + // OkHttp進行添加攔截器loggingInterceptor + httpClientBuilder.addInterceptor(logging); + } + X509TrustManager manager = SSLSocketClientUtil.getX509TrustManager(); + // 设置ssl + httpClientBuilder.sslSocketFactory(SSLSocketClientUtil.getSocketFactory(manager), manager); + httpClientBuilder.hostnameVerifier(SSLSocketClientUtil.getHostnameVerifier());//忽略校验 + client = httpClientBuilder.build(); + } + return client; + + } + + + public JSONObject sendGet(MediaServer mediaServerItem, String api, Map param, RequestCallback callback) { + OkHttpClient client = getClient(); + + if (mediaServerItem == null) { + return null; + } + if (mediaServerItem.getRecordAssistPort() <= 0) { + log.warn("未启用Assist服务"); + return null; + } + StringBuilder stringBuffer = new StringBuilder(); + stringBuffer.append(api); + JSONObject responseJSON = null; + + if (param != null && !param.keySet().isEmpty()) { + stringBuffer.append("?"); + int index = 1; + for (String key : param.keySet()){ + if (param.get(key) != null) { + stringBuffer.append(key + "=" + param.get(key)); + if (index < param.size()) { + stringBuffer.append("&"); + } + } + index++; + } + } + + String url = stringBuffer.toString(); + log.info("[访问assist]: {}", url); + Request request = new Request.Builder() + .get() + .url(url) + .build(); + if (callback == null) { + try { + Response response = client.newCall(request).execute(); + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + String responseStr = responseBody.string(); + responseJSON = JSON.parseObject(responseStr); + } + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + } catch (ConnectException e) { + log.error(String.format("连接Assist失败: %s, %s", e.getCause().getMessage(), e.getMessage())); + log.info("请检查media配置并确认Assist已启动..."); + }catch (IOException e) { + log.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + }else { + client.newCall(request).enqueue(new Callback(){ + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response){ + if (response.isSuccessful()) { + try { + String responseStr = Objects.requireNonNull(response.body()).string(); + callback.run(JSON.parseObject(responseStr)); + } catch (IOException e) { + log.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + log.error(String.format("连接Assist失败: %s, %s", e.getCause().getMessage(), e.getMessage())); + log.info("请检查media配置并确认Assist已启动..."); + } + }); + } + + + + return responseJSON; + } + + public JSONObject sendPost(MediaServer mediaServerItem, String url, + JSONObject param, ZLMRESTfulUtils.RequestCallback callback, + Integer readTimeOut) { + OkHttpClient client = getClient(readTimeOut); + + if (mediaServerItem == null) { + return null; + } + log.info("[访问assist]: {}, 参数: {}", url, param); + JSONObject responseJSON = new JSONObject(); + //-2自定义流媒体 调用错误码 + responseJSON.put("code",-2); + responseJSON.put("msg","ASSIST调用失败"); + + RequestBody requestBodyJson = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), param.toString()); + + Request request = new Request.Builder() + .post(requestBodyJson) + .url(url) + .addHeader("Content-Type", "application/json") + .build(); + if (callback == null) { + try { + Response response = client.newCall(request).execute(); + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + String responseStr = responseBody.string(); + responseJSON = JSON.parseObject(responseStr); + } + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + }catch (IOException e) { + log.error(String.format("[ %s ]ASSIST请求失败: %s", url, e.getMessage())); + + if(e instanceof SocketTimeoutException){ + //读取超时超时异常 + log.error(String.format("读取ASSIST数据失败: %s, %s", url, e.getMessage())); + } + if(e instanceof ConnectException){ + //判断连接异常,我这里是报Failed to connect to 10.7.5.144 + log.error(String.format("连接ASSIST失败: %s, %s", url, e.getMessage())); + } + + }catch (Exception e){ + log.error(String.format("访问ASSIST失败: %s, %s", url, e.getMessage())); + } + }else { + client.newCall(request).enqueue(new Callback(){ + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response){ + if (response.isSuccessful()) { + try { + String responseStr = Objects.requireNonNull(response.body()).string(); + callback.run(responseStr); + } catch (IOException e) { + log.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + log.error(String.format("连接ZLM失败: %s, %s", call.request().toString(), e.getMessage())); + + if(e instanceof SocketTimeoutException){ + //读取超时超时异常 + log.error(String.format("读取ZLM数据失败: %s, %s", call.request().toString(), e.getMessage())); + } + if(e instanceof ConnectException){ + //判断连接异常,我这里是报Failed to connect to 10.7.5.144 + log.error(String.format("连接ZLM失败: %s, %s", call.request().toString(), e.getMessage())); + } + } + }); + } + + + + return responseJSON; + } + + public JSONObject getInfo(MediaServer mediaServerItem, RequestCallback callback){ + Map param = new HashMap<>(); + return sendGet(mediaServerItem, "api/record/info",param, callback); + } + + public JSONObject addTask(MediaServer mediaServerItem, String app, String stream, String startTime, + String endTime, String callId, List filePathList, String remoteHost) { + + JSONObject videoTaskInfoJSON = new JSONObject(); + videoTaskInfoJSON.put("app", app); + videoTaskInfoJSON.put("stream", stream); + videoTaskInfoJSON.put("startTime", startTime); + videoTaskInfoJSON.put("endTime", endTime); + videoTaskInfoJSON.put("callId", callId); + videoTaskInfoJSON.put("filePathList", filePathList); + if (!ObjectUtils.isEmpty(remoteHost)) { + videoTaskInfoJSON.put("remoteHost", remoteHost); + } + String urlStr = String.format("%s/api/record/file/download/task/add", remoteHost);; + return sendPost(mediaServerItem, urlStr, videoTaskInfoJSON, null, 30); + } + + public JSONObject queryTaskList(MediaServer mediaServerItem, String app, String stream, String callId, + String taskId, Boolean isEnd, String scheme) { + Map param = new HashMap<>(); + if (!ObjectUtils.isEmpty(app)) { + param.put("app", app); + } + if (!ObjectUtils.isEmpty(stream)) { + param.put("stream", stream); + } + if (!ObjectUtils.isEmpty(callId)) { + param.put("callId", callId); + } + if (!ObjectUtils.isEmpty(taskId)) { + param.put("taskId", taskId); + } + if (!ObjectUtils.isEmpty(isEnd)) { + param.put("isEnd", isEnd); + } + String urlStr = String.format("%s://%s:%s/api/record/file/download/task/list", + scheme, mediaServerItem.getIp(), mediaServerItem.getRecordAssistPort());; + return sendGet(mediaServerItem, urlStr, param, null); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java new file mode 100755 index 0000000..605d301 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java @@ -0,0 +1,316 @@ +package com.genersoft.iot.vmp.media.zlm; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.ResultForOnPublish; +import com.genersoft.iot.vmp.media.event.media.*; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaSendRtpStoppedEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.ZLMServerConfig; +import com.genersoft.iot.vmp.media.zlm.dto.hook.*; +import com.genersoft.iot.vmp.media.zlm.event.HookZlmServerKeepaliveEvent; +import com.genersoft.iot.vmp.media.zlm.event.HookZlmServerStartEvent; +import com.genersoft.iot.vmp.service.IMediaService; +import com.genersoft.iot.vmp.utils.MediaServerUtils; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletRequest; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * @description:针对 ZLMediaServer的hook事件监听 + * @author: swwheihei + * @date: 2020年5月8日 上午10:46:48 + */ +@Slf4j +@RestController +@RequestMapping("/index/hook") +@Hidden +public class ZLMHttpHookListener { + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private IMediaService mediaService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + + /** + * 服务器定时上报时间,上报间隔可配置,默认10s上报一次 + */ + @ResponseBody + @PostMapping(value = "/on_server_keepalive", produces = "application/json;charset=UTF-8") + public HookResult onServerKeepalive(@RequestBody OnServerKeepaliveHookParam param) { + try { + HookZlmServerKeepaliveEvent event = new HookZlmServerKeepaliveEvent(this); + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + event.setMediaServerItem(mediaServerItem); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + log.info("[ZLM-HOOK-心跳] 发送通知失败 ", e); + } + return HookResult.SUCCESS(); + } + + /** + * 播放器鉴权事件,rtsp/rtmp/http-flv/ws-flv/hls的播放都将触发此鉴权事件。 + */ + @ResponseBody + @PostMapping(value = "/on_play", produces = "application/json;charset=UTF-8") + public HookResult onPlay(@RequestBody OnPlayHookParam param) { + + Map paramMap = MediaServerUtils.urlParamToMap(param.getParams()); + // 对于播放流进行鉴权 + boolean authenticateResult = mediaService.authenticatePlay(param.getApp(), param.getStream(), paramMap.get("callId")); + if (!authenticateResult) { + log.info("[ZLM HOOK] 播放鉴权 失败:{}->{}", param.getMediaServerId(), param); + return new HookResult(401, "Unauthorized"); + } + if (log.isDebugEnabled()){ + log.debug("[ZLM HOOK] 播放鉴权成功:{}->{}", param.getMediaServerId(), param); + } + return HookResult.SUCCESS(); + } + + /** + * rtsp/rtmp/rtp推流鉴权事件。 + */ + @ResponseBody + @PostMapping(value = "/on_publish", produces = "application/json;charset=UTF-8") + public HookResultForOnPublish onPublish(@RequestBody OnPublishHookParam param) { + + JSONObject json = (JSONObject) JSON.toJSON(param); + + log.info("[ZLM HOOK]推流鉴权:{}->{}", param.getMediaServerId(), param); + // TODO 加快处理速度 + + String mediaServerId = json.getString("mediaServerId"); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + HookResultForOnPublish fail = HookResultForOnPublish.Fail(); + log.warn("[ZLM HOOK]推流鉴权 响应:{}->找不到对应的mediaServer", param.getMediaServerId()); + return fail; + } + + ResultForOnPublish resultForOnPublish = mediaService.authenticatePublish(mediaServer, param.getApp(), param.getStream(), param.getParams()); + if (resultForOnPublish != null) { + HookResultForOnPublish successResult = HookResultForOnPublish.getInstance(resultForOnPublish); + log.info("[ZLM HOOK]推流鉴权 响应:{}->{}->>>>{}", param.getMediaServerId(), param, successResult); + return successResult; + }else { + HookResultForOnPublish fail = HookResultForOnPublish.Fail(); + log.info("[ZLM HOOK]推流鉴权 响应:{}->{}->>>>{}", param.getMediaServerId(), param, fail); + return fail; + } + } + + /** + * rtsp/rtmp流注册或注销时触发此事件;此事件对回复不敏感。 + */ + @ResponseBody + @PostMapping(value = "/on_stream_changed", produces = "application/json;charset=UTF-8") + public HookResult onStreamChanged(@RequestBody OnStreamChangedHookParam param) { + MediaServer mediaServer = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServer == null) { + return HookResult.SUCCESS(); + } + if (!ObjectUtils.isEmpty(mediaServer.getTranscodeSuffix()) + && !"null".equalsIgnoreCase(mediaServer.getTranscodeSuffix()) + && param.getStream().endsWith(mediaServer.getTranscodeSuffix()) ) { + return HookResult.SUCCESS(); + } + if (param.getSchema().equalsIgnoreCase("rtsp")) { + if (param.isRegist()) { + log.info("[ZLM HOOK] 流注册, {}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream()); + String queryParams = param.getParams(); + if (queryParams == null) { + try { + URL url = new URL("http" + param.getOriginUrl().substring(4)); + queryParams = url.getQuery(); + }catch (MalformedURLException ignored) {} + } + if (queryParams != null) { + param.setParamMap(MediaServerUtils.urlParamToMap(queryParams)); + }else { + param.setParamMap(new HashMap<>()); + } + MediaArrivalEvent mediaArrivalEvent = MediaArrivalEvent.getInstance(this, param, mediaServer, userSetting.getServerId()); + applicationEventPublisher.publishEvent(mediaArrivalEvent); + } else { + log.info("[ZLM HOOK] 流注销, {}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream()); + MediaDepartureEvent mediaDepartureEvent = MediaDepartureEvent.getInstance(this, param, mediaServer); + applicationEventPublisher.publishEvent(mediaDepartureEvent); + } + } + + return HookResult.SUCCESS(); + } + + /** + * 流无人观看时事件,用户可以通过此事件选择是否关闭无人看的流。 + */ + @ResponseBody + @PostMapping(value = "/on_stream_none_reader", produces = "application/json;charset=UTF-8") + public JSONObject onStreamNoneReader(@RequestBody OnStreamNoneReaderHookParam param) { + + log.info("[ZLM HOOK]流无人观看:{}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), + param.getApp(), param.getStream()); + + MediaServer mediaInfo = mediaServerService.getOne(param.getMediaServerId()); + if (mediaInfo == null) { + JSONObject ret = new JSONObject(); + ret.put("code", 0); + return ret; + } + if (mediaInfo.getTranscodeSuffix() != null && param.getStream().endsWith(mediaInfo.getTranscodeSuffix())) { + param.setStream(param.getStream().substring(0, param.getStream().lastIndexOf(mediaInfo.getTranscodeSuffix()) - 1)); + } + if (!ObjectUtils.isEmpty(mediaInfo.getTranscodeSuffix()) + && !"null".equalsIgnoreCase(mediaInfo.getTranscodeSuffix()) + && param.getStream().endsWith(mediaInfo.getTranscodeSuffix()) ) { + param.setStream(param.getStream().substring(0, param.getStream().lastIndexOf(mediaInfo.getTranscodeSuffix()) -1 )); + } + + JSONObject ret = new JSONObject(); + boolean close = mediaService.closeStreamOnNoneReader(param.getMediaServerId(), param.getApp(), param.getStream(), param.getSchema()); + log.info("[ZLM HOOK]流无人观看是否触发关闭:{}, {}->{}->{}/{}", close, param.getMediaServerId(), param.getSchema(), + param.getApp(), param.getStream()); + ret.put("code", 0); + ret.put("close", close); + return ret; + } + + /** + * 流未找到事件,用户可以在此事件触发时,立即去拉流,这样可以实现按需拉流;此事件对回复不敏感。 + */ + @ResponseBody + @PostMapping(value = "/on_stream_not_found", produces = "application/json;charset=UTF-8") + public HookResult onStreamNotFound(@RequestBody OnStreamNotFoundHookParam param) { + log.info("[ZLM HOOK] 流未找到:{}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream()); + + + MediaServer mediaServer = mediaServerService.getOne(param.getMediaServerId()); + if (!userSetting.getAutoApplyPlay() || mediaServer == null) { + return HookResult.SUCCESS(); + } + MediaNotFoundEvent mediaNotFoundEvent = MediaNotFoundEvent.getInstance(this, param, mediaServer); + applicationEventPublisher.publishEvent(mediaNotFoundEvent); + return HookResult.SUCCESS(); + } + + /** + * 服务器启动事件,可以用于监听服务器崩溃重启;此事件对回复不敏感。 + */ + @ResponseBody + @PostMapping(value = "/on_server_started", produces = "application/json;charset=UTF-8") + public HookResult onServerStarted(HttpServletRequest request, @RequestBody JSONObject jsonObject) { + + jsonObject.put("ip", request.getRemoteAddr()); + ZLMServerConfig zlmServerConfig = JSON.to(ZLMServerConfig.class, jsonObject); + zlmServerConfig.setIp(request.getRemoteAddr()); + log.info("[ZLM HOOK] zlm 启动 " + zlmServerConfig.getGeneralMediaServerId()); + try { + HookZlmServerStartEvent event = new HookZlmServerStartEvent(this); + MediaServer mediaServerItem = mediaServerService.getOne(zlmServerConfig.getMediaServerId()); + if (mediaServerItem != null) { + event.setMediaServerItem(mediaServerItem); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + log.info("[ZLM-HOOK-ZLM启动] 发送通知失败 ", e); + } + + return HookResult.SUCCESS(); + } + + /** + * 发送rtp(startSendRtp)被动关闭时回调 + */ + @ResponseBody + @PostMapping(value = "/on_send_rtp_stopped", produces = "application/json;charset=UTF-8") + public HookResult onSendRtpStopped(HttpServletRequest request, @RequestBody OnSendRtpStoppedHookParam param) { + + log.info("[ZLM HOOK] rtp发送关闭:{}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream()); + + // 查找对应的上级推流,发送停止 + if (!"rtp".equals(param.getApp())) { + return HookResult.SUCCESS(); + } + try { + MediaSendRtpStoppedEvent event = new MediaSendRtpStoppedEvent(this); + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + event.setMediaServer(mediaServerItem); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + log.info("[ZLM-HOOK-rtp发送关闭] 发送通知失败 ", e); + } + + return HookResult.SUCCESS(); + } + + /** + * rtpServer收流超时 + */ + @ResponseBody + @PostMapping(value = "/on_rtp_server_timeout", produces = "application/json;charset=UTF-8") + public HookResult onRtpServerTimeout(@RequestBody OnRtpServerTimeoutHookParam + param) { + log.info("[ZLM HOOK] rtpServer收流超时:{}->{}({})", param.getMediaServerId(), param.getStream_id(), param.getSsrc()); + + try { + MediaRtpServerTimeoutEvent event = new MediaRtpServerTimeoutEvent(this); + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + event.setMediaServer(mediaServerItem); + event.setApp("rtp"); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + log.info("[ZLM-HOOK-rtpServer收流超时] 发送通知失败 ", e); + } + + return HookResult.SUCCESS(); + } + + /** + * 录像完成事件 + */ + @ResponseBody + @PostMapping(value = "/on_record_mp4", produces = "application/json;charset=UTF-8") + public HookResult onRecordMp4(HttpServletRequest request, @RequestBody OnRecordMp4HookParam param) { + log.info("[ZLM HOOK] 录像完成:时长: {}, {}->{}",param.getTime_len(), param.getMediaServerId(), param.getFile_path()); + + try { + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + MediaRecordMp4Event event = MediaRecordMp4Event.getInstance(this, param, mediaServerItem); + event.setMediaServer(mediaServerItem); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + log.info("[ZLM-HOOK-rtpServer收流超时] 发送通知失败 ", e); + } + + return HookResult.SUCCESS(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaNodeServerService.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaNodeServerService.java new file mode 100644 index 0000000..628ee20 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaNodeServerService.java @@ -0,0 +1,690 @@ +package com.genersoft.iot.vmp.media.zlm; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.service.IMediaNodeServerService; +import com.genersoft.iot.vmp.media.zlm.dto.*; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.ObjectUtils; + +import java.util.*; + +@Slf4j +@Service("zlm") +public class ZLMMediaNodeServerService implements IMediaNodeServerService { + + + @Autowired + private ZLMRESTfulUtils zlmresTfulUtils; + + @Autowired + private ZLMServerFactory zlmServerFactory; + + @Autowired + private UserSetting userSetting; + + @Autowired + private HookSubscribe subscribe; + + @Override + public int createRTPServer(MediaServer mediaServer, String streamId, long ssrc, Integer port, Boolean onlyAuto, Boolean disableAudio, Boolean reUsePort, Integer tcpMode) { + return zlmServerFactory.createRTPServer(mediaServer, "rtp", streamId, ssrc, port, onlyAuto, disableAudio, reUsePort, tcpMode); + } + + @Override + public void closeRtpServer(MediaServer mediaServer, String streamId, CommonCallback callback) { + zlmServerFactory.closeRtpServer(mediaServer, streamId, callback); + } + + @Override + public int createJTTServer(MediaServer mediaServer, String streamId, Integer port, Boolean disableVideo, Boolean disableAudio, Integer tcpMode) { + return zlmServerFactory.createRTPServer(mediaServer, "1078", streamId, 0, port, disableVideo, disableAudio, false, tcpMode); + } + + @Override + public void closeJTTServer(MediaServer mediaServer, String streamId, CommonCallback callback) { + zlmServerFactory.closeRtpServer(mediaServer, streamId, callback); + } + + @Override + public void closeStreams(MediaServer mediaServer, String app, String stream) { + zlmresTfulUtils.closeStreams(mediaServer, app, stream); + } + + @Override + public Boolean updateRtpServerSSRC(MediaServer mediaServer, String streamId, String ssrc) { + return zlmServerFactory.updateRtpServerSSRC(mediaServer, streamId, ssrc); + } + + @Override + public boolean checkNodeId(MediaServer mediaServer) { + if (mediaServer == null) { + return false; + } + ZLMResult> mediaServerConfig = zlmresTfulUtils.getMediaServerConfig(mediaServer); + if (mediaServerConfig != null) { + List data = mediaServerConfig.getData(); + if (data != null && !data.isEmpty()) { + ZLMServerConfig zlmServerConfig= JSON.parseObject(JSON.toJSONString(data.get(0)), ZLMServerConfig.class); + return zlmServerConfig.getGeneralMediaServerId().equals(mediaServer.getId()); + }else { + return false; + } + + }else { + return false; + } + } + + @Override + public void online(MediaServer mediaServer) { + + } + + @Override + public MediaServer checkMediaServer(String ip, int port, String secret) { + MediaServer mediaServer = new MediaServer(); + mediaServer.setServerId(userSetting.getServerId()); + mediaServer.setIp(ip); + mediaServer.setHttpPort(port); + mediaServer.setSecret(secret); + ZLMResult> mediaServerConfigResult = zlmresTfulUtils.getMediaServerConfig(mediaServer); + if (mediaServerConfigResult == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "连接失败"); + } + List configList = mediaServerConfigResult.getData(); + if (configList == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "读取配置失败"); + } + ZLMServerConfig zlmServerConfig = JSON.parseObject(JSON.toJSONString(configList.get(0)), ZLMServerConfig.class); + if (zlmServerConfig == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "读取配置失败"); + } + mediaServer.setId(zlmServerConfig.getGeneralMediaServerId()); + mediaServer.setHttpSSlPort(zlmServerConfig.getHttpSSLport()); + mediaServer.setRtmpPort(zlmServerConfig.getRtmpPort()); + mediaServer.setRtmpSSlPort(zlmServerConfig.getRtmpSslPort()); + mediaServer.setRtspPort(zlmServerConfig.getRtspPort()); + mediaServer.setRtspSSLPort(zlmServerConfig.getRtspSSlport()); + mediaServer.setRtpProxyPort(zlmServerConfig.getRtpProxyPort()); + mediaServer.setStreamIp(ip); + + mediaServer.setHookIp("127.0.0.1"); + mediaServer.setSdpIp(ip); + mediaServer.setType("zlm"); + return mediaServer; + } + + @Override + public boolean stopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc) { + Map param = new HashMap<>(); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + if (!ObjectUtils.isEmpty(ssrc)) { + param.put("ssrc", ssrc); + } + ZLMResult zlmResult = zlmresTfulUtils.stopSendRtp(mediaInfo, param); + if (zlmResult.getCode() == 0) { + log.info("[停止发流] 成功: 参数:{}", JSON.toJSONString(param)); + return true; + }else { + log.info("停止发流结果: {}, 参数:{}", zlmResult.getMsg(), JSON.toJSONString(param)); + return false; + } + } + + @Override + public boolean initStopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc) { + Map param = new HashMap<>(); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + if (!ObjectUtils.isEmpty(ssrc)) { + param.put("ssrc", ssrc); + } + ZLMResult zlmResult = zlmresTfulUtils.stopSendRtp(mediaInfo, param); + if (zlmResult.getCode() != 0 ) { + log.error("停止发流失败: {}, 参数:{}", zlmResult.getMsg(), JSON.toJSONString(param)); + return false; + } + return true; + } + + @Override + public boolean deleteRecordDirectory(MediaServer mediaServer, String app, String stream, String date, String fileName) { + log.info("[zlm-deleteRecordDirectory] 删除磁盘文件, server: {} {}:{}->{}/{}", mediaServer.getId(), app, stream, date, fileName); + ZLMResult zlmResult = zlmresTfulUtils.deleteRecordDirectory(mediaServer, app, + stream, date, fileName); + if (zlmResult.getCode() == 0) { + return true; + }else { + log.info("[zlm-deleteRecordDirectory] 删除磁盘文件错误, server: {} {}:{}->{}/{}, 结果: {}", mediaServer.getId(), app, stream, date, fileName, zlmResult); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "删除磁盘文件失败"); + } + } + + @Override + public List getMediaList(MediaServer mediaServer, String app, String stream, String callId) { + List streamInfoList = new ArrayList<>(); + ZLMResult zlmResult = zlmresTfulUtils.getMediaList(mediaServer, app, stream); + if (zlmResult != null) { + if (zlmResult.getCode() == 0) { + if (zlmResult.getData() == null) { + return streamInfoList; + } + for (int i = 0; i < zlmResult.getData().size(); i++) { + JSONObject mediaJSON = zlmResult.getData().getJSONObject(0); + MediaInfo mediaInfo = MediaInfo.getInstance(mediaJSON, mediaServer, userSetting.getServerId()); + StreamInfo streamInfo = getStreamInfoByAppAndStream(mediaServer, mediaInfo.getApp(), + mediaInfo.getStream(), mediaInfo, null, callId, true); + if (streamInfo != null) { + streamInfoList.add(streamInfo); + } + } + } + } + return streamInfoList; + } + + @Override + public Boolean connectRtpServer(MediaServer mediaServer, String address, int port, String stream) { + ZLMResult zlmResult = zlmresTfulUtils.connectRtpServer(mediaServer, address, port, stream); + log.info("[TCP主动连接对方] 结果: {}", zlmResult); + return zlmResult.getCode() == 0; + } + + @Override + public void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName) { + String streamUrl; + if (mediaServer.getRtspPort() != 0) { + streamUrl = String.format("rtsp://127.0.0.1:%s/%s/%s", mediaServer.getRtspPort(), "rtp", stream); + } else { + streamUrl = String.format("http://127.0.0.1:%s/%s/%s.live.mp4", mediaServer.getHttpPort(), "rtp", stream); + } + zlmresTfulUtils.getSnap(mediaServer, streamUrl, timeoutSec, expireSec, path, fileName); + } + + @Override + public MediaInfo getMediaInfo(MediaServer mediaServer, String app, String stream) { + ZLMResult zlmResult = zlmresTfulUtils.getMediaInfo(mediaServer, app, "rtsp", stream); + if (zlmResult.getCode() != 0 || zlmResult.getData() == null || zlmResult.getData().getString("app") == null ) { + return null; + } + return MediaInfo.getInstance(zlmResult.getData(), mediaServer, userSetting.getServerId()); + } + + @Override + public Boolean pauseRtpCheck(MediaServer mediaServer, String streamKey) { + ZLMResult zlmResult = zlmresTfulUtils.pauseRtpCheck(mediaServer, streamKey); + return zlmResult.getCode() == 0; + } + + @Override + public Boolean resumeRtpCheck(MediaServer mediaServer, String streamKey) { + ZLMResult zlmResult = zlmresTfulUtils.resumeRtpCheck(mediaServer, streamKey); + return zlmResult.getCode() == 0; + } + + @Override + public String getFfmpegCmd(MediaServer mediaServer, String cmdKey) { + ZLMResult> mediaServerConfigResult = zlmresTfulUtils.getMediaServerConfig(mediaServer); + if (mediaServerConfigResult == null || mediaServerConfigResult.getCode() != 0) { + log.warn("[getFfmpegCmd] 获取流媒体配置失败"); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "获取流媒体配置失败"); + } + List data = mediaServerConfigResult.getData(); + JSONObject mediaServerConfig = data.get(0); + if (ObjectUtils.isEmpty(cmdKey)) { + cmdKey = "ffmpeg.cmd"; + } + return mediaServerConfig.getString(cmdKey); + } + + @Override + public WVPResult addStreamProxy(MediaServer mediaServer, String app, String stream, String url, + boolean enableAudio, boolean enableMp4, String rtpType, Integer timeout) { + ZLMResult zlmResult = zlmresTfulUtils.addStreamProxy(mediaServer, app, stream, url, enableAudio, enableMp4, rtpType, timeout); + if (zlmResult.getCode() != 0) { + return WVPResult.fail(ErrorCode.ERROR100.getCode(), "添加代理失败"); + }else { + StreamProxyResult data = zlmResult.getData(); + if (data == null) { + return WVPResult.fail(ErrorCode.ERROR100.getCode(), "代理结果异常"); + }else { + return WVPResult.success(data.getKey()); + } + } + } + + @Override + public Boolean delFFmpegSource(MediaServer mediaServer, String streamKey) { + ZLMResult flagDataZLMResult = zlmresTfulUtils.delFFmpegSource(mediaServer, streamKey); + return flagDataZLMResult != null && flagDataZLMResult.getCode() == 0; + } + + @Override + public Boolean delStreamProxy(MediaServer mediaServer, String streamKey) { + ZLMResult flagDataZLMResult = zlmresTfulUtils.delStreamProxy(mediaServer, streamKey); + return flagDataZLMResult != null && flagDataZLMResult.getCode() == 0; + } + + @Override + public Map getFFmpegCMDs(MediaServer mediaServer) { + Map result = new HashMap<>(); + ZLMResult> mediaServerConfigResult = zlmresTfulUtils.getMediaServerConfig(mediaServer); + if (mediaServerConfigResult != null && mediaServerConfigResult.getCode() == 0 + && !mediaServerConfigResult.getData().isEmpty()){ + JSONObject jsonObject = mediaServerConfigResult.getData().get(0); + + for (String key : jsonObject.keySet()) { + if (key.startsWith("ffmpeg.cmd")){ + result.put(key, jsonObject.getString(key)); + } + } + } + return result; + } + + @Override + public Integer startSendRtpPassive(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout) { + Map param = new HashMap<>(12); + param.put("vhost","__defaultVhost__"); + param.put("app", sendRtpItem.getApp()); + param.put("stream", sendRtpItem.getStream()); + param.put("ssrc", sendRtpItem.getSsrc()); + param.put("src_port", sendRtpItem.getLocalPort()); + param.put("pt", sendRtpItem.getPt()); + param.put("use_ps", sendRtpItem.isUsePs() ? "1" : "0"); + param.put("only_audio", sendRtpItem.isOnlyAudio() ? "1" : "0"); + param.put("is_udp", sendRtpItem.isTcp() ? "0" : "1"); + param.put("recv_stream_id", sendRtpItem.getReceiveStream()); + param.put("enable_origin_recv_limit", "1"); + if (timeout != null) { + param.put("close_delay_ms", timeout); + } + if (!sendRtpItem.isTcp()) { + // 开启rtcp保活 + param.put("udp_rtcp_timeout", sendRtpItem.isRtcp()? "1":"0"); + } + if (!sendRtpItem.isTcpActive()) { + param.put("dst_url",sendRtpItem.getIp()); + param.put("dst_port", sendRtpItem.getPort()); + } + + ZLMResult zlmResult = zlmServerFactory.startSendRtpPassive(mediaServer, param, null); + if (zlmResult.getCode() != 0 ) { + log.error("启动监听TCP被动推流失败: {}, 参数:{}", zlmResult.getMsg(), JSON.toJSONString(param)); + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + } + log.info("调用ZLM-TCP被动推流接口成功: 本地端口: {}", zlmResult.getLocal_port()); + return zlmResult.getLocal_port(); + } + + @Override + public void startSendRtpStream(MediaServer mediaServer, SendRtpInfo sendRtpItem) { + Map param = new HashMap<>(12); + param.put("vhost", "__defaultVhost__"); + param.put("app", sendRtpItem.getApp()); + param.put("stream", sendRtpItem.getStream()); + param.put("ssrc", sendRtpItem.getSsrc()); + param.put("src_port", sendRtpItem.getLocalPort()); + param.put("pt", sendRtpItem.getPt()); + param.put("use_ps", sendRtpItem.isUsePs() ? "1" : "0"); + param.put("only_audio", sendRtpItem.isOnlyAudio() ? "1" : "0"); + param.put("is_udp", sendRtpItem.isTcp() ? "0" : "1"); + param.put("enable_origin_recv_limit", "1"); + if (!sendRtpItem.isTcp()) { + // udp模式下开启rtcp保活 + param.put("udp_rtcp_timeout", sendRtpItem.isRtcp() ? "500" : "0"); + } + param.put("dst_url", sendRtpItem.getIp()); + param.put("dst_port", sendRtpItem.getPort()); + ZLMResult zlmResult = zlmresTfulUtils.startSendRtp(mediaServer, param); + if (zlmResult == null ) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "连接zlm失败"); + }else if (zlmResult.getCode() != 0) { + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + } + log.info("[推流结果]:{} ,参数: {}", zlmResult, JSONObject.toJSONString(param)); + } + + @Override + public Integer startSendRtpTalk(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout) { + Map param = new HashMap<>(12); + param.put("vhost","__defaultVhost__"); + param.put("app", sendRtpItem.getApp()); + param.put("stream", sendRtpItem.getStream()); + param.put("ssrc", sendRtpItem.getSsrc()); + param.put("pt", sendRtpItem.getPt()); + param.put("type", sendRtpItem.isUsePs() ? "1" : "0"); + param.put("only_audio", sendRtpItem.isOnlyAudio() ? "1" : "0"); + param.put("recv_stream_id", sendRtpItem.getReceiveStream()); + param.put("enable_origin_recv_limit", "1"); + ZLMResult zlmResult = zlmServerFactory.startSendRtpTalk(mediaServer, param, null); + if (zlmResult.getCode() != 0 ) { + log.error("启动监听TCP被动推流失败: {}, 参数:{}", zlmResult.getMsg(), JSON.toJSONString(param)); + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + } + log.info("调用ZLM-TCP被动推流接口, 成功 本地端口: {}", zlmResult.getLocal_port()); + return zlmResult.getLocal_port(); + } + + @Override + public Long updateDownloadProcess(MediaServer mediaServer, String app, String stream) { + MediaInfo mediaInfo = getMediaInfo(mediaServer, app, stream); + if (mediaInfo == null) { + log.warn("[获取下载进度] 查询进度失败, 节点Id: {}, {}/{}", mediaServer.getId(), app, stream); + return null; + } + return mediaInfo.getDuration(); + } + + @Override + public String startProxy(MediaServer mediaServer, StreamProxy streamProxy) { + String dstUrl; + if ("ffmpeg".equalsIgnoreCase(streamProxy.getType())) { + + String ffmpegCmd = getFfmpegCmd(mediaServer, streamProxy.getFfmpegCmdKey()); + + if (ffmpegCmd == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "ffmpeg拉流代理无法获取ffmpeg cmd"); + } + String schema = getSchemaFromFFmpegCmd(ffmpegCmd); + if (schema == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "ffmpeg拉流代理无法从ffmpeg cmd中获取到输出格式"); + } + int port; + String schemaForUri; + if (schema.equalsIgnoreCase("rtsp")) { + port = mediaServer.getRtspPort(); + schemaForUri = schema; + }else if (schema.equalsIgnoreCase("flv")) { + if (mediaServer.getRtmpPort() == 0) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "ffmpeg拉流代理播放时发现未设置rtmp端口"); + } + port = mediaServer.getRtmpPort(); + schemaForUri = "rtmp"; + }else { + port = mediaServer.getRtmpPort(); + schemaForUri = schema; + } + + dstUrl = String.format("%s://%s:%s/%s/%s", schemaForUri, "127.0.0.1", port, streamProxy.getApp(), + streamProxy.getStream()); + }else { + dstUrl = String.format("rtsp://%s:%s/%s/%s", "127.0.0.1", mediaServer.getRtspPort(), streamProxy.getApp(), + streamProxy.getStream()); + } + MediaInfo mediaInfo = getMediaInfo(mediaServer, streamProxy.getApp(), streamProxy.getStream()); + + if (mediaInfo != null) { + closeStreams(mediaServer, streamProxy.getApp(), streamProxy.getStream()); + } + + ZLMResult zlmResult = null; + if ("ffmpeg".equalsIgnoreCase(streamProxy.getType())){ + if (streamProxy.getTimeout() == 0) { + streamProxy.setTimeout(15); + } + zlmResult = zlmresTfulUtils.addFFmpegSource(mediaServer, streamProxy.getSrcUrl().trim(), dstUrl, + streamProxy.getTimeout(), streamProxy.isEnableAudio(), streamProxy.isEnableMp4(), + streamProxy.getFfmpegCmdKey()); + }else { + zlmResult = zlmresTfulUtils.addStreamProxy(mediaServer, streamProxy.getApp(), streamProxy.getStream(), streamProxy.getSrcUrl().trim(), + streamProxy.isEnableAudio(), streamProxy.isEnableMp4(), streamProxy.getRtspType(), streamProxy.getTimeout()); + } + if (zlmResult.getCode() != 0) { + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + }else { + StreamProxyResult data = zlmResult.getData(); + if (data == null) { + throw new ControllerException(zlmResult.getCode(), "代理结果异常: " + zlmResult); + }else { + return data.getKey(); + } + } + } + + private String getSchemaFromFFmpegCmd(String ffmpegCmd) { + ffmpegCmd = ffmpegCmd.replaceAll(" + ", " "); + String[] paramArray = ffmpegCmd.split(" "); + if (paramArray.length == 0) { + return null; + } + for (int i = 0; i < paramArray.length; i++) { + if (paramArray[i].equalsIgnoreCase("-f")) { + if (i + 1 < paramArray.length - 1) { + return paramArray[i+1]; + }else { + return null; + } + + } + } + return null; + } + + @Override + public void stopProxy(MediaServer mediaServer, String streamKey, String type) { + ZLMResult zlmResult = zlmresTfulUtils.delStreamProxy(mediaServer, streamKey); + if (zlmResult == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败"); + }else if (zlmResult.getCode() != 0) { + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + } + } + + @Override + public List listRtpServer(MediaServer mediaServer) { + ZLMResult> zlmResult = zlmresTfulUtils.listRtpServer(mediaServer); + List result = new ArrayList<>(); + if (zlmResult.getCode() != 0) { + return result; + } + List data = zlmResult.getData(); + if (data == null || data.isEmpty()) { + return result; + } + for (RtpServerResult datum : data) { + result.add(datum.getStream_id()); + } + return result; + } + + @Override + public void loadMP4File(MediaServer mediaServer, String app, String stream, String filePath, String fileName, ErrorCallback callback) { + String buildApp = "mp4_record"; + String buildStream = app + "_" + stream + "_" + fileName + "_" + RandomStringUtils.randomAlphabetic(6).toLowerCase(); + + Hook hook = Hook.getInstance(HookType.on_media_arrival, buildApp, buildStream, mediaServer.getServerId()); + subscribe.addSubscribe(hook, (hookData) -> { + StreamInfo streamInfo = getStreamInfoByAppAndStream(mediaServer, buildApp, buildStream, hookData.getMediaInfo(), null, null, true); + if (callback != null) { + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo); + } + }); + + ZLMResult zlmResult = zlmresTfulUtils.loadMP4File(mediaServer, buildApp, buildStream, filePath); + + if (zlmResult == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败"); + } + if (zlmResult.getCode() != 0) { + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + } + } + + @Override + public void loadMP4FileForDate(MediaServer mediaServer, String app, String stream, String date, String dateDir, ErrorCallback callback) { + String buildApp = "mp4_record"; + String buildStream = app + "_" + stream + "_" + date; + MediaInfo mediaInfo = getMediaInfo(mediaServer, buildApp, buildStream); + if (mediaInfo != null) { + if (callback != null) { + StreamInfo streamInfo = getStreamInfoByAppAndStream(mediaServer, buildApp, buildStream, mediaInfo, null, null, true); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo); + } + return; + } + + Hook hook = Hook.getInstance(HookType.on_media_arrival, buildApp, buildStream, mediaServer.getServerId()); + subscribe.addSubscribe(hook, (hookData) -> { + StreamInfo streamInfo = getStreamInfoByAppAndStream(mediaServer, buildApp, buildStream, hookData.getMediaInfo(), null, null, true); + if (callback != null) { + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo); + } + }); + + ZLMResult zlmResult = zlmresTfulUtils.loadMP4File(mediaServer, buildApp, buildStream, dateDir); + + if (zlmResult == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败"); + } + if (zlmResult.getCode() != 0) { + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + } + } + + @Override + public void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema) { + ZLMResult zlmResult = zlmresTfulUtils.seekRecordStamp(mediaServer, app, stream, stamp, schema); + if (zlmResult == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败"); + } + if (zlmResult.getCode() != 0) { + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + } + } + + @Override + public void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema) { + ZLMResult zlmResult = zlmresTfulUtils.setRecordSpeed(mediaServer, app, stream, speed, schema); + if (zlmResult == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败"); + } + if (zlmResult.getCode() != 0) { + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + } + } + + @Override + public DownloadFileInfo getDownloadFilePath(MediaServer mediaServerItem, RecordInfo recordInfo) { + + // 将filePath作为独立参数传入,避免%符号解析问题 + String pathTemplate = "%s://%s:%s/index/api/downloadFile?file_path=%s"; + + DownloadFileInfo info = new DownloadFileInfo(); + + // filePath作为第4个参数 + info.setHttpPath(String.format(pathTemplate, + "http", + mediaServerItem.getStreamIp(), + mediaServerItem.getHttpPort(), + recordInfo.getFilePath())); + + // 同样作为第4个参数 + if (mediaServerItem.getHttpSSlPort() > 0) { + info.setHttpsPath(String.format(pathTemplate, + "https", + mediaServerItem.getStreamIp(), + mediaServerItem.getHttpSSlPort(), + recordInfo.getFilePath())); + } + return info; + } + + @Override + public StreamInfo getStreamInfoByAppAndStream(MediaServer mediaServer, String app, String stream, MediaInfo mediaInfo, String addr, String callId, boolean isPlay) { + StreamInfo streamInfoResult = new StreamInfo(); + streamInfoResult.setStream(stream); + streamInfoResult.setApp(app); + if (addr == null) { + addr = mediaServer.getStreamIp(); + } + + streamInfoResult.setIp(addr); + if (mediaInfo != null) { + streamInfoResult.setServerId(mediaInfo.getServerId()); + }else { + streamInfoResult.setServerId(userSetting.getServerId()); + } + + streamInfoResult.setMediaServer(mediaServer); + Map param = new HashMap<>(); + if (!ObjectUtils.isEmpty(callId)) { + param.put("callId", callId); + } + if (mediaInfo != null && !ObjectUtils.isEmpty(mediaInfo.getOriginTypeStr())) { + if (!ObjectUtils.isEmpty(mediaInfo.getOriginTypeStr())) { + param.put("originTypeStr", mediaInfo.getOriginTypeStr()); + } + if (!ObjectUtils.isEmpty(mediaInfo.getVideoCodec())) { + param.put("videoCodec", mediaInfo.getVideoCodec()); + } + if (!ObjectUtils.isEmpty(mediaInfo.getAudioCodec())) { + param.put("audioCodec", mediaInfo.getAudioCodec()); + } + } + StringBuilder callIdParamBuilder = new StringBuilder(); + if (!param.isEmpty()) { + callIdParamBuilder.append("?"); + for (Map.Entry entry : param.entrySet()) { + callIdParamBuilder.append(entry.getKey()).append("=").append(entry.getValue()); + callIdParamBuilder.append("&"); + } + callIdParamBuilder.deleteCharAt(callIdParamBuilder.length() - 1); + } + + String callIdParam = callIdParamBuilder.toString(); + + streamInfoResult.setRtmp(addr, mediaServer.getRtmpPort(),mediaServer.getRtmpSSlPort(), app, stream, callIdParam); + streamInfoResult.setRtsp(addr, mediaServer.getRtspPort(),mediaServer.getRtspSSLPort(), app, stream, callIdParam); + + String flvFile = String.format("%s/%s.live.flv%s", app, stream, callIdParam); + streamInfoResult.setFlv(addr, mediaServer.getHttpPort(),mediaServer.getHttpSSlPort(), flvFile); + streamInfoResult.setWsFlv(addr, mediaServer.getHttpPort(),mediaServer.getHttpSSlPort(), flvFile); + + String mp4File = String.format("%s/%s.live.mp4%s", app, stream, callIdParam); + streamInfoResult.setFmp4(addr, mediaServer.getHttpPort(),mediaServer.getHttpSSlPort(), mp4File); + streamInfoResult.setWsMp4(addr, mediaServer.getHttpPort(),mediaServer.getHttpSSlPort(), mp4File); + + streamInfoResult.setHls(addr, mediaServer.getHttpPort(), mediaServer.getHttpSSlPort(), app, stream, callIdParam); + streamInfoResult.setWsHls(addr, mediaServer.getHttpPort(), mediaServer.getHttpSSlPort(), app, stream, callIdParam); + + streamInfoResult.setTs(addr, mediaServer.getHttpPort(), mediaServer.getHttpSSlPort(), app, stream, callIdParam); + streamInfoResult.setWsTs(addr, mediaServer.getHttpPort(), mediaServer.getHttpSSlPort(), app, stream, callIdParam); + + streamInfoResult.setRtc(addr, mediaServer.getHttpPort(), mediaServer.getHttpSSlPort(), app, stream, callIdParam, isPlay); + + streamInfoResult.setMediaInfo(mediaInfo); + + if (!"broadcast".equalsIgnoreCase(app) && !ObjectUtils.isEmpty(mediaServer.getTranscodeSuffix()) && !"null".equalsIgnoreCase(mediaServer.getTranscodeSuffix())) { + String newStream = stream + "_" + mediaServer.getTranscodeSuffix(); + mediaServer.setTranscodeSuffix(null); + StreamInfo transcodeStreamInfo = getStreamInfoByAppAndStream(mediaServer, app, newStream, null, addr, callId, isPlay); + streamInfoResult.setTranscodeStream(transcodeStreamInfo); + } + return streamInfoResult; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaServerStatusManager.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaServerStatusManager.java new file mode 100644 index 0000000..0281ffe --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaServerStatusManager.java @@ -0,0 +1,319 @@ +package com.genersoft.iot.vmp.media.zlm; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerChangeEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerDeleteEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.ZLMResult; +import com.genersoft.iot.vmp.media.zlm.dto.ZLMServerConfig; +import com.genersoft.iot.vmp.media.zlm.event.HookZlmServerKeepaliveEvent; +import com.genersoft.iot.vmp.media.zlm.event.HookZlmServerStartEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 管理zlm流媒体节点的状态 + */ +@Slf4j +@Component +public class ZLMMediaServerStatusManager { + + private final Map offlineZlmPrimaryMap = new ConcurrentHashMap<>(); + private final Map offlineZlmsecondaryMap = new ConcurrentHashMap<>(); + private final Map offlineZlmTimeMap = new ConcurrentHashMap<>(); + + @Autowired + private ZLMRESTfulUtils zlmresTfulUtils; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private DynamicTask dynamicTask; + + @Value("${server.ssl.enabled:false}") + private boolean sslEnabled; + + @Value("${server.port}") + private Integer serverPort; + + @Value("${server.servlet.context-path:}") + private String serverServletContextPath; + + @Autowired + private EventPublisher eventPublisher; + + private final String type = "zlm"; + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaServerChangeEvent event) { + if (event.getMediaServerItemList() == null + || event.getMediaServerItemList().isEmpty()) { + return; + } + for (MediaServer mediaServerItem : event.getMediaServerItemList()) { + if (!type.equals(mediaServerItem.getType())) { + continue; + } + log.info("[ZLM-添加待上线节点] ID:" + mediaServerItem.getId()); + offlineZlmPrimaryMap.put(mediaServerItem.getId(), mediaServerItem); + offlineZlmTimeMap.put(mediaServerItem.getId(), System.currentTimeMillis()); + execute(); + } + } + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(HookZlmServerStartEvent event) { + if (event.getMediaServerItem() == null + || !type.equals(event.getMediaServerItem().getType()) + || event.getMediaServerItem().isStatus()) { + return; + } + MediaServer serverItem = mediaServerService.getOne(event.getMediaServerItem().getId()); + if (serverItem == null) { + return; + } + log.info("[ZLM-HOOK事件-服务启动] ID:" + event.getMediaServerItem().getId()); + online(serverItem, null); + } + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(HookZlmServerKeepaliveEvent event) { + if (event.getMediaServerItem() == null) { + return; + } + MediaServer serverItem = mediaServerService.getOne(event.getMediaServerItem().getId()); + if (serverItem == null) { + return; + } + log.debug("[ZLM-HOOK事件-心跳] ID:" + event.getMediaServerItem().getId()); + online(serverItem, null); + } + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaServerDeleteEvent event) { + if (event.getMediaServer() == null) { + return; + } + log.info("[ZLM-节点被移除] ID:" + event.getMediaServer().getId()); + offlineZlmPrimaryMap.remove(event.getMediaServer().getId()); + offlineZlmsecondaryMap.remove(event.getMediaServer().getId()); + offlineZlmTimeMap.remove(event.getMediaServer().getId()); + } + + @Scheduled(fixedDelay = 10*1000) //每隔10秒检查一次 + public void execute(){ + // 初次加入的离线节点会在30分钟内,每间隔十秒尝试一次,30分钟后如果仍然没有上线,则每隔30分钟尝试一次连接 + if (offlineZlmPrimaryMap.isEmpty() && offlineZlmsecondaryMap.isEmpty()) { + return; + } + if (!offlineZlmPrimaryMap.isEmpty()) { + for (MediaServer mediaServerItem : offlineZlmPrimaryMap.values()) { + if (offlineZlmTimeMap.get(mediaServerItem.getId()) != null + && offlineZlmTimeMap.get(mediaServerItem.getId()) < System.currentTimeMillis() - 30*60*1000) { + offlineZlmsecondaryMap.put(mediaServerItem.getId(), mediaServerItem); + offlineZlmPrimaryMap.remove(mediaServerItem.getId()); + continue; + } + log.info("[ZLM-尝试连接] ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + ZLMResult> mediaServerConfigResult = zlmresTfulUtils.getMediaServerConfig(mediaServerItem); + ZLMServerConfig zlmServerConfig = null; + if (mediaServerConfigResult == null) { + log.info("[ZLM-尝试连接]失败, ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + continue; + } + List data = mediaServerConfigResult.getData(); + if (data == null || data.isEmpty()) { + log.info("[ZLM-尝试连接]失败, ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + }else { + zlmServerConfig = JSON.parseObject(JSON.toJSONString(data.get(0)), ZLMServerConfig.class); + initPort(mediaServerItem, zlmServerConfig); + online(mediaServerItem, zlmServerConfig); + } + } + } + if (!offlineZlmsecondaryMap.isEmpty()) { + for (MediaServer mediaServerItem : offlineZlmsecondaryMap.values()) { + if (offlineZlmTimeMap.get(mediaServerItem.getId()) < System.currentTimeMillis() - 30*60*1000) { + continue; + } + log.info("[ZLM-尝试连接] ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + ZLMResult> mediaServerConfig = zlmresTfulUtils.getMediaServerConfig(mediaServerItem); + ZLMServerConfig zlmServerConfig = null; + if (mediaServerConfig == null) { + log.info("[ZLM-尝试连接]失败, ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + offlineZlmTimeMap.put(mediaServerItem.getId(), System.currentTimeMillis()); + continue; + } + List data = mediaServerConfig.getData(); + if (data == null || data.isEmpty()) { + log.info("[ZLM-尝试连接]失败, ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + offlineZlmTimeMap.put(mediaServerItem.getId(), System.currentTimeMillis()); + }else { + zlmServerConfig = JSON.parseObject(JSON.toJSONString(data.get(0)), ZLMServerConfig.class); + initPort(mediaServerItem, zlmServerConfig); + online(mediaServerItem, zlmServerConfig); + } + } + } + } + + private void online(MediaServer mediaServerItem, ZLMServerConfig config) { + offlineZlmPrimaryMap.remove(mediaServerItem.getId()); + offlineZlmsecondaryMap.remove(mediaServerItem.getId()); + offlineZlmTimeMap.remove(mediaServerItem.getId()); + if (!mediaServerItem.isStatus()) { + log.info("[ZLM-连接成功] ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + mediaServerItem.setStatus(true); + mediaServerItem.setHookAliveInterval(10F); + // 发送上线通知 + eventPublisher.mediaServerOnlineEventPublish(mediaServerItem); + if(mediaServerItem.isAutoConfig()) { + if (config == null) { + ZLMResult> mediaServerConfig = zlmresTfulUtils.getMediaServerConfig(mediaServerItem); + List data = mediaServerConfig.getData(); + if (data != null && !data.isEmpty()) { + config = JSON.parseObject(JSON.toJSONString(data.get(0)), ZLMServerConfig.class); + } + } + if (config != null) { + initPort(mediaServerItem, config); + setZLMConfig(mediaServerItem, "0".equals(config.getHookEnable()) + || !Objects.equals(mediaServerItem.getHookAliveInterval(), config.getHookAliveInterval())); + } + } + mediaServerService.update(mediaServerItem); + } + // 设置两次心跳未收到则认为zlm离线 + String key = "zlm-keepalive-" + mediaServerItem.getId(); + dynamicTask.startDelay(key, ()->{ + log.warn("[ZLM-心跳超时] ID:{}", mediaServerItem.getId()); + mediaServerItem.setStatus(false); + offlineZlmPrimaryMap.put(mediaServerItem.getId(), mediaServerItem); + offlineZlmTimeMap.put(mediaServerItem.getId(), System.currentTimeMillis()); + // 发送离线通知 + eventPublisher.mediaServerOfflineEventPublish(mediaServerItem); + mediaServerService.update(mediaServerItem); + }, (int)(mediaServerItem.getHookAliveInterval() * 2 * 1000)); + } + private void initPort(MediaServer mediaServerItem, ZLMServerConfig zlmServerConfig) { + // 端口只会从配置中读取一次,一旦自己配置或者读取过了将不在配置 + if (mediaServerItem.getHttpSSlPort() == 0) { + mediaServerItem.setHttpSSlPort(zlmServerConfig.getHttpSSLport()); + } + if (mediaServerItem.getRtmpPort() == 0) { + mediaServerItem.setRtmpPort(zlmServerConfig.getRtmpPort()); + } + if (mediaServerItem.getRtmpSSlPort() == 0) { + mediaServerItem.setRtmpSSlPort(zlmServerConfig.getRtmpSslPort()); + } + if (mediaServerItem.getRtspPort() == 0) { + mediaServerItem.setRtspPort(zlmServerConfig.getRtspPort()); + } + if (mediaServerItem.getRtspSSLPort() == 0) { + mediaServerItem.setRtspSSLPort(zlmServerConfig.getRtspSSlport()); + } + if (mediaServerItem.getRtpProxyPort() == 0) { + mediaServerItem.setRtpProxyPort(zlmServerConfig.getRtpProxyPort()); + } + if (mediaServerItem.getFlvSSLPort() == 0) { + mediaServerItem.setFlvSSLPort(zlmServerConfig.getHttpSSLport()); + } + if (mediaServerItem.getWsFlvSSLPort() == 0) { + mediaServerItem.setWsFlvSSLPort(zlmServerConfig.getHttpSSLport()); + } + if (Objects.isNull(zlmServerConfig.getTranscodeSuffix())) { + mediaServerItem.setTranscodeSuffix(null); + }else { + mediaServerItem.setTranscodeSuffix(zlmServerConfig.getTranscodeSuffix()); + } + mediaServerItem.setRtpProxyPort(zlmServerConfig.getRtpProxyPort()); + mediaServerItem.setHookAliveInterval(10F); + } + + public void setZLMConfig(MediaServer mediaServerItem, boolean restart) { + log.info("[媒体服务节点] 正在设置 :{} -> {}:{}", + mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + String protocol = sslEnabled ? "https" : "http"; + String hookPrefix = String.format("%s://%s:%s%s/index/hook", protocol, mediaServerItem.getHookIp(), serverPort, (serverServletContextPath == null || "/".equals(serverServletContextPath)) ? "" : serverServletContextPath); + + Map param = new HashMap<>(); + if (mediaServerItem.getRtspPort() != 0) { + param.put("ffmpeg.snap", "%s -rtsp_transport tcp -i %s -y -f mjpeg -frames:v 1 %s"); + } + param.put("hook.enable","1"); + param.put("hook.on_flow_report",""); + param.put("hook.on_play",String.format("%s/on_play", hookPrefix)); + param.put("hook.on_http_access",""); + param.put("hook.on_publish", String.format("%s/on_publish", hookPrefix)); + param.put("hook.on_record_ts",""); + param.put("hook.on_rtsp_auth",""); + param.put("hook.on_rtsp_realm",""); + param.put("hook.on_server_started",String.format("%s/on_server_started", hookPrefix)); + param.put("hook.on_shell_login",""); + param.put("hook.on_stream_changed",String.format("%s/on_stream_changed", hookPrefix)); + param.put("hook.on_stream_none_reader",String.format("%s/on_stream_none_reader", hookPrefix)); + param.put("hook.on_stream_not_found",String.format("%s/on_stream_not_found", hookPrefix)); + param.put("hook.on_server_keepalive",String.format("%s/on_server_keepalive", hookPrefix)); + param.put("hook.on_send_rtp_stopped",String.format("%s/on_send_rtp_stopped", hookPrefix)); + param.put("hook.on_rtp_server_timeout",String.format("%s/on_rtp_server_timeout", hookPrefix)); + param.put("hook.on_record_mp4",String.format("%s/on_record_mp4", hookPrefix)); + param.put("hook.timeoutSec","30"); + param.put("hook.alive_interval", mediaServerItem.getHookAliveInterval()); + // 推流断开后可以在超时时间内重新连接上继续推流,这样播放器会接着播放。 + // 置0关闭此特性(推流断开会导致立即断开播放器) + // 此参数不应大于播放器超时时间 + // 优化此消息以更快的收到流注销事件 + param.put("protocol.continue_push_ms", "3000" ); + // 最多等待未初始化的Track时间,单位毫秒,超时之后会忽略未初始化的Track, 设置此选项优化那些音频错误的不规范流, + // 等zlm支持给每个rtpServer设置关闭音频的时候可以不设置此选项 + if (mediaServerItem.isRtpEnable() && !ObjectUtils.isEmpty(mediaServerItem.getRtpPortRange())) { + param.put("rtp_proxy.port_range", mediaServerItem.getRtpPortRange().replace(",", "-")); + } + + if (!ObjectUtils.isEmpty(mediaServerItem.getRecordPath())) { + File recordPathFile = new File(mediaServerItem.getRecordPath()); + param.put("protocol.mp4_save_path", recordPathFile.getParentFile().getPath()); + param.put("protocol.downloadRoot", recordPathFile.getParentFile().getPath()); + param.put("record.appName", recordPathFile.getName()); + } + + ZLMResult zlmResult = zlmresTfulUtils.setServerConfig(mediaServerItem, param); + + if (zlmResult != null && zlmResult.getCode() == 0) { + if (restart) { + log.info("[媒体服务节点] 设置成功,开始重启以保证配置生效 {} -> {}:{}", + mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + zlmresTfulUtils.restartServer(mediaServerItem); + }else { + log.info("[媒体服务节点] 设置成功 {} -> {}:{}", + mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + } + }else { + log.info("[媒体服务节点] 设置媒体服务节点失败 {} -> {}:{}", + mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java new file mode 100755 index 0000000..66bad9a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java @@ -0,0 +1,814 @@ +package com.genersoft.iot.vmp.media.zlm; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.TypeReference; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.*; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import okhttp3.logging.HttpLoggingInterceptor; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class ZLMRESTfulUtils { + + private OkHttpClient client; + + + public interface RequestCallback{ + void run(String response); + } + public interface ResultCallback{ + void run(ZLMResult response); + } + + private OkHttpClient getClient(){ + return getClient(null); + } + + private OkHttpClient getClient(Integer readTimeOut){ + if (client == null) { + if (readTimeOut == null) { + readTimeOut = 10; + } + OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); + //todo 暂时写死超时时间 均为5s + // 设置连接超时时间 + httpClientBuilder.connectTimeout(8,TimeUnit.SECONDS); + // 设置读取超时时间 + httpClientBuilder.readTimeout(readTimeOut,TimeUnit.SECONDS); + // 设置连接池 + httpClientBuilder.connectionPool(new ConnectionPool(16, 5, TimeUnit.MINUTES)); + if (log.isDebugEnabled()) { + HttpLoggingInterceptor logging = new HttpLoggingInterceptor(message -> { + log.debug("http请求参数:" + message); + }); + logging.setLevel(HttpLoggingInterceptor.Level.BASIC); + // OkHttp進行添加攔截器loggingInterceptor + httpClientBuilder.addInterceptor(logging); + } + client = httpClientBuilder.build(); + } + return client; + + } + + public String sendPost(MediaServer mediaServer, String api, Map param, RequestCallback callback) { + return sendPost(mediaServer, api, param, callback, null); + } + + + public String sendPost(MediaServer mediaServer, String api, Map param, RequestCallback callback, Integer readTimeOut) { + OkHttpClient client = getClient(readTimeOut); + + if (mediaServer == null) { + return null; + } + String url = String.format("http://%s:%s/index/api/%s", mediaServer.getIp(), mediaServer.getHttpPort(), api); + String result = null; + FormBody.Builder builder = new FormBody.Builder(); + builder.add("secret",mediaServer.getSecret()); + if (param != null && !param.isEmpty()) { + for (String key : param.keySet()){ + if (param.get(key) != null) { + builder.add(key, param.get(key).toString()); + } + } + } + + FormBody body = builder.build(); + + Request request = new Request.Builder() + .post(body) + .url(url) + .build(); + if (callback == null) { + try { + Response response = client.newCall(request).execute(); + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + result = responseBody.string(); + } + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + }catch (IOException e) { + log.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + + if(e instanceof SocketTimeoutException){ + //读取超时超时异常 + log.error(String.format("读取ZLM数据超时失败: %s, %s", url, e.getMessage())); + } + if(e instanceof ConnectException){ + //判断连接异常,我这里是报Failed to connect to 10.7.5.144 + log.error(String.format("连接ZLM连接失败: %s, %s", url, e.getMessage())); + } + + }catch (Exception e){ + log.error(String.format("访问ZLM失败: %s, %s", url, e.getMessage())); + } + }else { + client.newCall(request).enqueue(new Callback(){ + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response){ + if (response.isSuccessful()) { + try { + String responseStr = Objects.requireNonNull(response.body()).string(); + callback.run(responseStr); + } catch (IOException e) { + log.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + log.error(String.format("连接ZLM失败: %s, %s", call.request().toString(), e.getMessage())); + + if(e instanceof SocketTimeoutException){ + //读取超时超时异常 + log.error(String.format("读取ZLM数据失败: %s, %s", call.request().toString(), e.getMessage())); + } + if(e instanceof ConnectException){ + //判断连接异常,我这里是报Failed to connect to 10.7.5.144 + log.error(String.format("连接ZLM失败: %s, %s", call.request().toString(), e.getMessage())); + } + } + }); + } + + return result; + } + + public void sendGetForImg(MediaServer mediaServer, String api, Map params, String targetPath, String fileName) { + String url = String.format("http://%s:%s/index/api/%s", mediaServer.getIp(), mediaServer.getHttpPort(), api); + HttpUrl parseUrl = HttpUrl.parse(url); + if (parseUrl == null) { + return; + } + HttpUrl.Builder httpBuilder = parseUrl.newBuilder(); + + httpBuilder.addQueryParameter("secret", mediaServer.getSecret()); + if (params != null) { + for (Map.Entry param : params.entrySet()) { + httpBuilder.addQueryParameter(param.getKey(), param.getValue().toString()); + } + } + + Request request = new Request.Builder() + .url(httpBuilder.build()) + .build(); + if (log.isDebugEnabled()){ + log.debug(request.toString()); + } + try { + OkHttpClient client = getClient(); + Response response = client.newCall(request).execute(); + if (response.isSuccessful()) { + if (targetPath != null) { + File snapFolder = new File(targetPath); + if (!snapFolder.exists()) { + if (!snapFolder.mkdirs()) { + log.warn("{}路径创建失败", snapFolder.getAbsolutePath()); + } + + } + File snapFile = new File(targetPath + File.separator + fileName); + FileOutputStream outStream = new FileOutputStream(snapFile); + + outStream.write(Objects.requireNonNull(response.body()).bytes()); + outStream.flush(); + outStream.close(); + } else { + log.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message())); + } + } else { + log.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message())); + } + Objects.requireNonNull(response.body()).close(); + } catch (ConnectException e) { + log.error(String.format("连接ZLM失败: %s, %s", e.getCause().getMessage(), e.getMessage())); + log.info("请检查media配置并确认ZLM已启动..."); + } catch (IOException e) { + log.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + } + + public ZLMResult isMediaOnline(MediaServer mediaServer, String app, String stream, String schema){ + Map param = new HashMap<>(); + if (app != null) { + param.put("app",app); + } + if (stream != null) { + param.put("stream",stream); + } + if (schema != null) { + param.put("schema",schema); + } + param.put("vhost","__defaultVhost__"); + String response = sendPost(mediaServer, "isMediaOnline", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult getMediaList(MediaServer mediaServer, String app, String stream, String schema, ResultCallback callback){ + Map param = new HashMap<>(); + if (app != null) { + param.put("app",app); + } + if (stream != null) { + param.put("stream",stream); + } + if (schema != null) { + param.put("schema",schema); + } + param.put("vhost","__defaultVhost__"); + RequestCallback requestCallback = null; + if (callback != null) { + requestCallback = (responseStr -> { + if (callback == null) { + return; + } + if (responseStr == null) { + callback.run(ZLMResult.getFailForMediaServer()); + }else { + ZLMResult zlmResult = JSON.parseObject(responseStr, new TypeReference>() {}); + if (zlmResult == null) { + callback.run(ZLMResult.getFailForMediaServer()); + }else { + callback.run(zlmResult); + } + + } + }); + } + + String response = sendPost(mediaServer, "getMediaList",param, requestCallback); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, new TypeReference>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult getMediaList(MediaServer mediaServer, String app, String stream){ + return getMediaList(mediaServer, app, stream,null, null); + } + + public ZLMResult getMediaInfo(MediaServer mediaServer, String app, String schema, String stream){ + Map param = new HashMap<>(); + param.put("app",app); + param.put("schema",schema); + param.put("stream",stream); + param.put("vhost","__defaultVhost__"); + + String response = sendPost(mediaServer, "getMediaInfo",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + JSONObject jsonObject = JSON.parseObject(response); + if (jsonObject == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = new ZLMResult<>(); + zlmResult.setCode(0); + zlmResult.setData(jsonObject); + return zlmResult; + } + } + } + + public ZLMResult getRtpInfo(MediaServer mediaServer, String stream_id){ + Map param = new HashMap<>(); + param.put("stream_id",stream_id); + String response = sendPost(mediaServer, "getRtpInfo",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult addFFmpegSource(MediaServer mediaServer, String src_url, String dst_url, Integer timeout_sec, + boolean enable_audio, boolean enable_mp4, String ffmpeg_cmd_key){ + try { + src_url = URLEncoder.encode(src_url, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"url编码失败"); + } + + Map param = new HashMap<>(); + param.put("src_url", src_url); + param.put("dst_url", dst_url); + param.put("timeout_ms", timeout_sec*1000); + param.put("enable_mp4", enable_mp4); + param.put("ffmpeg_cmd_key", ffmpeg_cmd_key); + + String response = sendPost(mediaServer, "addFFmpegSource",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, new TypeReference>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult delFFmpegSource(MediaServer mediaServer, String key){ + Map param = new HashMap<>(); + param.put("key", key); + + String response = sendPost(mediaServer, "delFFmpegSource",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, new TypeReference>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult delStreamProxy(MediaServer mediaServer, String key){ + Map param = new HashMap<>(); + param.put("key", key); + String response = sendPost(mediaServer, "delStreamProxy",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, new TypeReference>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult> getMediaServerConfig(MediaServer mediaServer ){ + + String response = sendPost(mediaServer, "getServerConfig",null, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult> zlmResult = JSON.parseObject(response, new TypeReference>>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult setServerConfig(MediaServer mediaServer, Map param){ + String response = sendPost(mediaServer, "setServerConfig",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult openRtpServer(MediaServer mediaServer, Map param){ + String response = sendPost(mediaServer, "openRtpServer",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult closeRtpServer(MediaServer mediaServer, Map param) { + String response = sendPost(mediaServer, "closeRtpServer",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public void closeRtpServer(MediaServer mediaServer, Map param, ResultCallback callback) { + sendPost(mediaServer, "closeRtpServer",param, (response -> { + if (callback == null) { + return; + } + if (response == null) { + callback.run(ZLMResult.getFailForMediaServer()); + }else { + callback.run(JSON.parseObject(response, ZLMResult.class)); + } + })); + + } + + public ZLMResult> listRtpServer(MediaServer mediaServer) { + String response = sendPost(mediaServer, "listRtpServer",null, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult> zlmResult = JSON.parseObject(response, new TypeReference>>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult startSendRtp(MediaServer mediaServer, Map param) { + String response = sendPost(mediaServer, "startSendRtp",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult startSendRtpPassive(MediaServer mediaServer, Map param) { + String response = sendPost(mediaServer, "startSendRtpPassive",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult startSendRtpPassive(MediaServer mediaServer, Map param, ResultCallback callback) { + String response = sendPost(mediaServer, "startSendRtpPassive",param, (responseStr -> { + if (callback == null) { + return; + } + if (responseStr == null) { + callback.run(ZLMResult.getFailForMediaServer()); + }else { + ZLMResult zlmResult = JSON.parseObject(responseStr, ZLMResult.class); + if (zlmResult == null) { + callback.run(ZLMResult.getFailForMediaServer()); + }else { + callback.run(zlmResult); + } + } + })); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult startSendRtpTalk(MediaServer mediaServer, Map param, ResultCallback callback) { + String response = sendPost(mediaServer, "startSendRtpTalk",param, (responseStr -> { + if (callback == null) { + return; + } + if (responseStr == null) { + callback.run(ZLMResult.getFailForMediaServer()); + }else { + callback.run(JSON.parseObject(responseStr, ZLMResult.class)); + } + })); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult stopSendRtp(MediaServer mediaServer, Map param) { + String response = sendPost(mediaServer, "stopSendRtp",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult restartServer(MediaServer mediaServer) { + String response = sendPost(mediaServer, "restartServer",null, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult addStreamProxy(MediaServer mediaServer, String app, String stream, String url, boolean enable_audio, boolean enable_mp4, String rtp_type, Integer timeOut) { + Map param = new HashMap<>(); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + param.put("url", url); + param.put("enable_mp4", enable_mp4?1:0); + param.put("enable_audio", enable_audio?1:0); + param.put("rtp_type", rtp_type); + param.put("timeout_sec", timeOut); + // 拉流重试次数,默认为3 + param.put("retry_count", 3); + + String response = sendPost(mediaServer, "addStreamProxy",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, new TypeReference>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult closeStreams(MediaServer mediaServer, String app, String stream) { + Map param = new HashMap<>(); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + param.put("force", 1); + + String response = sendPost(mediaServer, "close_streams",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, new TypeReference>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult> getAllSession(MediaServer mediaServer) { + String response = sendPost(mediaServer, "getAllSession",null, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult> zlmResult = JSON.parseObject(response, new TypeReference>>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public void kickSessions(MediaServer mediaServer, String localPortSStr) { + Map param = new HashMap<>(); + param.put("local_port", localPortSStr); + sendPost(mediaServer, "kick_sessions",param, null); + } + + public void getSnap(MediaServer mediaServer, String streamUrl, int timeout_sec, int expire_sec, String targetPath, String fileName) { + Map param = new HashMap<>(3); + param.put("url", streamUrl); + param.put("timeout_sec", timeout_sec); + param.put("expire_sec", expire_sec); + param.put("async", 1); + sendGetForImg(mediaServer, "getSnap", param, targetPath, fileName); + } + + public ZLMResult pauseRtpCheck(MediaServer mediaServer, String streamId) { + Map param = new HashMap<>(1); + param.put("stream_id", streamId); + String response = sendPost(mediaServer, "pauseRtpCheck", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult resumeRtpCheck(MediaServer mediaServer, String streamId) { + Map param = new HashMap<>(1); + param.put("stream_id", streamId); + String response = sendPost(mediaServer, "resumeRtpCheck", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult connectRtpServer(MediaServer mediaServer, String dst_url, int dst_port, String stream_id) { + Map param = new HashMap<>(1); + param.put("dst_url", dst_url); + param.put("dst_port", dst_port); + param.put("stream_id", stream_id); + String response = sendPost(mediaServer, "connectRtpServer", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult updateRtpServerSSRC(MediaServer mediaServer, String streamId, String ssrc) { + Map param = new HashMap<>(1); + param.put("ssrc", ssrc); + param.put("stream_id", streamId); + + String response = sendPost(mediaServer, "updateRtpServerSSRC", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult deleteRecordDirectory(MediaServer mediaServer, String app, String stream, String date, String fileName) { + Map param = new HashMap<>(1); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + param.put("period", date); + param.put("name", fileName); + String response = sendPost(mediaServer, "deleteRecordDirectory", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult loadMP4File(MediaServer mediaServer, String app, String stream, String datePath) { + Map param = new HashMap<>(1); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + param.put("file_path", datePath); + param.put("file_repeat", "0"); + String response = sendPost(mediaServer, "loadMP4File", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult setRecordSpeed(MediaServer mediaServer, String app, String stream, int speed, String schema) { + Map param = new HashMap<>(1); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + param.put("speed", speed); + param.put("schema", schema); + String response = sendPost(mediaServer, "setRecordSpeed", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema) { + Map param = new HashMap<>(1); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + BigDecimal bigDecimal = new BigDecimal(stamp); + param.put("stamp", bigDecimal); + param.put("schema", schema); + + String response = sendPost(mediaServer, "seekRecordStamp", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMServerFactory.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMServerFactory.java new file mode 100755 index 0000000..4349445 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMServerFactory.java @@ -0,0 +1,249 @@ +package com.genersoft.iot.vmp.media.zlm; + +import com.alibaba.fastjson2.JSONArray; +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.ZLMResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +public class ZLMServerFactory { + @Autowired + private ZLMRESTfulUtils zlmresTfulUtils; + + + /** + * 开启rtpServer + * @param mediaServerItem zlm服务实例 + * @param streamId 流Id + * @param ssrc ssrc + * @param port 端口, 0/null为使用随机 + * @param reUsePort 是否重用端口 + * @param tcpMode 0/null udp 模式,1 tcp 被动模式, 2 tcp 主动模式。 + * @return + */ + public int createRTPServer(MediaServer mediaServerItem, String app, String streamId, long ssrc, Integer port, Boolean onlyAuto, Boolean disableAudio, Boolean reUsePort, Integer tcpMode) { + int result = -1; + // 查询此rtp server 是否已经存在 + ZLMResult rtpInfoResult = zlmresTfulUtils.getRtpInfo(mediaServerItem, streamId); + if(rtpInfoResult.getCode() == 0){ + if (rtpInfoResult.getExist() != null && rtpInfoResult.getExist()) { + result = rtpInfoResult.getLocal_port(); + if (result == 0) { + // 此时说明rtpServer已经创建但是流还没有推上来 + // 此时重新打开rtpServer + Map param = new HashMap<>(); + param.put("stream_id", streamId); + ZLMResult zlmResult = zlmresTfulUtils.closeRtpServer(mediaServerItem, param); + if (zlmResult != null ) { + if (zlmResult.getCode() == 0) { + return createRTPServer(mediaServerItem, streamId, app, ssrc, port,onlyAuto, reUsePort,disableAudio, tcpMode); + }else { + log.warn("[开启rtpServer], 重启RtpServer错误"); + } + } + } + return result; + } + }else if(rtpInfoResult.getCode() == -2){ + return result; + } + + Map param = new HashMap<>(); + + if (tcpMode == null) { + tcpMode = 0; + } + param.put("tcp_mode", tcpMode); + param.put("app", app); + param.put("stream_id", streamId); + if (disableAudio != null) { + param.put("only_track", disableAudio?2:0); + } + + if (reUsePort != null) { + param.put("re_use_port", reUsePort?"1":"0"); + } + // 推流端口设置0则使用随机端口 + if (port == null) { + param.put("port", 0); + }else { + param.put("port", port); + } + if (onlyAuto != null) { + param.put("only_audio", onlyAuto?"1":"0"); + } + if (ssrc != 0) { + param.put("ssrc", ssrc); + } + + ZLMResult zlmResult = zlmresTfulUtils.openRtpServer(mediaServerItem, param); + if (zlmResult != null) { + if (zlmResult.getCode() == 0) { + result= zlmResult.getPort(); + }else { + log.error("创建RTP Server 失败 {}: ", zlmResult.getMsg()); + } + }else { + // 检查ZLM状态 + log.error("创建RTP Server 失败 {}: 请检查ZLM服务", param.get("port")); + } + return result; + } + + public boolean closeRtpServer(MediaServer serverItem, String streamId) { + boolean result = false; + if (serverItem !=null){ + Map param = new HashMap<>(); + param.put("stream_id", streamId); + ZLMResult zlmResult = zlmresTfulUtils.closeRtpServer(serverItem, param); + if (zlmResult != null ) { + if (zlmResult.getCode() == 0) { + result = zlmResult.getHit() >= 1; + }else { + log.error("关闭RTP Server 失败: " + zlmResult.getMsg()); + } + }else { + // 检查ZLM状态 + log.error("关闭RTP Server 失败: 请检查ZLM服务"); + } + } + return result; + } + + public void closeRtpServer(MediaServer serverItem, String streamId, CommonCallback callback) { + if (serverItem == null) { + if (callback != null) { + callback.run(false); + } + return; + } + Map param = new HashMap<>(); + param.put("stream_id", streamId); + zlmresTfulUtils.closeRtpServer(serverItem, param, zlmResult -> { + if (zlmResult.getCode() == 0) { + if (callback != null) { + callback.run(zlmResult.getHit() >= 1); + } + return; + }else { + log.error("关闭RTP Server 失败: " + zlmResult.getMsg()); + } + if (callback != null) { + callback.run(false); + } + }); + } + + + /** + * 调用zlm RESTFUL API —— startSendRtp + */ + public ZLMResult startSendRtpStream(MediaServer mediaServerItem, Mapparam) { + return zlmresTfulUtils.startSendRtp(mediaServerItem, param); + } + + /** + * 调用zlm RESTFUL API —— startSendRtpPassive + */ + public ZLMResult startSendRtpPassive(MediaServer mediaServerItem, Mapparam) { + return zlmresTfulUtils.startSendRtpPassive(mediaServerItem, param); + } + + public ZLMResult startSendRtpPassive(MediaServer mediaServerItem, Map param, ZLMRESTfulUtils.ResultCallback callback) { + return zlmresTfulUtils.startSendRtpPassive(mediaServerItem, param, callback); + } + + public ZLMResult startSendRtpTalk(MediaServer mediaServer, Map param, ZLMRESTfulUtils.ResultCallback callback) { + return zlmresTfulUtils.startSendRtpTalk(mediaServer, param, callback); + } + + /** + * 查询待转推的流是否就绪 + */ + public Boolean isStreamReady(MediaServer mediaServerItem, String app, String streamId) { + ZLMResult zlmResult = zlmresTfulUtils.getMediaList(mediaServerItem, app, streamId); + if (zlmResult == null || zlmResult.getCode() == -2) { + return null; + } + ZLMResult result = (ZLMResult) zlmResult; + return (result.getCode() == 0 + && result.getData() != null + && !result.getData().isEmpty()); + } + + public ZLMResult startSendRtp(MediaServer mediaInfo, SendRtpInfo sendRtpItem) { + String is_Udp = sendRtpItem.isTcp() ? "0" : "1"; + log.info("rtp/{}开始推流, 目标={}:{},SSRC={}", sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort(), sendRtpItem.getSsrc()); + Map param = new HashMap<>(12); + param.put("vhost","__defaultVhost__"); + param.put("app",sendRtpItem.getApp()); + param.put("stream",sendRtpItem.getStream()); + param.put("ssrc", sendRtpItem.getSsrc()); + param.put("src_port", sendRtpItem.getLocalPort()); + param.put("pt", sendRtpItem.getPt()); + param.put("use_ps", sendRtpItem.isUsePs() ? "1" : "0"); + param.put("only_audio", sendRtpItem.isOnlyAudio() ? "1" : "0"); + if (!sendRtpItem.isTcp()) { + // udp模式下开启rtcp保活 + param.put("udp_rtcp_timeout", sendRtpItem.isRtcp()? "1":"0"); + } + + if (mediaInfo == null) { + return null; + } + // 如果是非严格模式,需要关闭端口占用 + ZLMResult zlmResult = null; + if (sendRtpItem.getLocalPort() != 0) { + if (sendRtpItem.isTcpActive()) { + zlmResult = startSendRtpPassive(mediaInfo, param); + }else { + param.put("is_udp", is_Udp); + param.put("dst_url", sendRtpItem.getIp()); + param.put("dst_port", sendRtpItem.getPort()); + zlmResult = startSendRtpStream(mediaInfo, param); + } + }else { + if (sendRtpItem.isTcpActive()) { + zlmResult = startSendRtpPassive(mediaInfo, param); + }else { + param.put("is_udp", is_Udp); + param.put("dst_url", sendRtpItem.getIp()); + param.put("dst_port", sendRtpItem.getPort()); + zlmResult = startSendRtpStream(mediaInfo, param); + } + } + return zlmResult; + } + + public Boolean updateRtpServerSSRC(MediaServer mediaServerItem, String streamId, String ssrc) { + boolean result = false; + ZLMResult zlmResult = zlmresTfulUtils.updateRtpServerSSRC(mediaServerItem, streamId, ssrc); + if (zlmResult.getCode() == 0) { + result= true; + log.info("[更新RTPServer] 成功"); + } else { + log.error("[更新RTPServer] 失败: {}, streamId:{},ssrc:{}", zlmResult.getMsg(), + streamId, ssrc); + } + return result; + } + + public ZLMResult stopSendRtpStream(MediaServer mediaServerItem, SendRtpInfo sendRtpItem) { + Map param = new HashMap<>(); + param.put("vhost", "__defaultVhost__"); + param.put("app", sendRtpItem.getApp()); + param.put("stream", sendRtpItem.getStream()); + param.put("ssrc", sendRtpItem.getSsrc()); + return zlmresTfulUtils.stopSendRtp(mediaServerItem, param); + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ChannelOnlineEvent.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ChannelOnlineEvent.java new file mode 100755 index 0000000..207ad4b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ChannelOnlineEvent.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; + +import java.text.ParseException; + +/** + * @author lin + */ +public interface ChannelOnlineEvent { + + void run(SendRtpInfo sendRtpItem) throws ParseException; +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/FlagData.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/FlagData.java new file mode 100644 index 0000000..a5bbdf7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/FlagData.java @@ -0,0 +1,8 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +import lombok.Data; + +@Data +public class FlagData { + private boolean flag; +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/RtpServerResult.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/RtpServerResult.java new file mode 100644 index 0000000..f7e61d6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/RtpServerResult.java @@ -0,0 +1,10 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +import lombok.Data; + +@Data +public class RtpServerResult { + private Integer port; + private String stream_id; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ServerKeepaliveData.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ServerKeepaliveData.java new file mode 100755 index 0000000..0cc81f2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ServerKeepaliveData.java @@ -0,0 +1,4 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +public class ServerKeepaliveData { +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/SessionData.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/SessionData.java new file mode 100644 index 0000000..fb8a33b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/SessionData.java @@ -0,0 +1,14 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +import lombok.Data; + +@Data +public class SessionData { + private String id; + private String local_ip; + private Integer local_port; + private String peer_ip; + private Integer peer_port; + private String typeid; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamAuthorityInfo.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamAuthorityInfo.java new file mode 100755 index 0000000..8b722a9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamAuthorityInfo.java @@ -0,0 +1,118 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; + +/** + * 流的鉴权信息 + * @author lin + */ +public class StreamAuthorityInfo { + + private String id; + private String app; + private String stream; + + /** + * 产生源类型, + * unknown = 0, + * rtmp_push=1, + * rtsp_push=2, + * rtp_push=3, + * pull=4, + * ffmpeg_pull=5, + * mp4_vod=6, + * device_chn=7 + */ + private int originType; + + /** + * 产生源类型的字符串描述 + */ + private String originTypeStr; + + /** + * 推流时自定义的播放鉴权ID + */ + private String callId; + + /** + * 推流的鉴权签名 + */ + private String sign; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public int getOriginType() { + return originType; + } + + public void setOriginType(int originType) { + this.originType = originType; + } + + public String getOriginTypeStr() { + return originTypeStr; + } + + public void setOriginTypeStr(String originTypeStr) { + this.originTypeStr = originTypeStr; + } + + public String getCallId() { + return callId; + } + + public void setCallId(String callId) { + this.callId = callId; + } + + public String getSign() { + return sign; + } + + public void setSign(String sign) { + this.sign = sign; + } + + public static StreamAuthorityInfo getInstanceByHook(String app, String stream, String id) { + StreamAuthorityInfo streamAuthorityInfo = new StreamAuthorityInfo(); + streamAuthorityInfo.setApp(app); + streamAuthorityInfo.setStream(stream); + streamAuthorityInfo.setId(id); + return streamAuthorityInfo; + } + + public static StreamAuthorityInfo getInstanceByHook(MediaArrivalEvent event) { + StreamAuthorityInfo streamAuthorityInfo = new StreamAuthorityInfo(); + streamAuthorityInfo.setApp(event.getApp()); + streamAuthorityInfo.setStream(event.getStream()); + streamAuthorityInfo.setId(event.getMediaServer().getId()); + if (event.getMediaInfo() != null) { + streamAuthorityInfo.setOriginType(event.getMediaInfo().getOriginType()); + } + + return streamAuthorityInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamProxyResult.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamProxyResult.java new file mode 100644 index 0000000..7276582 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamProxyResult.java @@ -0,0 +1,9 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +import lombok.Data; + +@Data +public class StreamProxyResult { + + private String key; +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMResult.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMResult.java new file mode 100644 index 0000000..5dd1838 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMResult.java @@ -0,0 +1,58 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +import lombok.Data; + +@Data +public class ZLMResult { + private int code; + private String msg; + private T data; + + + private Boolean online; + private Boolean exist; + private String peer_ip; + private Integer peer_port; + private String local_ip; + private Integer local_port; + private Integer changed; + private Integer port; + private Integer hit; + + public static ZLMResult getFailForMediaServer() { + ZLMResult zlmResult = new ZLMResult<>(); + zlmResult.setCode(-2); + zlmResult.setMsg("流媒体调用失败"); + return zlmResult; + } + + public static ZLMResult getMediaServer(int code, String msg) { + return getMediaServer(code, msg, null); + } + + public static ZLMResult getMediaServer(int code, String msg, T data) { + ZLMResult zlmResult = new ZLMResult<>(); + zlmResult.setCode(code); + zlmResult.setMsg(msg); + zlmResult.setData(data); + return zlmResult; + } + + @Override + public String toString() { + return "ZLMResult{" + + "code=" + code + + ", msg='" + msg + '\'' + + ", data=" + data + + (online != null ? (", online=" + online) : "") + + (exist != null ? (", exist=" + exist) : "") + + (peer_ip != null ? (", peer_ip=" + peer_ip) : "") + + (peer_port != null ? (", peer_port=" + peer_port) : "") + + (local_ip != null ? (", local_ip=" + local_ip) : "") + + (local_port != null ? (", local_port=" + local_port) : "") + + (changed != null ? (", changed=" + changed) : "") + + (port != null ? (", port=" + port) : "") + + (hit != null ? (", hit=" + hit) : "") + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMRunInfo.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMRunInfo.java new file mode 100755 index 0000000..624e4e1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMRunInfo.java @@ -0,0 +1,33 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +/** + * 记录zlm运行中一些参数 + */ +public class ZLMRunInfo { + + /** + * zlm当前流数量 + */ + private int mediaCount; + + /** + * 在线状态 + */ + private boolean online; + + public int getMediaCount() { + return mediaCount; + } + + public void setMediaCount(int mediaCount) { + this.mediaCount = mediaCount; + } + + public boolean isOnline() { + return online; + } + + public void setOnline(boolean online) { + this.online = online; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMServerConfig.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMServerConfig.java new file mode 100755 index 0000000..84ebc3e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMServerConfig.java @@ -0,0 +1,341 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.genersoft.iot.vmp.media.zlm.dto.hook.HookParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class ZLMServerConfig extends HookParam { + + @JSONField(name = "api.apiDebug") + private String apiDebug; + + @JSONField(name = "api.secret") + private String apiSecret; + + @JSONField(name = "api.snapRoot") + private String apiSnapRoot; + + @JSONField(name = "api.defaultSnap") + private String apiDefaultSnap; + + @JSONField(name = "ffmpeg.bin") + private String ffmpegBin; + + @JSONField(name = "ffmpeg.cmd") + private String ffmpegCmd; + + @JSONField(name = "ffmpeg.snap") + private String ffmpegSnap; + + @JSONField(name = "ffmpeg.log") + private String ffmpegLog; + + @JSONField(name = "ffmpeg.restart_sec") + private String ffmpegRestartSec; + + @JSONField(name = "protocol.modify_stamp") + private String protocolModifyStamp; + + @JSONField(name = "protocol.enable_audio") + private String protocolEnableAudio; + + @JSONField(name = "protocol.add_mute_audio") + private String protocolAddMuteAudio; + + @JSONField(name = "protocol.continue_push_ms") + private String protocolContinuePushMs; + + @JSONField(name = "protocol.enable_hls") + private String protocolEnableHls; + + @JSONField(name = "protocol.enable_mp4") + private String protocolEnableMp4; + + @JSONField(name = "protocol.enable_rtsp") + private String protocolEnableRtsp; + + @JSONField(name = "protocol.enable_rtmp") + private String protocolEnableRtmp; + + @JSONField(name = "protocol.enable_ts") + private String protocolEnableTs; + + @JSONField(name = "protocol.enable_fmp4") + private String protocolEnableFmp4; + + @JSONField(name = "protocol.mp4_as_player") + private String protocolMp4AsPlayer; + + @JSONField(name = "protocol.mp4_max_second") + private String protocolMp4MaxSecond; + + @JSONField(name = "protocol.mp4_save_path") + private String protocolMp4SavePath; + + @JSONField(name = "protocol.hls_save_path") + private String protocolHlsSavePath; + + @JSONField(name = "protocol.hls_demand") + private String protocolHlsDemand; + + @JSONField(name = "protocol.rtsp_demand") + private String protocolRtspDemand; + + @JSONField(name = "protocol.rtmp_demand") + private String protocolRtmpDemand; + + @JSONField(name = "protocol.ts_demand") + private String protocolTsDemand; + + @JSONField(name = "protocol.fmp4_demand") + private String protocolFmp4Demand; + + @JSONField(name = "general.enableVhost") + private String generalEnableVhost; + + @JSONField(name = "general.flowThreshold") + private String generalFlowThreshold; + + @JSONField(name = "general.maxStreamWaitMS") + private String generalMaxStreamWaitMS; + + @JSONField(name = "general.streamNoneReaderDelayMS") + private int generalStreamNoneReaderDelayMS; + + @JSONField(name = "general.resetWhenRePlay") + private String generalResetWhenRePlay; + + @JSONField(name = "general.mergeWriteMS") + private String generalMergeWriteMS; + + @JSONField(name = "general.mediaServerId") + private String generalMediaServerId; + + @JSONField(name = "general.wait_track_ready_ms") + private String generalWaitTrackReadyMs; + + @JSONField(name = "general.wait_add_track_ms") + private String generalWaitAddTrackMs; + + @JSONField(name = "general.unready_frame_cache") + private String generalUnreadyFrameCache; + + + @JSONField(name = "ip") + private String ip; + + private String sdpIp; + + private String streamIp; + + private String hookIp; + + private String updateTime; + + private String createTime; + + @JSONField(name = "hls.fileBufSize") + private String hlsFileBufSize; + + @JSONField(name = "hls.filePath") + private String hlsFilePath; + + @JSONField(name = "hls.segDur") + private String hlsSegDur; + + @JSONField(name = "hls.segNum") + private String hlsSegNum; + + @JSONField(name = "hls.segRetain") + private String hlsSegRetain; + + @JSONField(name = "hls.broadcastRecordTs") + private String hlsBroadcastRecordTs; + + @JSONField(name = "hls.deleteDelaySec") + private String hlsDeleteDelaySec; + + @JSONField(name = "hls.segKeep") + private String hlsSegKeep; + + @JSONField(name = "hook.access_file_except_hls") + private String hookAccessFileExceptHLS; + + @JSONField(name = "hook.admin_params") + private String hookAdminParams; + + @JSONField(name = "hook.alive_interval") + private Float hookAliveInterval; + + @JSONField(name = "hook.enable") + private String hookEnable; + + @JSONField(name = "hook.on_flow_report") + private String hookOnFlowReport; + + @JSONField(name = "hook.on_http_access") + private String hookOnHttpAccess; + + @JSONField(name = "hook.on_play") + private String hookOnPlay; + + @JSONField(name = "hook.on_publish") + private String hookOnPublish; + + @JSONField(name = "hook.on_record_mp4") + private String hookOnRecordMp4; + + @JSONField(name = "hook.on_rtsp_auth") + private String hookOnRtspAuth; + + @JSONField(name = "hook.on_rtsp_realm") + private String hookOnRtspRealm; + + @JSONField(name = "hook.on_shell_login") + private String hookOnShellLogin; + + @JSONField(name = "hook.on_stream_changed") + private String hookOnStreamChanged; + + @JSONField(name = "hook.on_stream_none_reader") + private String hookOnStreamNoneReader; + + @JSONField(name = "hook.on_stream_not_found") + private String hookOnStreamNotFound; + + @JSONField(name = "hook.on_server_started") + private String hookOnServerStarted; + + @JSONField(name = "hook.on_server_keepalive") + private String hookOnServerKeepalive; + + @JSONField(name = "hook.on_send_rtp_stopped") + private String hookOnSendRtpStopped; + + @JSONField(name = "hook.on_rtp_server_timeout") + private String hookOnRtpServerTimeout; + + @JSONField(name = "hook.timeoutSec") + private String hookTimeoutSec; + + @JSONField(name = "http.charSet") + private String httpCharSet; + + @JSONField(name = "http.keepAliveSecond") + private String httpKeepAliveSecond; + + @JSONField(name = "http.maxReqCount") + private String httpMaxReqCount; + + @JSONField(name = "http.maxReqSize") + private String httpMaxReqSize; + + @JSONField(name = "http.notFound") + private String httpNotFound; + + @JSONField(name = "http.port") + private int httpPort; + + @JSONField(name = "http.rootPath") + private String httpRootPath; + + @JSONField(name = "http.sendBufSize") + private String httpSendBufSize; + + @JSONField(name = "http.sslport") + private int httpSSLport; + + @JSONField(name = "multicast.addrMax") + private String multicastAddrMax; + + @JSONField(name = "multicast.addrMin") + private String multicastAddrMin; + + @JSONField(name = "multicast.udpTTL") + private String multicastUdpTTL; + + @JSONField(name = "record.appName") + private String recordAppName; + + @JSONField(name = "record.filePath") + private String recordFilePath; + + @JSONField(name = "record.fileSecond") + private String recordFileSecond; + + @JSONField(name = "record.sampleMS") + private String recordFileSampleMS; + + @JSONField(name = "rtmp.handshakeSecond") + private String rtmpHandshakeSecond; + + @JSONField(name = "rtmp.keepAliveSecond") + private String rtmpKeepAliveSecond; + + @JSONField(name = "rtmp.modifyStamp") + private String rtmpModifyStamp; + + @JSONField(name = "rtmp.port") + private int rtmpPort; + + @JSONField(name = "rtmp.sslport") + private int rtmpSslPort; + + @JSONField(name = "rtp.audioMtuSize") + private String rtpAudioMtuSize; + + @JSONField(name = "rtp.clearCount") + private String rtpClearCount; + + @JSONField(name = "rtp.cycleMS") + private String rtpCycleMS; + + @JSONField(name = "rtp.maxRtpCount") + private String rtpMaxRtpCount; + + @JSONField(name = "rtp.videoMtuSize") + private String rtpVideoMtuSize; + + @JSONField(name = "rtp_proxy.checkSource") + private String rtpProxyCheckSource; + + @JSONField(name = "rtp_proxy.dumpDir") + private String rtpProxyDumpDir; + + @JSONField(name = "rtp_proxy.port") + private int rtpProxyPort; + + @JSONField(name = "rtp_proxy.port_range") + private String portRange; + + @JSONField(name = "rtp_proxy.timeoutSec") + private String rtpProxyTimeoutSec; + + @JSONField(name = "rtsp.authBasic") + private String rtspAuthBasic; + + @JSONField(name = "rtsp.handshakeSecond") + private String rtspHandshakeSecond; + + @JSONField(name = "rtsp.keepAliveSecond") + private String rtspKeepAliveSecond; + + @JSONField(name = "rtsp.port") + private int rtspPort; + + @JSONField(name = "rtsp.sslport") + private int rtspSSlport; + + @JSONField(name = "shell.maxReqSize") + private String shellMaxReqSize; + + @JSONField(name = "shell.shell") + private String shellPhell; + + @JSONField(name = "transcode.suffix") + private String transcodeSuffix; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookParam.java new file mode 100755 index 0000000..8ae9e6f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookParam.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +import lombok.Data; + +/** + * zlm hook事件的参数 + * @author lin + */ +@Data +public class HookParam { + private String mediaServerId; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResult.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResult.java new file mode 100755 index 0000000..dee9d66 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResult.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +public class HookResult { + + private int code; + private String msg; + + + public HookResult() { + } + + public HookResult(int code, String msg) { + this.code = code; + this.msg = msg; + } + + public static HookResult SUCCESS(){ + return new HookResult(0, "success"); + } + + public static HookResultForOnPublish Fail(){ + return new HookResultForOnPublish(-1, "fail"); + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResultForOnPublish.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResultForOnPublish.java new file mode 100755 index 0000000..3777e19 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResultForOnPublish.java @@ -0,0 +1,52 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +import com.genersoft.iot.vmp.media.bean.ResultForOnPublish; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class HookResultForOnPublish extends HookResult{ + + private boolean enable_audio; + private boolean enable_mp4; + private int mp4_max_second; + private String mp4_save_path; + private String stream_replace; + private Integer modify_stamp; + + public HookResultForOnPublish() { + } + + public static HookResultForOnPublish SUCCESS(){ + return new HookResultForOnPublish(0, "success"); + } + + public static HookResultForOnPublish getInstance(ResultForOnPublish resultForOnPublish){ + HookResultForOnPublish successResult = new HookResultForOnPublish(0, "success"); + successResult.setEnable_audio(resultForOnPublish.isEnable_audio()); + successResult.setEnable_mp4(resultForOnPublish.isEnable_mp4()); + successResult.setModify_stamp(resultForOnPublish.getModify_stamp()); + successResult.setStream_replace(resultForOnPublish.getStream_replace()); + successResult.setMp4_max_second(resultForOnPublish.getMp4_max_second()); + successResult.setMp4_save_path(resultForOnPublish.getMp4_save_path()); + return successResult; + } + + public HookResultForOnPublish(int code, String msg) { + setCode(code); + setMsg(msg); + } + + @Override + public String toString() { + return "HookResultForOnPublish{" + + "enable_audio=" + enable_audio + + ", enable_mp4=" + enable_mp4 + + ", mp4_max_second=" + mp4_max_second + + ", mp4_save_path='" + mp4_save_path + '\'' + + ", stream_replace='" + stream_replace + '\'' + + ", modify_stamp='" + modify_stamp + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnPlayHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnPlayHookParam.java new file mode 100755 index 0000000..6e41cce --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnPlayHookParam.java @@ -0,0 +1,95 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +/** + * zlm hook事件中的on_play事件的参数 + * @author lin + */ +public class OnPlayHookParam extends HookParam{ + private String id; + private String app; + private String stream; + private String ip; + private String params; + private int port; + private String schema; + private String vhost; + + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public String getParams() { + return params; + } + + public void setParams(String params) { + this.params = params; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public String getVhost() { + return vhost; + } + + public void setVhost(String vhost) { + this.vhost = vhost; + } + + @Override + public String toString() { + return "OnPlayHookParam{" + + "id='" + id + '\'' + + ", app='" + app + '\'' + + ", stream='" + stream + '\'' + + ", ip='" + ip + '\'' + + ", params='" + params + '\'' + + ", port=" + port + + ", schema='" + schema + '\'' + + ", vhost='" + vhost + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnPublishHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnPublishHookParam.java new file mode 100755 index 0000000..e117213 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnPublishHookParam.java @@ -0,0 +1,59 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +import lombok.Getter; +import lombok.Setter; + +/** + * zlm hook事件中的on_publish事件的参数 + * @author lin + */ + +public class OnPublishHookParam extends HookParam{ + + @Getter + @Setter + private String id; + + @Getter + @Setter + private String app; + + @Getter + @Setter + private String stream; + + @Getter + @Setter + private String ip; + + @Getter + @Setter + private String params; + + @Getter + @Setter + private int port; + + @Getter + @Setter + private String schema; + + @Getter + @Setter + private String vhost; + + + @Override + public String toString() { + return "OnPublishHookParam{" + + "id='" + id + '\'' + + ", app='" + app + '\'' + + ", stream='" + stream + '\'' + + ", ip='" + ip + '\'' + + ", params='" + params + '\'' + + ", port=" + port + + ", schema='" + schema + '\'' + + ", vhost='" + vhost + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnRecordMp4HookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnRecordMp4HookParam.java new file mode 100755 index 0000000..deeeff4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnRecordMp4HookParam.java @@ -0,0 +1,124 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +/** + * zlm hook事件中的on_rtp_server_timeout事件的参数 + * @author lin + */ +public class OnRecordMp4HookParam extends HookParam{ + private String app; + private String stream; + private String file_name; + private String file_path; + private long file_size; + private String folder; + private String url; + private String vhost; + private long start_time; + private double time_len; + private String params; + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getFile_name() { + return file_name; + } + + public void setFile_name(String file_name) { + this.file_name = file_name; + } + + public String getFile_path() { + return file_path; + } + + public void setFile_path(String file_path) { + this.file_path = file_path; + } + + public long getFile_size() { + return file_size; + } + + public void setFile_size(long file_size) { + this.file_size = file_size; + } + + public String getFolder() { + return folder; + } + + public void setFolder(String folder) { + this.folder = folder; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getVhost() { + return vhost; + } + + public void setVhost(String vhost) { + this.vhost = vhost; + } + + public long getStart_time() { + return start_time; + } + + public void setStart_time(long start_time) { + this.start_time = start_time; + } + + public double getTime_len() { + return time_len; + } + + public void setTime_len(double time_len) { + this.time_len = time_len; + } + + public String getParams() { + return params; + } + + public void setParams(String params) { + this.params = params; + } + + @Override + public String toString() { + return "OnRecordMp4HookParam{" + + "app='" + app + '\'' + + ", stream='" + stream + '\'' + + ", file_name='" + file_name + '\'' + + ", file_path='" + file_path + '\'' + + ", file_size='" + file_size + '\'' + + ", folder='" + folder + '\'' + + ", url='" + url + '\'' + + ", vhost='" + vhost + '\'' + + ", start_time=" + start_time + + ", time_len=" + time_len + + ", params=" + params + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnRtpServerTimeoutHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnRtpServerTimeoutHookParam.java new file mode 100755 index 0000000..6179ce4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnRtpServerTimeoutHookParam.java @@ -0,0 +1,64 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +/** + * zlm hook事件中的on_rtp_server_timeout事件的参数 + * @author lin + */ +public class OnRtpServerTimeoutHookParam extends HookParam{ + private int local_port; + private String stream_id; + private int tcpMode; + private boolean re_use_port; + private String ssrc; + + public int getLocal_port() { + return local_port; + } + + public void setLocal_port(int local_port) { + this.local_port = local_port; + } + + public String getStream_id() { + return stream_id; + } + + public void setStream_id(String stream_id) { + this.stream_id = stream_id; + } + + public int getTcpMode() { + return tcpMode; + } + + public void setTcpMode(int tcpMode) { + this.tcpMode = tcpMode; + } + + public boolean isRe_use_port() { + return re_use_port; + } + + public void setRe_use_port(boolean re_use_port) { + this.re_use_port = re_use_port; + } + + public String getSsrc() { + return ssrc; + } + + public void setSsrc(String ssrc) { + this.ssrc = ssrc; + } + + @Override + public String toString() { + return "OnRtpServerTimeoutHookParam{" + + "local_port=" + local_port + + ", stream_id='" + stream_id + '\'' + + ", tcpMode=" + tcpMode + + ", re_use_port=" + re_use_port + + ", ssrc='" + ssrc + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnSendRtpStoppedHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnSendRtpStoppedHookParam.java new file mode 100755 index 0000000..4989b4a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnSendRtpStoppedHookParam.java @@ -0,0 +1,35 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +/** + * zlm hook事件中的on_send_rtp_stopped事件的参数 + * @author lin + */ +public class OnSendRtpStoppedHookParam extends HookParam{ + private String app; + private String stream; + + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + @Override + public String toString() { + return "OnSendRtpStoppedHookParam{" + + "app='" + app + '\'' + + ", stream='" + stream + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnServerKeepaliveHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnServerKeepaliveHookParam.java new file mode 100755 index 0000000..5439f20 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnServerKeepaliveHookParam.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +import com.genersoft.iot.vmp.media.zlm.dto.ServerKeepaliveData; + +/** + * zlm hook事件中的on_play事件的参数 + * @author lin + */ +public class OnServerKeepaliveHookParam extends HookParam{ + + private ServerKeepaliveData data; + + public ServerKeepaliveData getData() { + return data; + } + + public void setData(ServerKeepaliveData data) { + this.data = data; + } + + @Override + public String toString() { + return "OnServerKeepaliveHookParam{" + + "data=" + data + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamChangedHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamChangedHookParam.java new file mode 100755 index 0000000..5ed378e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamChangedHookParam.java @@ -0,0 +1,220 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; +import java.util.Map; + +/** + * @author lin + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class OnStreamChangedHookParam extends HookParam{ + + /** + * 注册/注销 + */ + private boolean regist; + + /** + * 应用名 + */ + private String app; + + /** + * 流id + */ + private String stream; + + /** + * 推流鉴权Id + */ + private String callId; + + /** + * 观看总人数,包括hls/rtsp/rtmp/http-flv/ws-flv + */ + private int totalReaderCount; + + /** + * 协议 包括hls/rtsp/rtmp/http-flv/ws-flv + */ + private String schema; + + + /** + * 产生源类型, + * unknown = 0, + * rtmp_push=1, + * rtsp_push=2, + * rtp_push=3, + * pull=4, + * ffmpeg_pull=5, + * mp4_vod=6, + * device_chn=7 + */ + private int originType; + + /** + * 客户端和服务器网络信息,可能为null类型 + */ + private OriginSock originSock; + + /** + * 产生源类型的字符串描述 + */ + private String originTypeStr; + + /** + * 产生源的url + */ + private String originUrl; + + /** + * 服务器id + */ + private String severId; + + /** + * GMT unix系统时间戳,单位秒 + */ + private Long createStamp; + + /** + * 存活时间,单位秒 + */ + private Long aliveSecond; + + /** + * 数据产生速度,单位byte/s + */ + private Long bytesSpeed; + + /** + * 音视频轨道 + */ + private List tracks; + + /** + * 音视频轨道 + */ + private String vhost; + + /** + * 额外的参数字符串 + */ + private String params; + + /** + * 额外的参数 + */ + private Map paramMap; + + /** + * 是否是docker部署, docker部署不会自动更新zlm使用的端口,需要自己手动修改 + */ + private boolean docker; + + @Data + public static class MediaTrack { + /** + * 音频通道数 + */ + private int channels; + + /** + * H264 = 0, H265 = 1, AAC = 2, G711A = 3, G711U = 4 + */ + private int codec_id; + + /** + * 编码类型名称 CodecAAC CodecH264 + */ + private String codec_id_name; + + /** + * Video = 0, Audio = 1 + */ + private int codec_type; + + /** + * 轨道是否准备就绪 + */ + private boolean ready; + + /** + * 音频采样位数 + */ + private int sample_bit; + + /** + * 音频采样率 + */ + private int sample_rate; + + /** + * 视频fps + */ + private float fps; + + /** + * 视频高 + */ + private int height; + + /** + * 视频宽 + */ + private int width; + + /** + * 帧数 + */ + private int frames; + + /** + * 关键帧数 + */ + private int key_frames; + + /** + * GOP大小 + */ + private int gop_size; + + /** + * GOP间隔时长(ms) + */ + private int gop_interval_ms; + + /** + * 丢帧率 + */ + private float loss; + } + + @Data + public static class OriginSock{ + private String identifier; + private String local_ip; + private int local_port; + private String peer_ip; + private int peer_port; + + } + + private StreamContent streamInfo; + + @Override + public String toString() { + return "OnStreamChangedHookParam{" + + "regist=" + regist + + ", app='" + app + '\'' + + ", stream='" + stream + '\'' + + ", severId='" + severId + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamNoneReaderHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamNoneReaderHookParam.java new file mode 100755 index 0000000..3b62842 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamNoneReaderHookParam.java @@ -0,0 +1,51 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +public class OnStreamNoneReaderHookParam extends HookParam{ + + private String schema; + private String app; + private String stream; + private String vhost; + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getVhost() { + return vhost; + } + + public void setVhost(String vhost) { + this.vhost = vhost; + } + + @Override + public String toString() { + return "OnStreamNoneReaderHookParam{" + + "schema='" + schema + '\'' + + ", app='" + app + '\'' + + ", stream='" + stream + '\'' + + ", vhost='" + vhost + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamNotFoundHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamNotFoundHookParam.java new file mode 100755 index 0000000..76e6a72 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamNotFoundHookParam.java @@ -0,0 +1,95 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +/** + * zlm hook事件中的on_stream_not_found事件的参数 + * @author lin + */ +public class OnStreamNotFoundHookParam extends HookParam{ + private String id; + private String app; + private String stream; + private String ip; + private String params; + private int port; + private String schema; + private String vhost; + + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public String getParams() { + return params; + } + + public void setParams(String params) { + this.params = params; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public String getVhost() { + return vhost; + } + + public void setVhost(String vhost) { + this.vhost = vhost; + } + + @Override + public String toString() { + return "OnStreamNotFoundHookParam{" + + "id='" + id + '\'' + + ", app='" + app + '\'' + + ", stream='" + stream + '\'' + + ", ip='" + ip + '\'' + + ", params='" + params + '\'' + + ", port=" + port + + ", schema='" + schema + '\'' + + ", vhost='" + vhost + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OriginType.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OriginType.java new file mode 100755 index 0000000..926cf4d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OriginType.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +public enum OriginType { + // 不可调整顺序 + UNKNOWN("UNKNOWN"), + RTMP_PUSH("PUSH"), + RTSP_PUSH("PUSH"), + RTP_PUSH("RTP"), + PULL("PULL"), + FFMPEG_PULL("PULL"), + MP4_VOD("MP4_VOD"), + DEVICE_CHN("DEVICE_CHN"), + RTC_PUSH("PUSH"); + + private final String type; + OriginType(String type) { + this.type = type; + } + + public String getType() { + return type; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/event/HookZlmServerKeepaliveEvent.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/event/HookZlmServerKeepaliveEvent.java new file mode 100644 index 0000000..b927062 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/event/HookZlmServerKeepaliveEvent.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.media.zlm.event; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import org.springframework.context.ApplicationEvent; + +/** + * zlm 心跳事件 + */ +public class HookZlmServerKeepaliveEvent extends ApplicationEvent { + + public HookZlmServerKeepaliveEvent(Object source) { + super(source); + } + + private MediaServer mediaServerItem; + + public MediaServer getMediaServerItem() { + return mediaServerItem; + } + + public void setMediaServerItem(MediaServer mediaServerItem) { + this.mediaServerItem = mediaServerItem; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/event/HookZlmServerStartEvent.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/event/HookZlmServerStartEvent.java new file mode 100644 index 0000000..e1c28b1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/event/HookZlmServerStartEvent.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.media.zlm.event; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import org.springframework.context.ApplicationEvent; + +/** + * zlm server_start事件 + */ +public class HookZlmServerStartEvent extends ApplicationEvent { + + public HookZlmServerStartEvent(Object source) { + super(source); + } + + private MediaServer mediaServerItem; + + public MediaServer getMediaServerItem() { + return mediaServerItem; + } + + public void setMediaServerItem(MediaServer mediaServerItem) { + this.mediaServerItem = mediaServerItem; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/ICloudRecordService.java b/src/main/java/com/genersoft/iot/vmp/service/ICloudRecordService.java new file mode 100755 index 0000000..80e865a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/ICloudRecordService.java @@ -0,0 +1,76 @@ +package com.genersoft.iot.vmp.service; + +import com.alibaba.fastjson2.JSONArray; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.CloudRecordItem; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.cloudRecord.bean.CloudRecordUrl; +import com.github.pagehelper.PageInfo; + +import java.util.List; +import java.util.Set; + +/** + * 云端录像管理 + * @author lin + */ +public interface ICloudRecordService { + + /** + * 分页回去云端录像列表 + */ + PageInfo getList(int page, int count, String query, String app, String stream, String startTime, String endTime, List mediaServerItems, String callId, Boolean ascOrder); + + /** + * 获取所有的日期 + */ + List getDateList(String app, String stream, int year, int month, List mediaServerItems); + + /** + * 添加合并任务 + */ + String addTask(String app, String stream, MediaServer mediaServerItem, String startTime, + String endTime, String callId, String remoteHost, boolean filterMediaServer); + + + /** + * 查询合并任务列表 + */ + JSONArray queryTask(String app, String stream, String callId, String taskId, String mediaServerId, Boolean isEnd, String scheme); + + /** + * 收藏视频,收藏的视频过期不会删除 + */ + int changeCollect(boolean result, String app, String stream, String mediaServerId, String startTime, String endTime, String callId); + + /** + * 添加指定录像收藏 + */ + int changeCollectById(Integer recordId, boolean result); + + /** + * 获取播放地址 + */ + DownloadFileInfo getPlayUrlPath(Integer recordId); + + List getAllList(String query, String app, String stream, String startTime, String endTime, List mediaServerItems, String callId, List ids); + + /** + * 加载录像文件,形成录像流 + */ + void loadMP4FileForDate(String app, String stream, String date, ErrorCallback callback); + + void seekRecord(String mediaServerId,String app, String stream, Double seek, String schema); + + void setRecordSpeed(String mediaServerId, String app, String stream, Integer speed, String schema); + + void deleteFileByIds(Set ids); + + void loadMP4File(String app, String stream, int cloudRecordId, ErrorCallback callback); + + List getUrlListByIds(List ids); + + List getUrlList(String app, String stream, String callId); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/ILogService.java b/src/main/java/com/genersoft/iot/vmp/service/ILogService.java new file mode 100644 index 0000000..ef6161c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/ILogService.java @@ -0,0 +1,12 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.service.bean.LogFileInfo; + +import java.io.File; +import java.util.List; + +public interface ILogService { + List queryList(String query, String startTime, String endTime); + + File getFileByName(String fileName); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IMapService.java b/src/main/java/com/genersoft/iot/vmp/service/IMapService.java new file mode 100644 index 0000000..bc80673 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IMapService.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.vmanager.bean.MapConfig; +import com.genersoft.iot.vmp.vmanager.bean.MapModelIcon; + +import java.util.List; + +public interface IMapService { + + List getConfig(); + + List getModelList(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IMediaService.java b/src/main/java/com/genersoft/iot/vmp/service/IMediaService.java new file mode 100755 index 0000000..26f1585 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IMediaService.java @@ -0,0 +1,19 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.media.bean.ResultForOnPublish; +import com.genersoft.iot.vmp.media.bean.MediaServer; + +/** + * 媒体信息业务 + */ +public interface IMediaService { + + /** + * 播放鉴权 + */ + boolean authenticatePlay(String app, String stream, String callId); + + ResultForOnPublish authenticatePublish(MediaServer mediaServer, String app, String stream, String params); + + boolean closeStreamOnNoneReader(String mediaServerId, String app, String stream, String schema); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IMobilePositionService.java b/src/main/java/com/genersoft/iot/vmp/service/IMobilePositionService.java new file mode 100644 index 0000000..1643ad7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IMobilePositionService.java @@ -0,0 +1,21 @@ +package com.genersoft.iot.vmp.service; + + +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import com.genersoft.iot.vmp.gb28181.bean.Platform; + +import java.util.List; + +public interface IMobilePositionService { + + void add(List mobilePositionList); + + void add(MobilePosition mobilePosition); + + List queryMobilePositions(String deviceId, String channelId, String startTime, String endTime); + + List queryEnablePlatformListWithAsMessageChannel(); + + MobilePosition queryLatestPosition(String deviceId); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IReceiveRtpServerService.java b/src/main/java/com/genersoft/iot/vmp/service/IReceiveRtpServerService.java new file mode 100644 index 0000000..caf2b90 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IReceiveRtpServerService.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.gb28181.bean.OpenRTPServerResult; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.RTPServerParam; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; + +public interface IReceiveRtpServerService { + SSRCInfo openRTPServer(RTPServerParam rtpServerParam, ErrorCallback callback); + + void closeRTPServer(MediaServer mediaServer, SSRCInfo ssrcInfo); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java b/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java new file mode 100644 index 0000000..f3b3491 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.service.bean.RecordPlan; +import com.github.pagehelper.PageInfo; + +import java.util.List; + +public interface IRecordPlanService { + + + RecordPlan get(Integer planId); + + void update(RecordPlan plan); + + void delete(Integer planId); + + PageInfo query(Integer page, Integer count, String query); + + void add(RecordPlan plan); + + void link(List channelIds, Integer planId); + + PageInfo queryChannelList(int page, int count, String query, Integer channelType, Boolean online, Integer planId, Boolean hasLink); + + void linkAll(Integer planId); + + void cleanAll(Integer planId); + + Integer recording(String app, String stream); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IRoleService.java b/src/main/java/com/genersoft/iot/vmp/service/IRoleService.java new file mode 100755 index 0000000..d207c6a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IRoleService.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.storager.dao.dto.Role; + +import java.util.List; + +public interface IRoleService { + + Role getRoleById(int id); + + int add(Role role); + + int delete(int id); + + List getAll(); + + int update(Role role); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/ISendRtpServerService.java b/src/main/java/com/genersoft/iot/vmp/service/ISendRtpServerService.java new file mode 100644 index 0000000..ca8c33d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/ISendRtpServerService.java @@ -0,0 +1,45 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; + +import java.util.List; + +public interface ISendRtpServerService { + + SendRtpInfo createSendRtpInfo(MediaServer mediaServer, String ip, Integer port, String ssrc, String requesterId, + String deviceId, Integer channelId, Boolean isTcp, Boolean rtcp); + + SendRtpInfo createSendRtpInfo(MediaServer mediaServer, String ip, Integer port, String ssrc, String platformId, + String app, String stream, Integer channelId, Boolean tcp, Boolean rtcp); + + void update(SendRtpInfo sendRtpItem); + + SendRtpInfo queryByChannelId(Integer channelId, String targetId); + + SendRtpInfo queryByCallId(String callId); + + List queryByStream(String stream); + + SendRtpInfo queryByStream(String stream, String targetId); + + void delete(SendRtpInfo sendRtpInfo); + + void deleteByCallId(String callId); + + void deleteByStream(String Stream, String targetId); + + void deleteByChannel(Integer channelId, String targetId); + + List queryAll(); + + boolean isChannelSendingRTP(Integer channelId); + + List queryForPlatform(String platformId); + + List queryByChannelId(int id); + + void deleteByStream(String stream); + + int getNextPort(MediaServer mediaServer); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IUserApiKeyService.java b/src/main/java/com/genersoft/iot/vmp/service/IUserApiKeyService.java new file mode 100644 index 0000000..b3cc580 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IUserApiKeyService.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.storager.dao.dto.UserApiKey; +import com.github.pagehelper.PageInfo; + +public interface IUserApiKeyService { + int addApiKey(UserApiKey userApiKey); + + boolean isApiKeyExists(String apiKey); + + PageInfo getUserApiKeys(int page, int count); + + int enable(Integer id); + + int disable(Integer id); + + int remark(Integer id, String remark); + + int delete(Integer id); + + UserApiKey getUserApiKeyById(Integer id); + + int reset(Integer id, String apiKey); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IUserService.java b/src/main/java/com/genersoft/iot/vmp/service/IUserService.java new file mode 100755 index 0000000..1e9b724 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IUserService.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.storager.dao.dto.User; +import com.github.pagehelper.PageInfo; + +import java.util.List; + +public interface IUserService { + + User getUser(String username, String password); + + boolean changePassword(int id, String password); + + User getUserById(int id); + + User getUserByUsername(String username); + + int addUser(User user); + + int deleteUser(int id); + + List getAllUsers(); + + int updateUsers(User user); + + boolean checkPushAuthority(String callId, String sign); + + PageInfo getUsers(int page, int count); + + int changePushKey(int id, String pushKey); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/CloudRecordItem.java b/src/main/java/com/genersoft/iot/vmp/service/bean/CloudRecordItem.java new file mode 100644 index 0000000..01e452a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/CloudRecordItem.java @@ -0,0 +1,119 @@ +package com.genersoft.iot.vmp.service.bean; + +import com.genersoft.iot.vmp.media.event.media.MediaRecordMp4Event; +import com.genersoft.iot.vmp.media.event.media.MediaRecordProcessEvent; +import com.genersoft.iot.vmp.utils.MediaServerUtils; +import lombok.Data; + +import java.util.Map; + +/** + * 云端录像数据 + */ +@Data +public class CloudRecordItem { + /** + * 主键 + */ + private int id; + + /** + * 应用名 + */ + private String app; + + /** + * 流 + */ + private String stream; + + /** + * 健全ID + */ + private String callId; + + /** + * 开始时间 + */ + private long startTime; + + /** + * 结束时间 + */ + private long endTime; + + /** + * ZLM Id + */ + private String mediaServerId; + + /** + * 文件名称 + */ + private String fileName; + + /** + * 文件路径 + */ + private String filePath; + + /** + * 文件夹 + */ + private String folder; + + /** + * 收藏,收藏的文件不移除 + */ + private Boolean collect; + + /** + * 保留,收藏的文件不移除 + */ + private Boolean reserve; + + /** + * 文件大小 + */ + private long fileSize; + + /** + * 文件时长 + */ + private double timeLen; + + /** + * 所属服务ID + */ + private String serverId; + + public static CloudRecordItem getInstance(MediaRecordMp4Event param) { + CloudRecordItem cloudRecordItem = new CloudRecordItem(); + cloudRecordItem.setApp(param.getApp()); + cloudRecordItem.setStream(param.getStream()); + cloudRecordItem.setStartTime(param.getRecordInfo().getStartTime()); + cloudRecordItem.setFileName(param.getRecordInfo().getFileName()); + cloudRecordItem.setFolder(param.getRecordInfo().getFolder()); + cloudRecordItem.setFileSize(param.getRecordInfo().getFileSize()); + cloudRecordItem.setFilePath(param.getRecordInfo().getFilePath()); + cloudRecordItem.setMediaServerId(param.getMediaServer().getId()); + cloudRecordItem.setTimeLen(param.getRecordInfo().getTimeLen()); + cloudRecordItem.setEndTime((param.getRecordInfo().getStartTime() + (long)param.getRecordInfo().getTimeLen())); + Map paramsMap = MediaServerUtils.urlParamToMap(param.getRecordInfo().getParams()); + if (paramsMap.get("callId") != null) { + cloudRecordItem.setCallId(paramsMap.get("callId")); + } + return cloudRecordItem; + } + + public static CloudRecordItem getInstance(MediaRecordProcessEvent event) { + CloudRecordItem cloudRecordItem = new CloudRecordItem(); + cloudRecordItem.setApp(event.getApp()); + cloudRecordItem.setStream(event.getStream()); + cloudRecordItem.setFileName(event.getFileName()); + cloudRecordItem.setMediaServerId(event.getMediaServer().getId()); + cloudRecordItem.setTimeLen(event.getCurrentFileDuration() * 1000); + return cloudRecordItem; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/DownloadFileInfo.java b/src/main/java/com/genersoft/iot/vmp/service/bean/DownloadFileInfo.java new file mode 100644 index 0000000..c8e7b15 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/DownloadFileInfo.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.service.bean; + +import lombok.Data; + +@Data +public class DownloadFileInfo { + + private String httpPath; + private String httpsPath; + private String httpDomainPath; + private String httpsDomainPath; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/ErrorCallback.java b/src/main/java/com/genersoft/iot/vmp/service/bean/ErrorCallback.java new file mode 100755 index 0000000..6211d00 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/ErrorCallback.java @@ -0,0 +1,6 @@ +package com.genersoft.iot.vmp.service.bean; + +public interface ErrorCallback { + + void run(int code, String msg, T data); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/GPSMsgInfo.java b/src/main/java/com/genersoft/iot/vmp/service/bean/GPSMsgInfo.java new file mode 100755 index 0000000..eca5c5d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/GPSMsgInfo.java @@ -0,0 +1,68 @@ +package com.genersoft.iot.vmp.service.bean; + +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.Data; + +@Data +public class GPSMsgInfo { + + /** + * 通道国标ID + */ + private String id; + + /** + * 通道ID + */ + private Integer channelId; + + /** + * + */ + private String app; + + /** + * 经度 (必选) + */ + private double lng; + + /** + * 纬度 (必选) + */ + private double lat; + + /** + * 速度,单位:km/h (可选) + */ + private Double speed; + + /** + * 产生通知时间, 时间格式: 2020-01-14T14:32:12 + */ + private String time; + + /** + * 方向,取值为当前摄像头方向与正北方的顺时针夹角,取值范围0°~360°,单位:(°)(可选) + */ + private Double direction; + + /** + * 海拔高度,单位:m(可选) + */ + private Double altitude; + + private boolean stored; + + public static GPSMsgInfo getInstance(MobilePosition mobilePosition) { + GPSMsgInfo gpsMsgInfo = new GPSMsgInfo(); + gpsMsgInfo.setChannelId(mobilePosition.getChannelId()); + gpsMsgInfo.setAltitude(mobilePosition.getAltitude()); + gpsMsgInfo.setLng(mobilePosition.getLongitude()); + gpsMsgInfo.setLat(mobilePosition.getLatitude()); + gpsMsgInfo.setSpeed(mobilePosition.getSpeed()); + gpsMsgInfo.setDirection(mobilePosition.getDirection()); + gpsMsgInfo.setTime(DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(mobilePosition.getTime())); + return gpsMsgInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/InviteErrorCode.java b/src/main/java/com/genersoft/iot/vmp/service/bean/InviteErrorCode.java new file mode 100755 index 0000000..70a84c0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/InviteErrorCode.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.service.bean; + +/** + * 全局错误码 + */ +public enum InviteErrorCode { + SUCCESS(0, "成功"), + FAIL(-100, "失败"), + ERROR_FOR_SIGNALLING_TIMEOUT(-1, "信令超时"), + ERROR_FOR_STREAM_TIMEOUT(-2, "收流超时"), + ERROR_FOR_RESOURCE_EXHAUSTION(-3, "资源耗尽"), + ERROR_FOR_CATCH_DATA(-4, "缓存数据异常"), + ERROR_FOR_SIGNALLING_ERROR(-5, "收到信令错误"), + ERROR_FOR_STREAM_PARSING_EXCEPTIONS(-6, "流地址解析错误"), + ERROR_FOR_SDP_PARSING_EXCEPTIONS(-7, "SDP信息解析失败"), + ERROR_FOR_SSRC_UNAVAILABLE(-8, "SSRC不可用"), + ERROR_FOR_RESET_SSRC(-9, "重新设置收流信息失败"), + ERROR_FOR_SIP_SENDING_FAILED(-10, "命令发送失败"), + ERROR_FOR_ASSIST_NOT_READY(-11, "没有可用的assist服务"), + ERROR_FOR_PARAMETER_ERROR(-13, "参数异常"), + ERROR_FOR_TCP_ACTIVE_CONNECTION_REFUSED_ERROR(-14, "TCP主动连接失败"), + ERROR_FOR_FINISH(-20, "已结束"), + ; + + private final int code; + private final String msg; + + InviteErrorCode(int code, String msg) { + this.code = code; + this.msg = msg; + } + + public int getCode() { + return code; + } + + public String getMsg() { + return msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/InviteTimeOutCallback.java b/src/main/java/com/genersoft/iot/vmp/service/bean/InviteTimeOutCallback.java new file mode 100755 index 0000000..e30db5d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/InviteTimeOutCallback.java @@ -0,0 +1,6 @@ +package com.genersoft.iot.vmp.service.bean; + +public interface InviteTimeOutCallback { + + void run(int code, String msg); // code: 0 sip超时, 1 收流超时 +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/LogFileInfo.java b/src/main/java/com/genersoft/iot/vmp/service/bean/LogFileInfo.java new file mode 100644 index 0000000..c73cff3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/LogFileInfo.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.service.bean; + +import lombok.Data; + +@Data +public class LogFileInfo { + + private String fileName; + private Long fileSize; + private Long startTime; + private Long endTime; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/MediaServerLoad.java b/src/main/java/com/genersoft/iot/vmp/service/bean/MediaServerLoad.java new file mode 100755 index 0000000..cb30f67 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/MediaServerLoad.java @@ -0,0 +1,50 @@ +package com.genersoft.iot.vmp.service.bean; + +public class MediaServerLoad { + + private String id; + private int push; + private int proxy; + private int gbReceive; + private int gbSend; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public int getPush() { + return push; + } + + public void setPush(int push) { + this.push = push; + } + + public int getProxy() { + return proxy; + } + + public void setProxy(int proxy) { + this.proxy = proxy; + } + + public int getGbReceive() { + return gbReceive; + } + + public void setGbReceive(int gbReceive) { + this.gbReceive = gbReceive; + } + + public int getGbSend() { + return gbSend; + } + + public void setGbSend(int gbSend) { + this.gbSend = gbSend; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/MessageForPushChannel.java b/src/main/java/com/genersoft/iot/vmp/service/bean/MessageForPushChannel.java new file mode 100755 index 0000000..3c8aba5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/MessageForPushChannel.java @@ -0,0 +1,74 @@ +package com.genersoft.iot.vmp.service.bean; + +import lombok.Data; + +/** + * 当上级平台 + * @author lin + */ + +@Data +public class MessageForPushChannel { + /** + * 消息类型 + * 0 流注销 1 流注册 + */ + private int type; + + /** + * 流应用名 + */ + private String app; + + /** + * 流Id + */ + private String stream; + + /** + * 国标ID + */ + private String gbId; + + /** + * 请求的平台国标编号 + */ + private String platFormId; + + /** + * 请求的平台自增ID + */ + private int platFormIndex; + + /** + * 请求平台名称 + */ + private String platFormName; + + /** + * WVP服务ID + */ + private String serverId; + + /** + * 目标流媒体节点ID + */ + private String mediaServerId; + + + + public static MessageForPushChannel getInstance(int type, String app, String stream, String gbId, + String platFormId, String platFormName, String serverId, + String mediaServerId){ + MessageForPushChannel messageForPushChannel = new MessageForPushChannel(); + messageForPushChannel.setType(type); + messageForPushChannel.setGbId(gbId); + messageForPushChannel.setApp(app); + messageForPushChannel.setStream(stream); + messageForPushChannel.setServerId(serverId); + messageForPushChannel.setMediaServerId(mediaServerId); + messageForPushChannel.setPlatFormId(platFormId); + messageForPushChannel.setPlatFormName(platFormName); + return messageForPushChannel; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/MessageForPushChannelResponse.java b/src/main/java/com/genersoft/iot/vmp/service/bean/MessageForPushChannelResponse.java new file mode 100755 index 0000000..10d1b43 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/MessageForPushChannelResponse.java @@ -0,0 +1,71 @@ +package com.genersoft.iot.vmp.service.bean; + +/** + * 当redis回复推流结果上级平台 + * @author lin + */ +public class MessageForPushChannelResponse { + /** + * 错误玛 + * 0 成功 1 失败 + */ + private int code; + /** + * 错误内容 + */ + private String msg; + + /** + * 流应用名 + */ + private String app; + + /** + * 流Id + */ + private String stream; + + + + public static MessageForPushChannelResponse getInstance(int code, String msg, String app, String stream){ + MessageForPushChannelResponse messageForPushChannel = new MessageForPushChannelResponse(); + messageForPushChannel.setCode(code); + messageForPushChannel.setMsg(msg); + messageForPushChannel.setApp(app); + messageForPushChannel.setStream(stream); + return messageForPushChannel; + } + + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/PlayBackCallback.java b/src/main/java/com/genersoft/iot/vmp/service/bean/PlayBackCallback.java new file mode 100755 index 0000000..33a09bd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/PlayBackCallback.java @@ -0,0 +1,7 @@ +package com.genersoft.iot.vmp.service.bean; + +public interface PlayBackCallback { + + void call(PlayBackResult msg); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/PlayBackResult.java b/src/main/java/com/genersoft/iot/vmp/service/bean/PlayBackResult.java new file mode 100755 index 0000000..d7da931 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/PlayBackResult.java @@ -0,0 +1,69 @@ +package com.genersoft.iot.vmp.service.bean; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.media.bean.MediaServer; + +import java.util.EventObject; + + +/** + * @author lin + */ +public class PlayBackResult { + private int code; + + private String msg; + private T data; + private MediaServer mediaServerItem; + private JSONObject response; + private SipSubscribe.EventResult event; + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } + + public MediaServer getMediaServerItem() { + return mediaServerItem; + } + + public void setMediaServerItem(MediaServer mediaServerItem) { + this.mediaServerItem = mediaServerItem; + } + + public JSONObject getResponse() { + return response; + } + + public void setResponse(JSONObject response) { + this.response = response; + } + + public SipSubscribe.EventResult getEvent() { + return event; + } + + public void setEvent(SipSubscribe.EventResult event) { + this.event = event; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/PushStreamStatusChangeFromRedisDto.java b/src/main/java/com/genersoft/iot/vmp/service/bean/PushStreamStatusChangeFromRedisDto.java new file mode 100755 index 0000000..9e9ce35 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/PushStreamStatusChangeFromRedisDto.java @@ -0,0 +1,19 @@ +package com.genersoft.iot.vmp.service.bean; + +import lombok.Data; + +import java.util.List; + +/** + * 收到redis通知修改推流通道状态 + * @author lin + */ +@Data +public class PushStreamStatusChangeFromRedisDto { + + private boolean setAllOffline; + + private List onlineStreams; + + private List offlineStreams; +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/RTPServerParam.java b/src/main/java/com/genersoft/iot/vmp/service/bean/RTPServerParam.java new file mode 100644 index 0000000..2baf335 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/RTPServerParam.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.service.bean; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import lombok.Data; + +@Data +public class RTPServerParam { + + private MediaServer mediaServerItem; + private String streamId; + private String presetSsrc; + private boolean ssrcCheck; + private boolean playback; + private Integer port; + private boolean onlyAuto; + private boolean disableAudio; + private boolean reUsePort; + + /** + * tcp模式,0时为不启用tcp监听,1时为启用tcp监听,2时为tcp主动连接模式 + */ + private Integer tcpMode; + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlan.java b/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlan.java new file mode 100644 index 0000000..5333b2c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlan.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.service.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "录制计划") +public class RecordPlan { + + @Schema(description = "计划数据库ID") + private int id; + + @Schema(description = "计划名称") + private String name; + + @Schema(description = "计划关联通道数量") + private int channelCount; + + @Schema(description = "是否开启定时截图") + private Boolean snap; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "更新时间") + private String updateTime; + + @Schema(description = "计划内容") + private List planItemList; +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlanItem.java b/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlanItem.java new file mode 100644 index 0000000..31fa321 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlanItem.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.service.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "录制计划项") +public class RecordPlanItem { + + @Schema(description = "计划项数据库ID") + private int id; + + @Schema(description = "计划开始时间的序号, 从0点开始,每半个小时增加1") + private Integer start; + + @Schema(description = "计划结束时间的序号, 从0点开始,每半个小时增加1") + private Integer stop; + + @Schema(description = "计划周几执行") + private Integer weekDay; + + @Schema(description = "所属计划ID") + private Integer planId; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/RequestPushStreamMsg.java b/src/main/java/com/genersoft/iot/vmp/service/bean/RequestPushStreamMsg.java new file mode 100755 index 0000000..84ee7ba --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/RequestPushStreamMsg.java @@ -0,0 +1,188 @@ +package com.genersoft.iot.vmp.service.bean; + +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; + +/** + * redis消息:请求下级推送流信息 + * @author lin + */ +public class RequestPushStreamMsg { + + + /** + * 下级服务ID + */ + private String mediaServerId; + + /** + * 流ID + */ + private String app; + + /** + * 应用名 + */ + private String stream; + + /** + * 目标IP + */ + private String ip; + + /** + * 目标端口 + */ + private int port; + + /** + * ssrc + */ + private String ssrc; + + /** + * 是否使用TCP方式 + */ + private boolean tcp; + + /** + * 本地使用的端口 + */ + private int srcPort; + + /** + * 发送时,rtp的pt(uint8_t),不传时默认为96 + */ + private int pt; + + /** + * 发送时,rtp的负载类型。为true时,负载为ps;为false时,为es; + */ + private boolean ps; + + /** + * 是否只有音频 + */ + private boolean onlyAudio; + + + public static RequestPushStreamMsg getInstance(String mediaServerId, String app, String stream, String ip, int port, String ssrc, + boolean tcp, int srcPort, int pt, boolean ps, boolean onlyAudio) { + RequestPushStreamMsg requestPushStreamMsg = new RequestPushStreamMsg(); + requestPushStreamMsg.setMediaServerId(mediaServerId); + requestPushStreamMsg.setApp(app); + requestPushStreamMsg.setStream(stream); + requestPushStreamMsg.setIp(ip); + requestPushStreamMsg.setPort(port); + requestPushStreamMsg.setSsrc(ssrc); + requestPushStreamMsg.setTcp(tcp); + requestPushStreamMsg.setSrcPort(srcPort); + requestPushStreamMsg.setPt(pt); + requestPushStreamMsg.setPs(ps); + requestPushStreamMsg.setOnlyAudio(onlyAudio); + return requestPushStreamMsg; + } + + public static RequestPushStreamMsg getInstance(SendRtpInfo sendRtpItem) { + RequestPushStreamMsg requestPushStreamMsg = new RequestPushStreamMsg(); + requestPushStreamMsg.setMediaServerId(sendRtpItem.getMediaServerId()); + requestPushStreamMsg.setApp(sendRtpItem.getApp()); + requestPushStreamMsg.setStream(sendRtpItem.getStream()); + requestPushStreamMsg.setIp(sendRtpItem.getIp()); + requestPushStreamMsg.setPort(sendRtpItem.getPort()); + requestPushStreamMsg.setSsrc(sendRtpItem.getSsrc()); + requestPushStreamMsg.setTcp(sendRtpItem.isTcp()); + requestPushStreamMsg.setSrcPort(sendRtpItem.getLocalPort()); + requestPushStreamMsg.setPt(sendRtpItem.getPt()); + requestPushStreamMsg.setPs(sendRtpItem.isUsePs()); + requestPushStreamMsg.setOnlyAudio(sendRtpItem.isOnlyAudio()); + return requestPushStreamMsg; + } + + public String getMediaServerId() { + return mediaServerId; + } + + public void setMediaServerId(String mediaServerId) { + this.mediaServerId = mediaServerId; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getSsrc() { + return ssrc; + } + + public void setSsrc(String ssrc) { + this.ssrc = ssrc; + } + + public boolean isTcp() { + return tcp; + } + + public void setTcp(boolean tcp) { + this.tcp = tcp; + } + + public int getSrcPort() { + return srcPort; + } + + public void setSrcPort(int srcPort) { + this.srcPort = srcPort; + } + + public int getPt() { + return pt; + } + + public void setPt(int pt) { + this.pt = pt; + } + + public boolean isPs() { + return ps; + } + + public void setPs(boolean ps) { + this.ps = ps; + } + + public boolean isOnlyAudio() { + return onlyAudio; + } + + public void setOnlyAudio(boolean onlyAudio) { + this.onlyAudio = onlyAudio; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/RequestSendItemMsg.java b/src/main/java/com/genersoft/iot/vmp/service/bean/RequestSendItemMsg.java new file mode 100755 index 0000000..41c16e2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/RequestSendItemMsg.java @@ -0,0 +1,188 @@ +package com.genersoft.iot.vmp.service.bean; + +/** + * redis消息:请求下级回复推送信息 + * @author lin + */ +public class RequestSendItemMsg { + + /** + * 下级服务ID + */ + private String serverId; + + /** + * 下级服务ID + */ + private String mediaServerId; + + /** + * 流ID + */ + private String app; + + /** + * 应用名 + */ + private String stream; + + /** + * 目标IP + */ + private String ip; + + /** + * 目标端口 + */ + private int port; + + /** + * ssrc + */ + private String ssrc; + + /** + * 平台国标编号 + */ + private String platformId; + + /** + * 平台名称 + */ + private String platformName; + + /** + * 通道ID + */ + private String channelId; + + + /** + * 是否使用TCP + */ + private Boolean isTcp; + + + /** + * 是否使用TCP + */ + private Boolean rtcp; + + + + + public static RequestSendItemMsg getInstance(String serverId, String mediaServerId, String app, String stream, String ip, int port, + String ssrc, String platformId, String channelId, Boolean isTcp, Boolean rtcp, String platformName) { + RequestSendItemMsg requestSendItemMsg = new RequestSendItemMsg(); + requestSendItemMsg.setServerId(serverId); + requestSendItemMsg.setMediaServerId(mediaServerId); + requestSendItemMsg.setApp(app); + requestSendItemMsg.setStream(stream); + requestSendItemMsg.setIp(ip); + requestSendItemMsg.setPort(port); + requestSendItemMsg.setSsrc(ssrc); + requestSendItemMsg.setPlatformId(platformId); + requestSendItemMsg.setPlatformName(platformName); + requestSendItemMsg.setChannelId(channelId); + requestSendItemMsg.setTcp(isTcp); + requestSendItemMsg.setRtcp(rtcp); + + return requestSendItemMsg; + } + + public String getServerId() { + return serverId; + } + + public void setServerId(String serverId) { + this.serverId = serverId; + } + + public String getMediaServerId() { + return mediaServerId; + } + + public void setMediaServerId(String mediaServerId) { + this.mediaServerId = mediaServerId; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getSsrc() { + return ssrc; + } + + public void setSsrc(String ssrc) { + this.ssrc = ssrc; + } + + public String getPlatformId() { + return platformId; + } + + public void setPlatformId(String platformId) { + this.platformId = platformId; + } + + public String getPlatformName() { + return platformName; + } + + public void setPlatformName(String platformName) { + this.platformName = platformName; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + public Boolean getTcp() { + return isTcp; + } + + public void setTcp(Boolean tcp) { + isTcp = tcp; + } + + public Boolean getRtcp() { + return rtcp; + } + + public void setRtcp(Boolean rtcp) { + this.rtcp = rtcp; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/RequestStopPushStreamMsg.java b/src/main/java/com/genersoft/iot/vmp/service/bean/RequestStopPushStreamMsg.java new file mode 100755 index 0000000..a63d916 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/RequestStopPushStreamMsg.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.service.bean; + +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; + + +public class RequestStopPushStreamMsg { + + + private SendRtpInfo sendRtpItem; + + + private String platformName; + + + private int platFormIndex; + + public SendRtpInfo getSendRtpItem() { + return sendRtpItem; + } + + public void setSendRtpItem(SendRtpInfo sendRtpItem) { + this.sendRtpItem = sendRtpItem; + } + + public String getPlatformName() { + return platformName; + } + + public void setPlatformName(String platformName) { + this.platformName = platformName; + } + + + public int getPlatFormIndex() { + return platFormIndex; + } + + public void setPlatFormIndex(int platFormIndex) { + this.platFormIndex = platFormIndex; + } + + public static RequestStopPushStreamMsg getInstance(SendRtpInfo sendRtpItem, String platformName, int platFormIndex) { + RequestStopPushStreamMsg streamMsg = new RequestStopPushStreamMsg(); + streamMsg.setSendRtpItem(sendRtpItem); + streamMsg.setPlatformName(platformName); + streamMsg.setPlatFormIndex(platFormIndex); + return streamMsg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/ResponseSendItemMsg.java b/src/main/java/com/genersoft/iot/vmp/service/bean/ResponseSendItemMsg.java new file mode 100755 index 0000000..8680099 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/ResponseSendItemMsg.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.service.bean; + +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; + +/** + * redis消息:下级回复推送信息 + * @author lin + */ +public class ResponseSendItemMsg { + + private SendRtpInfo sendRtpItem; + + private MediaServer mediaServerItem; + + public SendRtpInfo getSendRtpItem() { + return sendRtpItem; + } + + public void setSendRtpItem(SendRtpInfo sendRtpItem) { + this.sendRtpItem = sendRtpItem; + } + + public MediaServer getMediaServerItem() { + return mediaServerItem; + } + + public void setMediaServerItem(MediaServer mediaServerItem) { + this.mediaServerItem = mediaServerItem; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/SSRCInfo.java b/src/main/java/com/genersoft/iot/vmp/service/bean/SSRCInfo.java new file mode 100755 index 0000000..c35ceb5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/SSRCInfo.java @@ -0,0 +1,21 @@ +package com.genersoft.iot.vmp.service.bean; + +import lombok.Data; + +@Data +public class SSRCInfo { + + private int port; + private String ssrc; + private String app; + private String Stream; + private String timeOutTaskKey; + + public SSRCInfo(int port, String ssrc, String app, String stream, String timeOutTaskKey) { + this.port = port; + this.ssrc = ssrc; + this.app = app; + this.Stream = stream; + this.timeOutTaskKey = timeOutTaskKey; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/StreamPushItemFromRedis.java b/src/main/java/com/genersoft/iot/vmp/service/bean/StreamPushItemFromRedis.java new file mode 100755 index 0000000..ff32d79 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/StreamPushItemFromRedis.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.service.bean; + + +public class StreamPushItemFromRedis { + private String app; + private String stream; + private long timeStamp; + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public long getTimeStamp() { + return timeStamp; + } + + public void setTimeStamp(long timeStamp) { + this.timeStamp = timeStamp; + } +} + + diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/ThirdPartyGB.java b/src/main/java/com/genersoft/iot/vmp/service/bean/ThirdPartyGB.java new file mode 100755 index 0000000..9d6b06a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/ThirdPartyGB.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.service.bean; + +public class ThirdPartyGB { + + private String name; + private String nationalStandardNo; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getNationalStandardNo() { + return nationalStandardNo; + } + + public void setNationalStandardNo(String nationalStandardNo) { + this.nationalStandardNo = nationalStandardNo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsg.java b/src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsg.java new file mode 100755 index 0000000..74890be --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsg.java @@ -0,0 +1,116 @@ +package com.genersoft.iot.vmp.service.bean; + +/** + * @author lin + */ +public class WvpRedisMsg { + + public static WvpRedisMsg getInstance(String fromId, String toId, String type, String cmd, String serial, String content){ + WvpRedisMsg wvpRedisMsg = new WvpRedisMsg(); + wvpRedisMsg.setFromId(fromId); + wvpRedisMsg.setToId(toId); + wvpRedisMsg.setType(type); + wvpRedisMsg.setCmd(cmd); + wvpRedisMsg.setSerial(serial); + wvpRedisMsg.setContent(content); + return wvpRedisMsg; + } + + private String fromId; + + private String toId; + /** + * req 请求, res 回复 + */ + private String type; + private String cmd; + + /** + * 消息的ID + */ + private String serial; + private String content; + + private final static String requestTag = "req"; + private final static String responseTag = "res"; + + public static WvpRedisMsg getRequestInstance(String fromId, String toId, String cmd, String serial, String content) { + WvpRedisMsg wvpRedisMsg = new WvpRedisMsg(); + wvpRedisMsg.setType(requestTag); + wvpRedisMsg.setFromId(fromId); + wvpRedisMsg.setToId(toId); + wvpRedisMsg.setCmd(cmd); + wvpRedisMsg.setSerial(serial); + wvpRedisMsg.setContent(content); + return wvpRedisMsg; + } + + public static WvpRedisMsg getResponseInstance() { + WvpRedisMsg wvpRedisMsg = new WvpRedisMsg(); + wvpRedisMsg.setType(responseTag); + return wvpRedisMsg; + } + + public static WvpRedisMsg getResponseInstance(String fromId, String toId, String cmd, String serial, String content) { + WvpRedisMsg wvpRedisMsg = new WvpRedisMsg(); + wvpRedisMsg.setType(responseTag); + wvpRedisMsg.setFromId(fromId); + wvpRedisMsg.setToId(toId); + wvpRedisMsg.setCmd(cmd); + wvpRedisMsg.setSerial(serial); + wvpRedisMsg.setContent(content); + return wvpRedisMsg; + } + + public static boolean isRequest(WvpRedisMsg wvpRedisMsg) { + return requestTag.equals(wvpRedisMsg.getType()); + } + + public String getSerial() { + return serial; + } + + public void setSerial(String serial) { + this.serial = serial; + } + + public String getFromId() { + return fromId; + } + + public void setFromId(String fromId) { + this.fromId = fromId; + } + + public String getToId() { + return toId; + } + + public void setToId(String toId) { + this.toId = toId; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getCmd() { + return cmd; + } + + public void setCmd(String cmd) { + this.cmd = cmd; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsgCmd.java b/src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsgCmd.java new file mode 100755 index 0000000..e9ee4cb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsgCmd.java @@ -0,0 +1,22 @@ +package com.genersoft.iot.vmp.service.bean; + +/** + * @author lin + */ + +public class WvpRedisMsgCmd { + + /** + * 请求获取推流信息 + */ + public static final String GET_SEND_ITEM = "GetSendItem"; + /** + * 请求推流的请求 + */ + public static final String REQUEST_PUSH_STREAM = "RequestPushStream"; + /** + * 停止推流的请求 + */ + public static final String REQUEST_STOP_PUSH_STREAM = "RequestStopPushStream"; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/CloudRecordServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/CloudRecordServiceImpl.java new file mode 100644 index 0000000..4dc5df8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/CloudRecordServiceImpl.java @@ -0,0 +1,431 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.media.event.media.MediaRecordMp4Event; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.AssistRESTfulUtils; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.service.ICloudRecordService; +import com.genersoft.iot.vmp.service.bean.CloudRecordItem; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.storager.dao.CloudRecordServiceMapper; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.cloudRecord.bean.CloudRecordUrl; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import java.io.File; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Slf4j +@Service +public class CloudRecordServiceImpl implements ICloudRecordService { + + @Autowired + private CloudRecordServiceMapper cloudRecordServiceMapper; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private AssistRESTfulUtils assistRESTfulUtils; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IRedisRpcPlayService redisRpcPlayService; + + @Override + public PageInfo getList(int page, int count, String query, String app, String stream, String startTime, + String endTime, List mediaServerItems, String callId, Boolean ascOrder) { + // 开始时间和结束时间在数据库中都是以秒为单位的 + Long startTimeStamp = null; + Long endTimeStamp = null; + if (startTime != null ) { + if (!DateUtil.verification(startTime, DateUtil.formatter)) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "开始时间格式错误,正确格式为: " + DateUtil.formatter); + } + startTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(startTime); + + } + if (endTime != null ) { + if (!DateUtil.verification(endTime, DateUtil.formatter)) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "结束时间格式错误,正确格式为: " + DateUtil.formatter); + } + endTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(endTime); + + } + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = cloudRecordServiceMapper.getList(query, app, stream, startTimeStamp, endTimeStamp, + callId, mediaServerItems, null, ascOrder); + return new PageInfo<>(all); + } + + @Override + public List getDateList(String app, String stream, int year, int month, List mediaServerItems) { + LocalDate startDate = LocalDate.of(year, month, 1); + LocalDate endDate; + if (month == 12) { + endDate = LocalDate.of(year + 1, 1, 1); + }else { + endDate = LocalDate.of(year, month + 1, 1); + } + long startTimeStamp = startDate.atStartOfDay().toInstant(ZoneOffset.ofHours(8)).toEpochMilli(); + long endTimeStamp = endDate.atStartOfDay().toInstant(ZoneOffset.ofHours(8)).toEpochMilli(); + List cloudRecordItemList = cloudRecordServiceMapper.getList(null, app, stream, startTimeStamp, + endTimeStamp, null, mediaServerItems, null, null); + if (cloudRecordItemList.isEmpty()) { + return new ArrayList<>(); + } + Set resultSet = new HashSet<>(); + cloudRecordItemList.stream().forEach(cloudRecordItem -> { + String date = DateUtil.timestampTo_yyyy_MM_dd(cloudRecordItem.getStartTime()); + resultSet.add(date); + }); + return new ArrayList<>(resultSet); + } + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaRecordMp4Event event) { + CloudRecordItem cloudRecordItem = CloudRecordItem.getInstance(event); + cloudRecordItem.setServerId(userSetting.getServerId()); + if (ObjectUtils.isEmpty(cloudRecordItem.getCallId())) { + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(event.getApp(), event.getStream()); + if (streamAuthorityInfo != null) { + cloudRecordItem.setCallId(streamAuthorityInfo.getCallId()); + } + } + log.info("[添加录像记录] {}/{}, callId: {}, 内容:{}", event.getApp(), event.getStream(), cloudRecordItem.getCallId(), event.getRecordInfo()); + cloudRecordServiceMapper.add(cloudRecordItem); + } + + @Override + public String addTask(String app, String stream, MediaServer mediaServerItem, String startTime, String endTime, + String callId, String remoteHost, boolean filterMediaServer) { + // 参数校验 + Assert.notNull(app,"应用名为NULL"); + Assert.notNull(stream,"流ID为NULL"); + if (mediaServerItem.getRecordAssistPort() == 0) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "为配置Assist服务"); + } + Long startTimeStamp = null; + Long endTimeStamp = null; + if (startTime != null) { + startTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime); + } + if (endTime != null) { + endTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime); + } + + List mediaServers = new ArrayList<>(); + mediaServers.add(mediaServerItem); + // 检索相关的录像文件 + List filePathList = cloudRecordServiceMapper.queryRecordFilePathList(app, stream, startTimeStamp, + endTimeStamp, callId, filterMediaServer ? mediaServers : null); + if (filePathList == null || filePathList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未检索到视频文件"); + } + JSONObject result = assistRESTfulUtils.addTask(mediaServerItem, app, stream, startTime, endTime, callId, filePathList, remoteHost); + if (result.getInteger("code") != 0) { + throw new ControllerException(result.getInteger("code"), result.getString("msg")); + } + return result.getString("data"); + } + + @Override + public JSONArray queryTask(String app, String stream, String callId, String taskId, String mediaServerId, + Boolean isEnd, String scheme) { + MediaServer mediaServerItem = null; + if (mediaServerId == null) { + mediaServerItem = mediaServerService.getDefaultMediaServer(); + }else { + mediaServerItem = mediaServerService.getOne(mediaServerId); + } + if (mediaServerItem == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的流媒体"); + } + + JSONObject result = assistRESTfulUtils.queryTaskList(mediaServerItem, app, stream, callId, taskId, isEnd, scheme); + if (result == null || result.getInteger("code") != 0) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), result == null ? "查询任务列表失败" : result.getString("msg")); + } + return result.getJSONArray("data"); + } + + @Override + public int changeCollect(boolean result, String app, String stream, String mediaServerId, String startTime, String endTime, String callId) { + // 开始时间和结束时间在数据库中都是以秒为单位的 + Long startTimeStamp = null; + Long endTimeStamp = null; + if (startTime != null ) { + if (!DateUtil.verification(startTime, DateUtil.formatter)) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "开始时间格式错误,正确格式为: " + DateUtil.formatter); + } + startTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime); + + } + if (endTime != null ) { + if (!DateUtil.verification(endTime, DateUtil.formatter)) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "结束时间格式错误,正确格式为: " + DateUtil.formatter); + } + endTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime); + + } + + List mediaServerItems; + if (!ObjectUtils.isEmpty(mediaServerId)) { + mediaServerItems = new ArrayList<>(); + MediaServer mediaServerItem = mediaServerService.getOne(mediaServerId); + if (mediaServerItem == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体: " + mediaServerId); + } + mediaServerItems.add(mediaServerItem); + } else { + mediaServerItems = null; + } + + List all = cloudRecordServiceMapper.getList(null, app, stream, startTimeStamp, endTimeStamp, + callId, mediaServerItems, null, null); + if (all.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到待收藏的视频"); + } + int limitCount = 50; + int resultCount = 0; + if (all.size() > limitCount) { + for (int i = 0; i < all.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > all.size()) { + toIndex = all.size(); + } + resultCount += cloudRecordServiceMapper.updateCollectList(result, all.subList(i, toIndex)); + + } + }else { + resultCount = cloudRecordServiceMapper.updateCollectList(result, all); + } + return resultCount; + } + + @Override + public int changeCollectById(Integer recordId, boolean result) { + return cloudRecordServiceMapper.changeCollectById(result, recordId); + } + + @Override + public DownloadFileInfo getPlayUrlPath(Integer recordId) { + CloudRecordItem recordItem = cloudRecordServiceMapper.queryOne(recordId); + if (recordItem == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "资源不存在"); + } + if (!userSetting.getServerId().equals(recordItem.getServerId())) { + return redisRpcPlayService.getRecordPlayUrl(recordItem.getServerId(), recordId); + } + + MediaServer mediaServer = mediaServerService.getOne(recordItem.getMediaServerId()); + + return mediaServerService.getDownloadFilePath(mediaServer, RecordInfo.getInstance(recordItem)); + } + + @Override + public List getAllList(String query, String app, String stream, String startTime, String endTime, List mediaServerItems, String callId, List ids) { + // 开始时间和结束时间在数据库中都是以秒为单位的 + Long startTimeStamp = null; + Long endTimeStamp = null; + if (startTime != null ) { + if (!DateUtil.verification(startTime, DateUtil.formatter)) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "开始时间格式错误,正确格式为: " + DateUtil.formatter); + } + startTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(startTime); + + } + if (endTime != null ) { + if (!DateUtil.verification(endTime, DateUtil.formatter)) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "结束时间格式错误,正确格式为: " + DateUtil.formatter); + } + endTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(endTime); + + } + return cloudRecordServiceMapper.getList(query, app, stream, startTimeStamp, endTimeStamp, + callId, mediaServerItems, ids, null); + } + + @Override + public void loadMP4File(String app, String stream, int cloudRecordId, ErrorCallback callback) { + + CloudRecordItem recordItem = cloudRecordServiceMapper.queryOne(cloudRecordId); + if (recordItem == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "无录像"); + } + String mediaServerId = recordItem.getMediaServerId(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + log.warn("[云端录像] 播放 未找到录制的流媒体,将自动选择低负载流媒体使用"); + mediaServer = mediaServerService.getMediaServerForMinimumLoad(null); + } + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "无可用流媒体"); + } + String fileName = recordItem.getFileName().substring(0 , recordItem.getFileName().indexOf(".")); + String filePath = recordItem.getFilePath(); +// if (filePath != null) { +// fileName = filePath.substring(0, filePath.lastIndexOf("/")); +// } + mediaServerService.loadMP4File(mediaServer, app, stream, filePath, fileName, ((code, msg, streamInfo) -> { + if (code == ErrorCode.SUCCESS.getCode()) { + streamInfo.setDuration(recordItem.getTimeLen()); + } + callback.run(code, msg, streamInfo); + })); + } + + @Override + public void loadMP4FileForDate(String app, String stream, String date, ErrorCallback callback) { + long startTimestamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(date + " 00:00:00"); + long endTimestamp = startTimestamp + 24 * 60 * 60 * 1000; + + List recordItemList = cloudRecordServiceMapper.getList(null, app, stream, startTimestamp, endTimestamp, null, null, null, false); + if (recordItemList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "此时间无录像"); + } + String mediaServerId = recordItemList.get(0).getMediaServerId(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "媒体节点不存在: " + mediaServerId); + } + String dateDir = null; + String filePath = recordItemList.get(0).getFilePath(); + if (filePath != null) { + dateDir = filePath.substring(0, filePath.lastIndexOf("/")); + } + mediaServerService.loadMP4FileForDate(mediaServer, app, stream, date, dateDir, callback); + + } + + @Override + public void seekRecord(String mediaServerId,String app, String stream, Double seek, String schema) { + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "媒体节点不存在: " + mediaServerId); + } + mediaServerService.seekRecordStamp(mediaServer, app, stream, seek, schema); + } + + @Override + public void setRecordSpeed(String mediaServerId, String app, String stream, Integer speed, String schema) { + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "媒体节点不存在: " + mediaServerId); + } + mediaServerService.setRecordSpeed(mediaServer, app, stream, speed, schema); + } + + @Override + public void deleteFileByIds(Set ids) { + log.info("[删除录像文件] ids: {}", ids.toArray()); + List cloudRecordItemList = cloudRecordServiceMapper.queryRecordByIds(ids); + if (cloudRecordItemList.isEmpty()) { + return; + } + List cloudRecordItemIdListForDelete = new ArrayList<>(); + StringBuilder stringBuilder = new StringBuilder(); + for (CloudRecordItem cloudRecordItem : cloudRecordItemList) { + String date = new File(cloudRecordItem.getFilePath()).getParentFile().getName(); + MediaServer mediaServer = mediaServerService.getOne(cloudRecordItem.getMediaServerId()); + try { + boolean deleteResult = mediaServerService.deleteRecordDirectory(mediaServer, cloudRecordItem.getApp(), + cloudRecordItem.getStream(), date, cloudRecordItem.getFileName()); + if (deleteResult) { + log.warn("[录像文件] 删除磁盘文件成功: {}", cloudRecordItem.getFilePath()); + cloudRecordItemIdListForDelete.add(cloudRecordItem); + } + }catch (ControllerException e) { + if (stringBuilder.length() > 0) { + stringBuilder.append(", "); + } + stringBuilder.append(cloudRecordItem.getFileName()); + } + + } + if (!cloudRecordItemIdListForDelete.isEmpty()) { + cloudRecordServiceMapper.deleteList(cloudRecordItemIdListForDelete); + } + if (stringBuilder.length() > 0) { + stringBuilder.append(" 删除失败"); + throw new ControllerException(ErrorCode.ERROR100.getCode(), stringBuilder.toString()); + } + } + + @Override + public List getUrlListByIds(List ids) { + List cloudRecordItems = cloudRecordServiceMapper.queryRecordByIds(ids); + if (cloudRecordItems.isEmpty()) { + return List.of(); + } + return getCloudRecordUrl(cloudRecordItems); + } + + @Override + public List getUrlList(String app, String stream, String callId) { + List cloudRecordItems = cloudRecordServiceMapper.queryRecordByAppStreamAndCallId(app, stream, callId); + if (cloudRecordItems.isEmpty()) { + return List.of(); + } + return getCloudRecordUrl(cloudRecordItems); + } + + private List getCloudRecordUrl(List cloudRecordItems) { + if (cloudRecordItems.isEmpty()) { + return List.of(); + } + List resultList = new ArrayList<>(); + for (CloudRecordItem cloudRecordItem : cloudRecordItems) { + CloudRecordUrl cloudRecordUrl = new CloudRecordUrl(); + cloudRecordUrl.setId(cloudRecordItem.getId()); + cloudRecordUrl.setFileName(cloudRecordItem.getStartTime() + ".mp4"); + cloudRecordUrl.setFilePath(cloudRecordItem.getFilePath()); + if (!userSetting.getServerId().equals(cloudRecordItem.getServerId())) { + cloudRecordUrl.setDownloadUrl(redisRpcPlayService.getRecordPlayUrl(cloudRecordItem.getServerId(), cloudRecordItem.getId()).getHttpPath()); + }else { + MediaServer mediaServer = mediaServerService.getOne(cloudRecordItem.getMediaServerId()); + mediaServer.setStreamIp(mediaServer.getIp()); + DownloadFileInfo downloadFilePath = mediaServerService.getDownloadFilePath(mediaServer, RecordInfo.getInstance(cloudRecordItem)); + cloudRecordUrl.setDownloadUrl(downloadFilePath.getHttpPath()); + } + resultList.add(cloudRecordUrl); + } + + return resultList; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/LogServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/LogServiceImpl.java new file mode 100644 index 0000000..47ca3ac --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/LogServiceImpl.java @@ -0,0 +1,110 @@ +package com.genersoft.iot.vmp.service.impl; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.core.rolling.RollingFileAppender; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.service.ILogService; +import com.genersoft.iot.vmp.service.bean.LogFileInfo; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.input.ReversedLinesFileReader; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.io.*; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +@Service +@Slf4j +public class LogServiceImpl implements ILogService { + + @Override + public List queryList(String query, String startTime, String endTime) { + File logFile = getLogDir(); + if (logFile == null || !logFile.exists()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "获取日志文件目录失败"); + } + File[] files = logFile.listFiles(); + List result = new ArrayList<>(); + if (files == null || files.length == 0) { + return result; + } + + // 读取文件创建时间作为开始时间,修改时间为结束时间 + Long startTimestamp = null; + if (startTime != null) { + startTimestamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(startTime); + } + Long endTimestamp = null; + if (endTime != null) { + endTimestamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(endTime); + } + for (File file : files) { + LogFileInfo logFileInfo = new LogFileInfo(); + logFileInfo.setFileName(file.getName()); + logFileInfo.setFileSize(file.length()); + if (query != null && !file.getName().contains(query)) { + continue; + } + try { + Long[] fileAttributes = getFileAttributes(file); + if (fileAttributes == null) { + continue; + } + long startTimestampForFile = fileAttributes[0]; + long endTimestampForFile = fileAttributes[1]; + logFileInfo.setStartTime(startTimestampForFile); + logFileInfo.setEndTime(endTimestampForFile); + if (startTimestamp != null && startTimestamp > startTimestampForFile) { + continue; + } + if (endTimestamp != null && endTimestamp < endTimestampForFile) { + continue; + } + } catch (IOException e) { + log.error("[读取日志文件列表] 获取创建时间和修改时间失败", e); + continue; + } + result.add(logFileInfo); + + } + result.sort((o1, o2) -> o2.getStartTime().compareTo(o1.getStartTime())); + return result; + } + + private File getLogDir() { + Logger logger = (Logger) LoggerFactory.getLogger("root"); + RollingFileAppender rollingFileAppender = (RollingFileAppender) logger.getAppender("RollingFile"); + File rollingFile = new File(rollingFileAppender.getFile()); + return rollingFile.getParentFile(); + } + + Long[] getFileAttributes(File file) throws IOException { + BufferedReader bufferedReader = new BufferedReader(new FileReader(file)); + String startLine = bufferedReader.readLine(); + if (startLine== null) { + return null; + } + String startTime = startLine.substring(0, 19); + + // 最后一行的开头不一定是时间 +// String lastLine = ""; +// try (ReversedLinesFileReader reversedLinesReader = new ReversedLinesFileReader(file, Charset.defaultCharset())) { +// lastLine = reversedLinesReader.readLine(); +// } catch (Exception e) { +// log.error("file read error, msg:{}", e.getMessage(), e); +// } +// String endTime = lastLine.substring(0, 19); + return new Long[]{DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(startTime), file.lastModified()}; + } + + @Override + public File getFileByName(String fileName) { + File logDir = getLogDir(); + + return new File(logDir, fileName); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java new file mode 100755 index 0000000..8337425 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java @@ -0,0 +1,300 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionStatus; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.jt1078.bean.JTMediaStreamType; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078PlayService; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.ResultForOnPublish; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.service.IMediaService; +import com.genersoft.iot.vmp.service.IRecordPlanService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.IUserService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyService; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.MediaServerUtils; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.OtherPsSendInfo; +import com.genersoft.iot.vmp.vmanager.bean.OtherRtpSendInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Slf4j +@Service +public class MediaServiceImpl implements IMediaService { + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IStreamProxyService streamProxyService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IUserService userService; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private Ijt1078Service ijt1078Service; + + @Autowired + private Ijt1078PlayService jt1078PlayService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + + @Autowired + private IRecordPlanService recordPlanService; + + @Override + public boolean authenticatePlay(String app, String stream, String callId) { + if (app == null || stream == null) { + return false; + } + if ("rtp".equals(app)) { + return true; + } + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(app, stream); + if (streamAuthorityInfo == null || streamAuthorityInfo.getCallId() == null) { + return true; + } + return streamAuthorityInfo.getCallId().equals(callId); + } + + @Override + public ResultForOnPublish authenticatePublish(MediaServer mediaServer, String app, String stream, String params) { + // 推流鉴权的处理 + if (!"rtp".equals(app) && !"1078".equals(app) ) { + if ("talk".equals(app) && stream.endsWith("_talk")) { + ResultForOnPublish result = new ResultForOnPublish(); + result.setEnable_mp4(false); + result.setEnable_audio(true); + return result; + } + if ("jt_talk".equals(app) && stream.endsWith("_talk")) { + ResultForOnPublish result = new ResultForOnPublish(); + result.setEnable_mp4(false); + result.setEnable_audio(true); + return result; + } + if ("mp4_record".equals(app) ) { + ResultForOnPublish result = new ResultForOnPublish(); + result.setEnable_mp4(false); + result.setEnable_audio(true); + return result; + } + StreamProxy streamProxyItem = streamProxyService.getStreamProxyByAppAndStream(app, stream); + if (streamProxyItem != null) { + ResultForOnPublish result = new ResultForOnPublish(); + result.setEnable_audio(streamProxyItem.isEnableAudio()); + result.setEnable_mp4(streamProxyItem.isEnableMp4()); + return result; + } + if (userSetting.getPushAuthority()) { + // 对于推流进行鉴权 + Map paramMap = MediaServerUtils.urlParamToMap(params); + // 推流鉴权 + if (params == null) { + log.info("推流鉴权失败: 缺少必要参数:sign=md5(user表的pushKey)"); + throw new ControllerException(ErrorCode.ERROR401.getCode(), "Unauthorized"); + } + + String sign = paramMap.get("sign"); + if (sign == null) { + log.info("推流鉴权失败: 缺少必要参数:sign=md5(user表的pushKey)"); + throw new ControllerException(ErrorCode.ERROR401.getCode(), "Unauthorized"); + } + // 推流自定义播放鉴权码 + String callId = paramMap.get("callId"); + // 鉴权配置 + boolean hasAuthority = userService.checkPushAuthority(callId, sign); + if (!hasAuthority) { + log.info("推流鉴权失败: sign 无权限: callId={}. sign={}", callId, sign); + throw new ControllerException(ErrorCode.ERROR401.getCode(), "Unauthorized"); + } + StreamAuthorityInfo streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(app, stream, mediaServer.getId()); + streamAuthorityInfo.setCallId(callId); + streamAuthorityInfo.setSign(sign); + // 鉴权通过 + redisCatchStorage.updateStreamAuthorityInfo(app, stream, streamAuthorityInfo); + } + } + + + ResultForOnPublish result = new ResultForOnPublish(); + result.setEnable_audio(true); + + // 国标流 + if ("rtp".equals(app)) { + + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, stream); + + if (inviteInfo != null) { + result.setEnable_mp4(inviteInfo.getRecord()); + }else { + result.setEnable_mp4(userSetting.getRecordSip()); + } + + // 单端口模式下修改流 ID + if (!mediaServer.isRtpEnable() && inviteInfo == null) { + String ssrc = String.format("%010d", Long.parseLong(stream, 16)); + inviteInfo = inviteStreamService.getInviteInfoBySSRC(ssrc); + if (inviteInfo != null) { + result.setStream_replace(inviteInfo.getStream()); + log.info("[HOOK]推流鉴权 stream: {} 替换为 {}", stream, inviteInfo.getStream()); + stream = inviteInfo.getStream(); + } + } + + // 设置音频信息及录制信息 + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByStream(app, stream); + if (ssrcTransaction != null ) { + + // 为录制国标模拟一个鉴权信息, 方便后续写入录像文件时使用 + StreamAuthorityInfo streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(app, stream, mediaServer.getId()); + streamAuthorityInfo.setApp(app); + streamAuthorityInfo.setStream(ssrcTransaction.getStream()); + streamAuthorityInfo.setCallId(ssrcTransaction.getSipTransactionInfo().getCallId()); + + redisCatchStorage.updateStreamAuthorityInfo(app, ssrcTransaction.getStream(), streamAuthorityInfo); + + String deviceId = ssrcTransaction.getDeviceId(); + Integer channelId = ssrcTransaction.getChannelId(); + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channelId); + if (deviceChannel != null) { + result.setEnable_audio(deviceChannel.isHasAudio()); + } + // 如果是录像下载就设置视频间隔十秒 + if (ssrcTransaction.getType() == InviteSessionType.DOWNLOAD) { + // 获取录像的总时长,然后设置为这个视频的时长 + InviteInfo inviteInfoForDownload = inviteStreamService.getInviteInfo(InviteSessionType.DOWNLOAD, channelId, stream); + if (inviteInfoForDownload != null) { + String startTime = inviteInfoForDownload.getStartTime(); + String endTime = inviteInfoForDownload.getEndTime(); + long difference = DateUtil.getDifference(startTime, endTime) / 1000; + result.setMp4_max_second((int) difference); + result.setEnable_mp4(true); + // 设置为2保证得到的mp4的时长是正常的 + result.setModify_stamp(2); + } + } + // 如果是talk对讲,则默认获取声音 + if (ssrcTransaction.getType() == InviteSessionType.TALK) { + result.setEnable_audio(true); + } + } + } else if (app.equals("broadcast")) { + result.setEnable_audio(true); + result.setEnable_mp4(userSetting.getRecordSip()); + } else if (app.equals("talk")) { + result.setEnable_audio(true); + result.setEnable_mp4(userSetting.getRecordSip()); + }else { + result.setEnable_mp4(userSetting.getRecordPushLive()); + } + if (app.equalsIgnoreCase("rtp")) { + String receiveKey = VideoManagerConstants.WVP_OTHER_RECEIVE_RTP_INFO + userSetting.getServerId() + "_" + stream; + OtherRtpSendInfo otherRtpSendInfo = (OtherRtpSendInfo) redisTemplate.opsForValue().get(receiveKey); + + String receiveKeyForPS = VideoManagerConstants.WVP_OTHER_RECEIVE_PS_INFO + userSetting.getServerId() + "_" + stream; + OtherPsSendInfo otherPsSendInfo = (OtherPsSendInfo) redisTemplate.opsForValue().get(receiveKeyForPS); + if (otherRtpSendInfo != null || otherPsSendInfo != null) { + result.setEnable_mp4(true); + } + } + return result; + } + + @Override + public boolean closeStreamOnNoneReader(String mediaServerId, String app, String stream, String schema) { + boolean result = false; + if (recordPlanService.recording(app, stream) != null) { + return false; + } + // 国标类型的流 + if ("rtp".equals(app)) { + result = userSetting.getStreamOnDemand(); + // 国标流, 点播/录像回放/录像下载 + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, stream); + if (inviteInfo != null) { + if (inviteInfo.getStatus() == InviteSessionStatus.ok){ + // 录像下载 + if (inviteInfo.getType() == InviteSessionType.DOWNLOAD) { + return false; + } + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(inviteInfo.getChannelId()); + if (deviceChannel == null) { + return false; + } + } + return result; + } + }else if ("1078".equals(app)) { + // 判断是否是1078播放类型 + JTMediaStreamType jtMediaStreamType = ijt1078Service.checkStreamFromJt(stream); + if (jtMediaStreamType != null) { + String[] streamParamArray = stream.split("_"); + if (jtMediaStreamType.equals(JTMediaStreamType.PLAY)) { + jt1078PlayService.stopPlay(streamParamArray[0], Integer.parseInt(streamParamArray[1])); + }else if (jtMediaStreamType.equals(JTMediaStreamType.PLAYBACK)) { + jt1078PlayService.stopPlayback(streamParamArray[0], Integer.parseInt(streamParamArray[1])); + } + }else { + return false; + } + }else if ("talk".equals(app) || "broadcast".equals(app)) { + return false; + } else if ("mp4_record".equals(app)) { + return true; + } else { + // 非国标流 推流/拉流代理 + // 拉流代理 + StreamProxy streamProxy = streamProxyService.getStreamProxyByAppAndStream(app, stream); + if (streamProxy != null) { + if (streamProxy.isEnableDisableNoneReader()) { + // 无人观看停用 + // 修改数据 + streamProxyService.stopByAppAndStream(app, stream); + return true; + } else { + // 无人观看不做处理 + return false; + } + }else { + return false; + } + } + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/MobilePositionServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/MobilePositionServiceImpl.java new file mode 100644 index 0000000..54771f3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/MobilePositionServiceImpl.java @@ -0,0 +1,146 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.dao.DeviceChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.DeviceMapper; +import com.genersoft.iot.vmp.gb28181.dao.DeviceMobilePositionMapper; +import com.genersoft.iot.vmp.gb28181.dao.PlatformMapper; +import com.genersoft.iot.vmp.gb28181.utils.Coordtransform; +import com.genersoft.iot.vmp.service.IMobilePositionService; +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +public class MobilePositionServiceImpl implements IMobilePositionService { + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private DeviceChannelMapper channelMapper; + + @Autowired + private DeviceMobilePositionMapper mobilePositionMapper; + + @Autowired + private UserSetting userSetting; + + + @Autowired + private PlatformMapper platformMapper; + + @Autowired + private RedisTemplate redisTemplate; + + private final String REDIS_MOBILE_POSITION_LIST = "redis_mobile_position_list"; + + @Override + public void add(MobilePosition mobilePosition) { + List list = new ArrayList<>(); + list.add(mobilePosition); + add(list); + } + + @Override + public void add(List mobilePositionList) { + redisTemplate.opsForList().leftPushAll(REDIS_MOBILE_POSITION_LIST, mobilePositionList); + } + + private List get(int length) { + Long size = redisTemplate.opsForList().size(REDIS_MOBILE_POSITION_LIST); + if (size == null || size == 0) { + return new ArrayList<>(); + } + return redisTemplate.opsForList().rightPop(REDIS_MOBILE_POSITION_LIST, Math.min(length, size)); + } + + + + /** + * 查询移动位置轨迹 + */ + @Override + public synchronized List queryMobilePositions(String deviceId, String channelId, String startTime, String endTime) { + return mobilePositionMapper.queryPositionByDeviceIdAndTime(deviceId, channelId, startTime, endTime); + } + + @Override + public List queryEnablePlatformListWithAsMessageChannel() { + return platformMapper.queryEnablePlatformListWithAsMessageChannel(); + } + + /** + * 查询最新移动位置 + * @param deviceId + */ + @Override + public MobilePosition queryLatestPosition(String deviceId) { + return mobilePositionMapper.queryLatestPositionByDevice(deviceId); + } + + @Scheduled(fixedDelay = 1000) + @Transactional + public void executeTaskQueue() { + int countLimit = 3000; + List mobilePositions = get(countLimit); + if (mobilePositions == null || mobilePositions.isEmpty()) { + return; + } + if (userSetting.getSavePositionHistory()) { + mobilePositionMapper.batchadd(mobilePositions); + } + log.info("[移动位置订阅]更新通道位置: {}", mobilePositions.size()); + Map> updateChannelMap = new HashMap<>(); + for (MobilePosition mobilePosition : mobilePositions) { + DeviceChannel deviceChannel = new DeviceChannel(); + deviceChannel.setId(mobilePosition.getChannelId()); + deviceChannel.setDeviceId(mobilePosition.getDeviceId()); + deviceChannel.setLongitude(mobilePosition.getLongitude()); + deviceChannel.setLatitude(mobilePosition.getLatitude()); + deviceChannel.setGpsTime(mobilePosition.getTime()); + deviceChannel.setUpdateTime(DateUtil.getNow()); + if (mobilePosition.getLongitude() > 0 || mobilePosition.getLatitude() > 0) { + Double[] wgs84Position = Coordtransform.GCJ02ToWGS84(mobilePosition.getLongitude(), mobilePosition.getLatitude()); + deviceChannel.setGbLongitude(wgs84Position[0]); + deviceChannel.setGbLatitude(wgs84Position[1]); + } + if (!updateChannelMap.containsKey(mobilePosition.getDeviceId())) { + updateChannelMap.put(mobilePosition.getDeviceId(), new HashMap<>()); + } + updateChannelMap.get(mobilePosition.getDeviceId()).put(mobilePosition.getChannelId(), deviceChannel); + } + List deviceIds = new ArrayList<>(updateChannelMap.keySet()); + if (deviceIds.isEmpty()) { + log.info("[移动位置订阅]为查询到对应的设备,消息已经忽略"); + return; + } + List deviceList = deviceMapper.queryByDeviceIds(deviceIds); + for (Device device : deviceList) { + Map channelMap = updateChannelMap.get(device.getDeviceId()); + if (device.getGeoCoordSys().equalsIgnoreCase("GCJ02")) { + channelMap.values().forEach(channel -> { + Double[] wgs84Position = Coordtransform.GCJ02ToWGS84(channel.getLongitude(), channel.getLatitude()); + channel.setGbLongitude(wgs84Position[0]); + channel.setGbLatitude(wgs84Position[1]); + }); + } + channelMapper.batchUpdatePosition(new ArrayList<>(channelMap.values())); + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java new file mode 100644 index 0000000..05c4d5f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java @@ -0,0 +1,288 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.IRecordPlanService; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.service.bean.RecordPlan; +import com.genersoft.iot.vmp.service.bean.RecordPlanItem; +import com.genersoft.iot.vmp.storager.dao.RecordPlanMapper; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.google.common.base.Joiner; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.TimeUnit; + +@Service +@Slf4j +public class RecordPlanServiceImpl implements IRecordPlanService { + + @Autowired + private RecordPlanMapper recordPlanMapper; + + @Autowired + private CommonGBChannelMapper channelMapper; + + @Autowired + private IGbChannelPlayService channelPlayService; + + @Autowired + private IMediaServerService mediaServerService; + + + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + // 流断开,检查是否还处于录像状态, 如果是则继续录像 + Integer channelId = recording(event.getApp(), event.getStream()); + if(channelId == null) { + return; + } + // 重新拉起 + CommonGBChannel channel = channelMapper.queryById(channelId); + if (channel == null) { + log.warn("[录制计划] 流离开时拉起需要录像的流时, 发现通道不存在, id: {}", channelId); + return; + } + // 开启点播, + channelPlayService.play(channel, null, true, ((code, msg, streamInfo) -> { + if (code == InviteErrorCode.SUCCESS.getCode() && streamInfo != null) { + log.info("[录像] 流离开时拉起需要录像的流, 开启成功, 通道ID: {}", channel.getGbId()); + recordStreamMap.put(channel.getGbId(), streamInfo); + } else { + recordStreamMap.remove(channelId); + log.info("[录像] 流离开时拉起需要录像的流, 开启失败, 十分钟后重试, 通道ID: {}", channel.getGbId()); + } + })); + } + + Map recordStreamMap = new HashMap<>(); + + @Scheduled(fixedRate = 1, timeUnit = TimeUnit.MINUTES) + public void execution() { + // 查询现在需要录像的通道Id + List startChannelIdList = queryCurrentChannelRecord(); + + if (startChannelIdList.isEmpty()) { + // 当前没有录像任务, 如果存在旧的正在录像的就移除 + if(!recordStreamMap.isEmpty()) { + Set recordStreamSet = new HashSet<>(recordStreamMap.keySet()); + stopStreams(recordStreamSet, recordStreamMap); + recordStreamMap.clear(); + } + }else { + // 当前存在录像任务, 获取正在录像中存在但是当前录制列表不存在的内容,进行停止; 获取正在录像中没有但是当前需录制的列表中存在的进行开启. + Set recordStreamSet = new HashSet<>(recordStreamMap.keySet()); + startChannelIdList.forEach(recordStreamSet::remove); + if (!recordStreamSet.isEmpty()) { + // 正在录像中存在但是当前录制列表不存在的内容,进行停止; + stopStreams(recordStreamSet, recordStreamMap); + } + + // 移除startChannelIdList中已经在录像的部分, 剩下的都是需要新添加的(正在录像中没有但是当前需录制的列表中存在的进行开启) + recordStreamMap.keySet().forEach(startChannelIdList::remove); + if (!startChannelIdList.isEmpty()) { + // 获取所有的关联的通道 + List channelList = channelMapper.queryByIds(startChannelIdList); + if (!channelList.isEmpty()) { + // 查找是否已经开启录像, 如果没有则开启录像 + for (CommonGBChannel channel : channelList) { + // 开启点播, + channelPlayService.play(channel, null, true, ((code, msg, streamInfo) -> { + if (code == InviteErrorCode.SUCCESS.getCode() && streamInfo != null) { + log.info("[录像] 开启成功, 通道ID: {}", channel.getGbId()); + recordStreamMap.put(channel.getGbId(), streamInfo); + } else { + log.info("[录像] 开启失败, 十分钟后重试, 通道ID: {}", channel.getGbId()); + } + })); + } + } else { + log.error("[录制计划] 数据异常, 这些关联的通道已经不存在了: {}", Joiner.on(",").join(startChannelIdList)); + } + } + } + } + + /** + * 获取当前时间段应该录像的通道Id列表 + */ + private List queryCurrentChannelRecord(){ + // 获取当前时间在一周内的序号, 数据库存储的从第几个30分钟开始, 0-47, 包括首尾 + LocalDateTime now = LocalDateTime.now(); + int week = now.getDayOfWeek().getValue(); + int index = now.getHour() * 60 + now.getMinute(); + + // 查询现在需要录像的通道Id + return recordPlanMapper.queryRecordIng(week, index); + } + + private void stopStreams(Collection channelIds, Map recordStreamMap) { + for (Integer channelId : channelIds) { + try { + StreamInfo streamInfo = recordStreamMap.get(channelId); + if (streamInfo == null) { + continue; + } + // 查看是否有人观看,存在则不做处理,等待后续自然处理,如果无人观看,则关闭该流 + MediaInfo mediaInfo = mediaServerService.getMediaInfo(streamInfo.getMediaServer(), streamInfo.getApp(), streamInfo.getStream()); + if (mediaInfo.getReaderCount() == null || mediaInfo.getReaderCount() == 0) { + mediaServerService.closeStreams(streamInfo.getMediaServer(), streamInfo.getApp(), streamInfo.getStream()); + log.info("[录制计划] 停止, 通道ID: {}", channelId); + } + }catch (Exception e) { + log.error("[录制计划] 停止时异常", e); + }finally { + recordStreamMap.remove(channelId); + } + } + } + + @Override + public Integer recording(String app, String stream) { + for (Integer channelId : recordStreamMap.keySet()) { + StreamInfo streamInfo = recordStreamMap.get(channelId); + if (streamInfo != null && streamInfo.getApp().equals(app) && streamInfo.getStream().equals(stream)) { + return channelId; + } + } + return null; + } + + @Override + @Transactional + public void add(RecordPlan plan) { + plan.setCreateTime(DateUtil.getNow()); + plan.setUpdateTime(DateUtil.getNow()); + recordPlanMapper.add(plan); + if (plan.getId() > 0 && !plan.getPlanItemList().isEmpty()) { + for (RecordPlanItem recordPlanItem : plan.getPlanItemList()) { + recordPlanItem.setPlanId(plan.getId()); + } + recordPlanMapper.batchAddItem(plan.getId(), plan.getPlanItemList()); + } + // TODO 更新录像队列 + } + + @Override + public RecordPlan get(Integer planId) { + RecordPlan recordPlan = recordPlanMapper.get(planId); + if (recordPlan == null) { + return null; + } + List recordPlanItemList = recordPlanMapper.getItemList(planId); + if (!recordPlanItemList.isEmpty()) { + recordPlan.setPlanItemList(recordPlanItemList); + } + return recordPlan; + } + + @Override + @Transactional + public void update(RecordPlan plan) { + plan.setUpdateTime(DateUtil.getNow()); + recordPlanMapper.update(plan); + recordPlanMapper.cleanItems(plan.getId()); + if (plan.getPlanItemList() != null && !plan.getPlanItemList().isEmpty()){ + List planItemList = new ArrayList<>(); + for (RecordPlanItem recordPlanItem : plan.getPlanItemList()) { + if (recordPlanItem.getStart() == null || recordPlanItem.getStop() == null || recordPlanItem.getWeekDay() == null){ + continue; + } + if (recordPlanItem.getPlanId() == null) { + recordPlanItem.setPlanId(plan.getId()); + } + planItemList.add(recordPlanItem); + } + if(!planItemList.isEmpty()) { + recordPlanMapper.batchAddItem(plan.getId(), planItemList); + } + } + // TODO 更新录像队列 + + } + + @Override + @Transactional + public void delete(Integer planId) { + RecordPlan recordPlan = recordPlanMapper.get(planId); + if (recordPlan == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "录制计划不存在"); + } + // 清理关联的通道 + channelMapper.removeRecordPlanByPlanId(recordPlan.getId()); + recordPlanMapper.cleanItems(planId); + recordPlanMapper.delete(planId); + // TODO 更新录像队列 + } + + @Override + public PageInfo query(Integer page, Integer count, String query) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = recordPlanMapper.query(query); + return new PageInfo<>(all); + } + + @Override + public void link(List channelIds, Integer planId) { + if (channelIds == null || channelIds.isEmpty()) { + log.info("[录制计划] 关联/移除关联时, 通道编号必须存在"); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道编号必须存在"); + } + if (planId == null) { + channelMapper.removeRecordPlan(channelIds); + }else { + channelMapper.addRecordPlan(channelIds, planId); + } + // 查看当前的待录制列表是否变化,如果变化,则调用录制计划马上开始录制 + execution(); + } + + @Override + public PageInfo queryChannelList(int page, int count, String query, Integer dataType, Boolean online, Integer planId, Boolean hasLink) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = channelMapper.queryForRecordPlanForWebList(planId, query, dataType, online, hasLink); + return new PageInfo<>(all); + } + + @Override + public void linkAll(Integer planId) { + channelMapper.addRecordPlanForAll(planId); + } + + @Override + public void cleanAll(Integer planId) { + channelMapper.removeRecordPlanByPlanId(planId); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/RoleServerImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/RoleServerImpl.java new file mode 100755 index 0000000..d31bbce --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/RoleServerImpl.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.genersoft.iot.vmp.service.IRoleService; +import com.genersoft.iot.vmp.storager.dao.RoleMapper; +import com.genersoft.iot.vmp.storager.dao.dto.Role; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class RoleServerImpl implements IRoleService { + + @Autowired + private RoleMapper roleMapper; + + @Override + public Role getRoleById(int id) { + return roleMapper.selectById(id); + } + + @Override + public int add(Role role) { + return roleMapper.add(role); + } + + @Override + public int delete(int id) { + return roleMapper.delete(id); + } + + @Override + public List getAll() { + return roleMapper.selectAll(); + } + + @Override + public int update(Role role) { + return roleMapper.update(role); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/RtpServerServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/RtpServerServiceImpl.java new file mode 100644 index 0000000..8609926 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/RtpServerServiceImpl.java @@ -0,0 +1,162 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.OpenRTPServerResult; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.IReceiveRtpServerService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.service.bean.RTPServerParam; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Slf4j +@Service +public class RtpServerServiceImpl implements IReceiveRtpServerService { + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private SSRCFactory ssrcFactory; + + @Autowired + private UserSetting userSetting; + + @Autowired + private HookSubscribe subscribe; + + @Autowired + private SipInviteSessionManager sessionManager; + + /** + * 流到来的处理 + */ + @Async("taskExecutor") + @org.springframework.context.event.EventListener + public void onApplicationEvent(MediaArrivalEvent event) { + + } + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + + } + + @Override + public SSRCInfo openRTPServer(RTPServerParam rtpServerParam, ErrorCallback callback) { + if (callback == null) { + log.warn("[开启RTP收流] 失败,回调为NULL"); + return null; + } + if (rtpServerParam.getMediaServerItem() == null) { + log.warn("[开启RTP收流] 失败,媒体节点为NULL"); + return null; + } + + // 获取mediaServer可用的ssrc + final String ssrc; + if (rtpServerParam.getPresetSsrc() != null) { + ssrc = rtpServerParam.getPresetSsrc(); + }else { + if (rtpServerParam.isPlayback()) { + ssrc = ssrcFactory.getPlayBackSsrc(rtpServerParam.getMediaServerItem().getId()); + }else { + ssrc = ssrcFactory.getPlaySsrc(rtpServerParam.getMediaServerItem().getId()); + } + } + final String streamId; + if (rtpServerParam.getStreamId() == null) { + streamId = String.format("%08x", Long.parseLong(ssrc)).toUpperCase(); + }else { + streamId = rtpServerParam.getStreamId(); + } + if (rtpServerParam.isSsrcCheck() && rtpServerParam.getTcpMode() > 0) { + // 目前zlm不支持 tcp模式更新ssrc,暂时关闭ssrc校验 + log.warn("[openRTPServer] 平台对接时下级可能自定义ssrc,但是tcp模式zlm收流目前无法更新ssrc,可能收流超时,此时请使用udp收流或者关闭ssrc校验"); + } + int rtpServerPort; + if (rtpServerParam.getMediaServerItem().isRtpEnable()) { + rtpServerPort = mediaServerService.createRTPServer(rtpServerParam.getMediaServerItem(), streamId, + rtpServerParam.isSsrcCheck() ? Long.parseLong(ssrc) : 0, rtpServerParam.getPort(), rtpServerParam.isOnlyAuto(), + rtpServerParam.isDisableAudio(), rtpServerParam.isReUsePort(), rtpServerParam.getTcpMode()); + } else { + rtpServerPort = rtpServerParam.getMediaServerItem().getRtpProxyPort(); + } + if (rtpServerPort == 0) { + callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "开启RTPServer失败", null); + // 释放ssrc + if (rtpServerParam.getPresetSsrc() == null) { + ssrcFactory.releaseSsrc(rtpServerParam.getMediaServerItem().getId(), ssrc); + } + return null; + } + + // 设置流超时的定时任务 + String timeOutTaskKey = UUID.randomUUID().toString(); + + SSRCInfo ssrcInfo = new SSRCInfo(rtpServerPort, ssrc, "rtp", streamId, timeOutTaskKey); + OpenRTPServerResult openRTPServerResult = new OpenRTPServerResult(); + openRTPServerResult.setSsrcInfo(ssrcInfo); + + Hook rtpHook = Hook.getInstance(HookType.on_media_arrival, ssrcInfo.getApp(), streamId, rtpServerParam.getMediaServerItem().getId()); + dynamicTask.startDelay(timeOutTaskKey, () -> { + // 收流超时 + // 释放ssrc + if (rtpServerParam.getPresetSsrc() == null) { + ssrcFactory.releaseSsrc(rtpServerParam.getMediaServerItem().getId(), ssrc); + } + // 关闭收流端口 + mediaServerService.closeRTPServer(rtpServerParam.getMediaServerItem(), streamId); + subscribe.removeSubscribe(rtpHook); + callback.run(InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), openRTPServerResult); + }, userSetting.getPlayTimeout()); + // 开启流到来的监听 + subscribe.addSubscribe(rtpHook, (hookData) -> { + dynamicTask.stop(timeOutTaskKey); + // hook响应 + openRTPServerResult.setHookData(hookData); + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), openRTPServerResult); + subscribe.removeSubscribe(rtpHook); + }); + + return ssrcInfo; + } + + @Override + public void closeRTPServer(MediaServer mediaServer, SSRCInfo ssrcInfo) { + if (mediaServer == null) { + return; + } + if (ssrcInfo.getTimeOutTaskKey() != null) { + dynamicTask.stop(ssrcInfo.getTimeOutTaskKey()); + } + if (ssrcInfo.getSsrc() != null) { + // 释放ssrc + ssrcFactory.releaseSsrc(mediaServer.getId(), ssrcInfo.getSsrc()); + } + mediaServerService.closeRTPServer(mediaServer, ssrcInfo.getStream()); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/SendRtpServerServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/SendRtpServerServiceImpl.java new file mode 100644 index 0000000..053ff9e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/SendRtpServerServiceImpl.java @@ -0,0 +1,256 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.PlayException; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.utils.JsonUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.math.NumberUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.support.atomic.RedisAtomicInteger; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Service +@Slf4j +public class SendRtpServerServiceImpl implements ISendRtpServerService { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + + @Override + public SendRtpInfo createSendRtpInfo(MediaServer mediaServer, String ip, Integer port, String ssrc, String requesterId, + String deviceId, Integer channelId, Boolean isTcp, Boolean rtcp) { + int localPort = getNextPort(mediaServer); + if (localPort <= 0) { + return null; + } + return SendRtpInfo.getInstance(localPort, mediaServer, ip, port, ssrc, deviceId, null, channelId, + isTcp, rtcp, userSetting.getServerId()); + } + + @Override + public SendRtpInfo createSendRtpInfo(MediaServer mediaServer, String ip, Integer port, String ssrc, String platformId, + String app, String stream, Integer channelId, Boolean tcp, Boolean rtcp){ + + int localPort = getNextPort(mediaServer); + if (localPort <= 0) { + throw new PlayException(javax.sip.message.Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + SendRtpInfo sendRtpInfo = SendRtpInfo.getInstance(localPort, mediaServer, ip, port, ssrc, null, platformId, channelId, + tcp, rtcp, userSetting.getServerId()); + if (sendRtpInfo == null) { + return null; + } + sendRtpInfo.setApp(app); + sendRtpInfo.setStream(stream); + return sendRtpInfo; + } + + @Override + public void update(SendRtpInfo sendRtpItem) { + redisTemplate.opsForHash().put(VideoManagerConstants.SEND_RTP_INFO_CALLID, sendRtpItem.getCallId(), sendRtpItem); + redisTemplate.opsForHash().put(VideoManagerConstants.SEND_RTP_INFO_STREAM + sendRtpItem.getStream(), sendRtpItem.getTargetId(), sendRtpItem); + redisTemplate.opsForHash().put(VideoManagerConstants.SEND_RTP_INFO_CHANNEL + sendRtpItem.getChannelId(), sendRtpItem.getTargetId(), sendRtpItem); + } + + @Override + public SendRtpInfo queryByChannelId(Integer channelId, String targetId) { + String key = VideoManagerConstants.SEND_RTP_INFO_CHANNEL + channelId; + return JsonUtil.redisHashJsonToObject(redisTemplate, key, targetId, SendRtpInfo.class); + } + + @Override + public SendRtpInfo queryByCallId(String callId) { + String key = VideoManagerConstants.SEND_RTP_INFO_CALLID; + return (SendRtpInfo)redisTemplate.opsForHash().get(key, callId); + } + + @Override + public SendRtpInfo queryByStream(String stream, String targetId) { + String key = VideoManagerConstants.SEND_RTP_INFO_STREAM + stream; + return JsonUtil.redisHashJsonToObject(redisTemplate, key, targetId, SendRtpInfo.class); + } + + @Override + public List queryByStream(String stream) { + String key = VideoManagerConstants.SEND_RTP_INFO_STREAM + stream; + List values = redisTemplate.opsForHash().values(key); + List result= new ArrayList<>(); + for (Object o : values) { + result.add((SendRtpInfo) o); + } + + return result; + } + + /** + * 删除RTP推送信息缓存 + */ + @Override + public void delete(SendRtpInfo sendRtpInfo) { + if (sendRtpInfo == null) { + return; + } + redisTemplate.opsForHash().delete(VideoManagerConstants.SEND_RTP_INFO_CALLID, sendRtpInfo.getCallId()); + redisTemplate.opsForHash().delete(VideoManagerConstants.SEND_RTP_INFO_STREAM + sendRtpInfo.getStream(), sendRtpInfo.getTargetId()); + redisTemplate.opsForHash().delete(VideoManagerConstants.SEND_RTP_INFO_CHANNEL + sendRtpInfo.getChannelId(), sendRtpInfo.getTargetId()); + } + @Override + public void deleteByCallId(String callId) { + SendRtpInfo sendRtpInfo = queryByCallId(callId); + if (sendRtpInfo == null) { + return; + } + delete(sendRtpInfo); + } + @Override + public void deleteByStream(String stream, String targetId) { + SendRtpInfo sendRtpInfo = queryByStream(stream, targetId); + if (sendRtpInfo == null) { + return; + } + delete(sendRtpInfo); + } + + @Override + public void deleteByStream(String stream) { + List sendRtpInfos = queryByStream(stream); + for (SendRtpInfo sendRtpInfo : sendRtpInfos) { + delete(sendRtpInfo); + } + } + + @Override + public void deleteByChannel(Integer channelId, String targetId) { + SendRtpInfo sendRtpInfo = queryByChannelId(channelId, targetId); + if (sendRtpInfo == null) { + return; + } + delete(sendRtpInfo); + } + + @Override + public List queryByChannelId(int channelId) { + String key = VideoManagerConstants.SEND_RTP_INFO_CHANNEL + channelId; + List values = redisTemplate.opsForHash().values(key); + List result= new ArrayList<>(); + for (Object o : values) { + result.add((SendRtpInfo) o); + } + return result; + } + + @Override + public List queryAll() { + String key = VideoManagerConstants.SEND_RTP_INFO_CALLID; + List values = redisTemplate.opsForHash().values(key); + List result= new ArrayList<>(); + for (Object o : values) { + result.add((SendRtpInfo) o); + } + return result; + } + + /** + * 查询某个通道是否存在上级点播(RTP推送) + */ + @Override + public boolean isChannelSendingRTP(Integer channelId) { + List sendRtpInfoList = queryByChannelId(channelId); + return !sendRtpInfoList.isEmpty(); + } + + @Override + public List queryForPlatform(String platformId) { + List sendRtpInfos = queryAll(); + if (!sendRtpInfos.isEmpty()) { + sendRtpInfos.removeIf(sendRtpInfo -> !sendRtpInfo.isSendToPlatform() || !sendRtpInfo.getTargetId().equals(platformId)); + } + return sendRtpInfos; + } + + private Set getAllSendRtpPort() { + String key = VideoManagerConstants.SEND_RTP_INFO_CALLID; + List values = redisTemplate.opsForHash().values(key); + Set result = new HashSet<>(); + for (Object value : values) { + SendRtpInfo sendRtpInfo = (SendRtpInfo) value; + result.add(sendRtpInfo.getPort()); + } + return result; + } + + + @Override + public synchronized int getNextPort(MediaServer mediaServer) { + if (mediaServer == null) { + log.warn("[发送端口管理] 参数错误,mediaServer为NULL"); + return -1; + } + String sendIndexKey = VideoManagerConstants.SEND_RTP_PORT + userSetting.getServerId() + ":" + mediaServer.getId(); + Set sendRtpSet = getAllSendRtpPort(); + String sendRtpPortRange = mediaServer.getSendRtpPortRange(); + int startPort; + int endPort; + if (sendRtpPortRange != null) { + String[] portArray = sendRtpPortRange.split(","); + if (portArray.length != 2 || !NumberUtils.isParsable(portArray[0]) || !NumberUtils.isParsable(portArray[1])) { + log.warn("{}发送端口配置格式错误,自动使用50000-60000作为端口范围", mediaServer.getId()); + startPort = 50000; + endPort = 60000; + }else { + if ( Integer.parseInt(portArray[1]) - Integer.parseInt(portArray[0]) < 1) { + log.warn("{}发送端口配置错误,结束端口至少比开始端口大一,自动使用50000-60000作为端口范围", mediaServer.getId()); + startPort = 50000; + endPort = 60000; + }else { + startPort = Integer.parseInt(portArray[0]); + endPort = Integer.parseInt(portArray[1]); + } + } + }else { + log.warn("{}未设置发送端口默认值,自动使用50000-60000作为端口范围", mediaServer.getId()); + startPort = 50000; + endPort = 60000; + } + if (redisTemplate == null || redisTemplate.getConnectionFactory() == null) { + log.warn("{}获取redis连接信息失败", mediaServer.getId()); + return -1; + } + RedisAtomicInteger redisAtomicInteger = new RedisAtomicInteger(sendIndexKey , redisTemplate.getConnectionFactory()); + if (redisAtomicInteger.get() < startPort) { + redisAtomicInteger.set(startPort); + return startPort; + }else { + for (int i = 0; i < endPort - startPort; i++) { + int port = redisAtomicInteger.getAndIncrement(); + if (port > endPort) { + redisAtomicInteger.set(startPort); + if (sendRtpSet.contains(startPort)) { + continue; + }else { + return startPort; + } + } + if (!sendRtpSet.contains(port)) { + return port; + } + } + } + log.warn("{}获取发送端口失败, 无可用端口", mediaServer.getId()); + return -1; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/UserApiKeyServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/UserApiKeyServiceImpl.java new file mode 100644 index 0000000..8c552b1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/UserApiKeyServiceImpl.java @@ -0,0 +1,78 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.genersoft.iot.vmp.service.IUserApiKeyService; +import com.genersoft.iot.vmp.storager.dao.UserApiKeyMapper; +import com.genersoft.iot.vmp.storager.dao.dto.UserApiKey; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class UserApiKeyServiceImpl implements IUserApiKeyService { + + @Autowired + UserApiKeyMapper userApiKeyMapper; + + @Autowired + private RedisTemplate redisTemplate; + + @Override + public int addApiKey(UserApiKey userApiKey) { + return userApiKeyMapper.add(userApiKey); + } + + @Override + public boolean isApiKeyExists(String apiKey) { + return userApiKeyMapper.isApiKeyExists(apiKey); + } + + @Override + public PageInfo getUserApiKeys(int page, int count) { + PageHelper.startPage(page, count); + List userApiKeys = userApiKeyMapper.getUserApiKeys(); + return new PageInfo<>(userApiKeys); + } + + @Cacheable(cacheNames = "userApiKey", key = "#id", sync = true) + @Override + public UserApiKey getUserApiKeyById(Integer id) { + return userApiKeyMapper.selectById(id); + } + + @CacheEvict(cacheNames = "userApiKey", key = "#id") + @Override + public int enable(Integer id) { + return userApiKeyMapper.enable(id); + } + + @CacheEvict(cacheNames = "userApiKey", key = "#id") + @Override + public int disable(Integer id) { + return userApiKeyMapper.disable(id); + } + + @CacheEvict(cacheNames = "userApiKey", key = "#id") + @Override + public int remark(Integer id, String remark) { + return userApiKeyMapper.remark(id, remark); + } + + @CacheEvict(cacheNames = "userApiKey", key = "#id") + @Override + public int delete(Integer id) { + return userApiKeyMapper.delete(id); + } + + @CacheEvict(cacheNames = "userApiKey", key = "#id") + @Override + public int reset(Integer id, String apiKey) { + return userApiKeyMapper.apiKey(id, apiKey); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java new file mode 100755 index 0000000..cf0bea2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java @@ -0,0 +1,97 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.genersoft.iot.vmp.service.IUserService; +import com.genersoft.iot.vmp.storager.dao.UserMapper; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.DigestUtils; + +import java.util.List; + +@Service +public class UserServiceImpl implements IUserService { + + @Autowired + private UserMapper userMapper; + + @Override + public User getUser(String username, String password) { + return userMapper.select(username, password); + } + + @Override + public boolean changePassword(int id, String password) { + User user = userMapper.selectById(id); + user.setPassword(password); + return userMapper.update(user) > 0; + } + + @Override + public User getUserById(int id) { + return userMapper.selectById(id); + } + + @Override + public User getUserByUsername(String username) { + return userMapper.getUserByUsername(username); + } + + @Override + public int addUser(User user) { + User userByUsername = userMapper.getUserByUsername(user.getUsername()); + if (userByUsername != null) { + return 0; + } + return userMapper.add(user); + } + @Override + public int deleteUser(int id) { + return userMapper.delete(id); + } + + @Override + public List getAllUsers() { + return userMapper.selectAll(); + } + + @Override + public int updateUsers(User user) { + return userMapper.update(user); + } + + + @Override + public boolean checkPushAuthority(String callId, String sign) { + + List users = userMapper.getUsers(); + if (users.size() == 0) { + return false; + } + for (User user : users) { + if (user.getPushKey() == null) { + continue; + } + String checkStr = callId == null? user.getPushKey():(callId + "_" + user.getPushKey()) ; + String checkSign = DigestUtils.md5DigestAsHex(checkStr.getBytes()); + if (checkSign.equals(sign)) { + return true; + } + } + return false; + } + + @Override + public PageInfo getUsers(int page, int count) { + PageHelper.startPage(page, count); + List users = userMapper.getUsers(); + return new PageInfo<>(users); + } + + @Override + public int changePushKey(int id, String pushKey) { + return userMapper.changePushKey(id,pushKey); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java new file mode 100644 index 0000000..69ed485 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.RecordInfo; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult; + +public interface IRedisRpcPlayService { + + + void play(String serverId, Integer channelId, ErrorCallback callback); + + void stop(String serverId, InviteSessionType type, int channelId, String stream); + + void playback(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback callback); + + void playbackPause(String serverId, String streamId); + + void playbackResume(String serverId, String streamId); + + void download(String serverId, Integer channelId, String startTime, String endTime, int downloadSpeed, ErrorCallback callback); + + void queryRecordInfo(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback callback); + + String frontEndCommand(String serverId, Integer channelId, int cmdCode, int parameter1, int parameter2, int combindCode2); + + void playPush(String serverId, Integer id, ErrorCallback callback); + + void playProxy(String serverId, int id, ErrorCallback callback); + + void stopProxy(String serverId, int id); + + DownloadFileInfo getRecordPlayUrl(String serverId, Integer recordId); + + AudioBroadcastResult audioBroadcast(String serverId, String deviceId, String channelDeviceId, Boolean broadcastMode); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcService.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcService.java new file mode 100644 index 0000000..dec5fcf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcService.java @@ -0,0 +1,69 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; + +import java.util.List; + +public interface IRedisRpcService { + + SendRtpInfo getSendRtpItem(String callId); + + WVPResult startSendRtp(String callId, SendRtpInfo sendRtpItem); + + WVPResult stopSendRtp(String callId); + + long waitePushStreamOnline(SendRtpInfo sendRtpItem, CommonCallback callback); + + void stopWaitePushStreamOnline(SendRtpInfo sendRtpItem); + + void rtpSendStopped(String callId); + + void removeCallback(long key); + + long onStreamOnlineEvent(String app, String stream, CommonCallback callback); + void unPushStreamOnlineEvent(String app, String stream); + + void subscribeCatalog(int id, int cycle); + + void subscribeMobilePosition(int id, int cycle, int interval); + + boolean updatePlatform(String serverId, Platform platform); + + void catalogEventPublish(String serverId, CatalogEvent catalogEvent); + + WVPResult devicesSync(String serverId, String deviceId); + + SyncStatus getChannelSyncStatus(String serverId, String deviceId); + + WVPResult deviceBasicConfig(String serverId, Device device, BasicParam basicParam); + + WVPResult deviceConfigQuery(String serverId, Device device, String channelId, String configType); + + void teleboot(String serverId, Device device); + + WVPResult recordControl(String serverId, Device device, String channelId, String recordCmdStr); + + WVPResult guard(String serverId, Device device, String guardCmdStr); + + WVPResult resetAlarm(String serverId, Device device, String channelId, String alarmMethod, String alarmType); + + void iFrame(String serverId, Device device, String channelId); + + WVPResult homePosition(String serverId, Device device, String channelId, Boolean enabled, Integer resetTime, Integer presetIndex); + + void dragZoomIn(String serverId, Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy); + + void dragZoomOut(String serverId, Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy); + + WVPResult deviceStatus(String serverId, Device device); + + WVPResult alarm(String serverId, Device device, String startPriority, String endPriority, String alarmMethod, String alarmType, String startTime, String endTime); + + WVPResult deviceInfo(String serverId, Device device); + + WVPResult> queryPreset(String serverId, Device device, String channelId); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisAlarmMsgListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisAlarmMsgListener.java new file mode 100755 index 0000000..d9453d2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisAlarmMsgListener.java @@ -0,0 +1,180 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.AlarmChannelMessage; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.service.IMobilePositionService; +import com.genersoft.iot.vmp.utils.DateUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 监听 SUBSCRIBE alarm_receive + * 发布 PUBLISH alarm_receive '{ "gbId": "", "alarmSn": 1, "alarmType": "111", "alarmDescription": "222", }' + */ +@Slf4j +@Component +public class RedisAlarmMsgListener implements MessageListener { + + @Autowired + private ISIPCommander commander; + + @Autowired + private ISIPCommanderForPlatform commanderForPlatform; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService channelService; + + @Autowired + private IMobilePositionService mobilePositionService; + + @Autowired + private IPlatformService platformService; + + @Autowired + private IPlatformChannelService platformChannelService; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Autowired + private UserSetting userSetting; + + @Override + public void onMessage(@NotNull Message message, byte[] bytes) { + log.info("[REDIS: ALARM]: {}", new String(message.getBody())); + taskQueue.offer(message); + } + + @Scheduled(fixedDelay = 100) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List messageDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + Message msg = taskQueue.poll(); + if (msg != null) { + messageDataList.add(msg); + } + } + if (messageDataList.isEmpty()) { + return; + } + for (Message msg : messageDataList) { + try { + AlarmChannelMessage alarmChannelMessage = JSON.parseObject(msg.getBody(), AlarmChannelMessage.class); + if (alarmChannelMessage == null) { + log.warn("[REDIS的ALARM通知]消息解析失败"); + continue; + } + String chanelId = alarmChannelMessage.getGbId(); + + DeviceAlarm deviceAlarm = new DeviceAlarm(); + deviceAlarm.setCreateTime(DateUtil.getNow()); + deviceAlarm.setChannelId(chanelId); + deviceAlarm.setAlarmDescription(alarmChannelMessage.getAlarmDescription()); + deviceAlarm.setAlarmMethod("" + alarmChannelMessage.getAlarmSn()); + deviceAlarm.setAlarmType("" + alarmChannelMessage.getAlarmType()); + deviceAlarm.setAlarmPriority("1"); + deviceAlarm.setAlarmTime(DateUtil.getNow()); + deviceAlarm.setLongitude(0); + deviceAlarm.setLatitude(0); + + if (ObjectUtils.isEmpty(chanelId)) { + if (userSetting.getSendToPlatformsWhenIdLost()) { + // 发送给所有的上级 + List parentPlatforms = platformService.queryEnablePlatformList(userSetting.getServerId()); + if (!parentPlatforms.isEmpty()) { + for (Platform parentPlatform : parentPlatforms) { + try { + deviceAlarm.setChannelId(parentPlatform.getDeviceGBId()); + commanderForPlatform.sendAlarmMessage(parentPlatform, deviceAlarm); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 发送报警: {}", e.getMessage()); + } + } + } + } else { + // 获取开启了消息推送的设备和平台 + List parentPlatforms = mobilePositionService.queryEnablePlatformListWithAsMessageChannel(); + if (!parentPlatforms.isEmpty()) { + for (Platform parentPlatform : parentPlatforms) { + try { + deviceAlarm.setChannelId(parentPlatform.getDeviceGBId()); + commanderForPlatform.sendAlarmMessage(parentPlatform, deviceAlarm); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 发送报警: {}", e.getMessage()); + } + } + } + } + // 获取开启了消息推送的设备和平台 + List devices = channelService.queryDeviceWithAsMessageChannel(); + if (!devices.isEmpty()) { + for (Device device : devices) { + try { + deviceAlarm.setChannelId(device.getDeviceId()); + commander.sendAlarmMessage(device, deviceAlarm); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 发送报警: {}", e.getMessage()); + } + } + } + } else { + // 获取该通道ID是属于设备还是对应的上级平台 + Device device = deviceService.getDeviceBySourceChannelDeviceId(chanelId); + List platforms = platformChannelService.queryByPlatformBySharChannelId(chanelId); + if (device != null && device.getServerId().equals(userSetting.getServerId()) && (platforms == null || platforms.isEmpty())) { + try { + commander.sendAlarmMessage(device, deviceAlarm); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 发送报警: {}", e.getMessage()); + } + } else if (device == null && (platforms != null && !platforms.isEmpty() )) { + for (Platform platform : platforms) { + if (platform.getServerId().equals(userSetting.getServerId())) { + try { + commanderForPlatform.sendAlarmMessage(platform, deviceAlarm); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 发送报警: {}", e.getMessage()); + } + } + } + } else { + log.warn("[REDIS的ALARM通知] 未查询到" + chanelId + "所属的平台或设备"); + } + } + } catch (Exception e) { + log.error("未处理的异常 ", e); + log.warn("[REDIS的ALARM通知] 发现未处理的异常, {}", e.getMessage()); + } + } + } +} + diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisCloseStreamMsgListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisCloseStreamMsgListener.java new file mode 100755 index 0000000..f0ba942 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisCloseStreamMsgListener.java @@ -0,0 +1,66 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 接收来自redis的关闭流更新通知 + * 消息举例: PUBLISH VM_MSG_STREAM_PUSH_CLOSE "{'app': 'live', 'stream': 'stream'}" + * @author lin + */ +@Slf4j +@Component +public class RedisCloseStreamMsgListener implements MessageListener { + + @Autowired + private IStreamPushService pushService; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Override + public void onMessage(@NotNull Message message, byte[] bytes) { + log.info("[REDIS: 关闭流]: {}", new String(message.getBody())); + taskQueue.offer(message); + } + + @Scheduled(fixedDelay = 100) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List messageDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + Message msg = taskQueue.poll(); + if (msg != null) { + messageDataList.add(msg); + } + } + if (messageDataList.isEmpty()) { + return; + } + for (Message msg : messageDataList) { + try { + JSONObject jsonObject = JSON.parseObject(msg.getBody()); + String app = jsonObject.getString("app"); + String stream = jsonObject.getString("stream"); + pushService.stopByAppAndStream(app, stream); + }catch (Exception e) { + log.warn("[REDIS的关闭推流通知] 发现未处理的异常, \r\n{}", JSON.toJSONString(msg)); + log.error("[REDIS的关闭推流通知] 异常内容: ", e); + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGpsMsgListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGpsMsgListener.java new file mode 100755 index 0000000..ad6bfe5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGpsMsgListener.java @@ -0,0 +1,101 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 接收来自redis的GPS更新通知 + * + * @author lin + * 监听: SUBSCRIBE VM_MSG_GPS + * 发布 PUBLISH VM_MSG_GPS '{"messageId":"1727228507555","id":"24212345671381000047","lng":116.30307666666667,"lat":40.03295833333333,"time":"2024-09-25T09:41:47","direction":"56.0","speed":0.0,"altitude":60.0,"unitNo":"100000000","memberNo":"10000047"}' + */ +@Slf4j +@Component +public class RedisGpsMsgListener implements MessageListener { + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IStreamPushService streamPushService; + + @Autowired + private IGbChannelService channelService; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + + @Override + public void onMessage(@NotNull Message message, byte[] bytes) { + log.debug("[REDIS: GPS]: {}", new String(message.getBody())); + taskQueue.offer(message); + } + + @Scheduled(fixedDelay = 200, timeUnit = TimeUnit.MILLISECONDS) //每400毫秒执行一次 + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List messageDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + Message msg = taskQueue.poll(); + if (msg != null) { + messageDataList.add(msg); + } + } + if (messageDataList.isEmpty()) { + return; + } + for (Message msg : messageDataList) { + try { + GPSMsgInfo gpsMsgInfo = JSON.parseObject(msg.getBody(), GPSMsgInfo.class); + gpsMsgInfo.setStored(false); + gpsMsgInfo.setTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(gpsMsgInfo.getTime())); + log.info("[REDIS的位置变化通知], {}", JSON.toJSONString(gpsMsgInfo)); + // 只是放入redis缓存起来 + redisCatchStorage.updateGpsMsgInfo(gpsMsgInfo); + } catch (Exception e) { + log.warn("[REDIS的位置变化通知] 发现未处理的异常, \r\n{}", JSON.toJSONString(msg)); + log.error("[REDIS的位置变化通知] 异常内容: ", e); + } + } + } + + /** + * 定时将经纬度更新到数据库 + */ + @Scheduled(fixedDelay = 2, timeUnit = TimeUnit.SECONDS) //每2秒执行一次 + public void execute() { + // 需要查询到 + List gpsMsgInfoList = redisCatchStorage.getAllGpsMsgInfo(); + if (!gpsMsgInfoList.isEmpty()) { + gpsMsgInfoList = gpsMsgInfoList.stream().filter(gpsMsgInfo -> !gpsMsgInfo.isStored()).collect(Collectors.toList());; + if (!gpsMsgInfoList.isEmpty()) { + channelService.updateGPSFromGPSMsgInfo(gpsMsgInfoList); + for (GPSMsgInfo msgInfo : gpsMsgInfoList) { + msgInfo.setStored(true); + redisCatchStorage.updateGpsMsgInfo(msgInfo); + } + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGroupChangeListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGroupChangeListener.java new file mode 100755 index 0000000..d6e8e4a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGroupChangeListener.java @@ -0,0 +1,264 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.Group; +import com.genersoft.iot.vmp.gb28181.bean.RedisGroupMessage; +import com.genersoft.iot.vmp.gb28181.service.IGroupService; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.utils.DateUtil; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * @Auther: JiangFeng + * @Date: 2022/8/16 11:32 + * @Description: 接收redis发送的推流设备列表更新通知 + * 监听: SUBSCRIBE VM_MSG_GROUP_LIST_CHANGE + * 发布 PUBLISH VM_MSG_GROUP_LIST_CHANGE '[{"groupName":"测试域修改新","topGroupGAlias":3,"messageType":"update","groupAlias":3}]' + */ +@Slf4j +@Component +public class RedisGroupChangeListener implements MessageListener { + + @Resource + private IGroupService groupService; + + @Resource + private IStreamPushService streamPushService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private SipConfig sipConfig; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Override + public void onMessage(Message message, byte[] bytes) { + log.info("[REDIS-分组信息改变] key: {}, : {}", VideoManagerConstants.VM_MSG_GROUP_LIST_CHANGE, new String(message.getBody())); + taskQueue.offer(message); + } + + @Scheduled(fixedDelay = 100) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List messageDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + Message msg = taskQueue.poll(); + if (msg != null) { + messageDataList.add(msg); + } + } + if (messageDataList.isEmpty()) { + return; + } + for (Message msg : messageDataList) { + try { + List groupMessages = JSON.parseArray(new String(msg.getBody()), RedisGroupMessage.class); + for (int i = 0; i < groupMessages.size(); i++) { + RedisGroupMessage groupMessage = groupMessages.get(i); + log.info("[REDIS消息-分组信息更新] {}", groupMessage.toString()); + switch (groupMessage.getMessageType()){ + case "add": + if (!userSetting.isUseAliasForGroupSync()) { + if (groupMessage.getGroupGbId() == null) { + log.info("[REDIS消息-分组信息新增] 分组编号未设置,{}", groupMessage.toString()); + continue; + } + Group group = groupService.queryGroupByDeviceId(groupMessage.getGroupGbId()); + if (group != null) { + log.info("[REDIS消息-分组信息新增] 失败 {},编号已经存在", groupMessage.getGroupGbId()); + continue; + } + if (ObjectUtils.isEmpty(groupMessage.getGroupName()) + || ObjectUtils.isEmpty(groupMessage.getTopGroupGbId()) ){ + log.info("[REDIS消息-分组信息新增] 消息关键字段缺失, {}", groupMessage.toString()); + continue; + } + group = new Group(); + group.setDeviceId(groupMessage.getGroupGbId()); + group.setAlias(groupMessage.getGroupAlias()); + group.setParentDeviceId(groupMessage.getParentGroupGbId()); + group.setBusinessGroup(groupMessage.getTopGroupGbId()); + group.setCreateTime(DateUtil.getNow()); + group.setUpdateTime(DateUtil.getNow()); + groupService.add(group); + + }else { + // 此处使用别名作为判断依据,别名此处常常是分组在第三方系统里的唯一ID + if (groupMessage.getGroupAlias() == null || ObjectUtils.isEmpty(groupMessage.getGroupName()) + || ObjectUtils.isEmpty(groupMessage.getTopGroupGAlias())) { + log.info("[REDIS消息-分组信息新增] 消息关键字段缺失, {}", groupMessage.toString()); + continue; + } + Group group = groupService.queryGroupByAlias(groupMessage.getGroupAlias()); + if (group != null) { + log.info("[REDIS消息-分组信息新增] 失败 {},别名已经存在", groupMessage.getGroupGbId()); + continue; + } + group = new Group(); + boolean isTop = groupMessage.getTopGroupGAlias().equals(groupMessage.getGroupAlias()); + String deviceId = buildGroupDeviceId(isTop); + group.setDeviceId(deviceId); + group.setAlias(groupMessage.getGroupAlias()); + group.setName(groupMessage.getGroupName()); + if (!isTop) { + if (ObjectUtils.isEmpty(groupMessage.getTopGroupGAlias()) ) { + log.info("[REDIS消息-分组信息新增] 消息缺失业务分组别名或者父节点别名, {}", groupMessage.toString()); + continue; + } + + Group topGroup = groupService.queryGroupByAlias(groupMessage.getTopGroupGAlias()); + if (topGroup == null) { + log.info("[REDIS消息-分组信息新增] 业务分组信息未入库, {}", groupMessage.toString()); + continue; + } + group.setBusinessGroup(topGroup.getDeviceId()); + group.setParentId(topGroup.getId()); + } + if (groupMessage.getParentGAlias() != null) { + Group parentGroup = groupService.queryGroupByAlias(groupMessage.getParentGAlias()); + if (parentGroup == null) { + log.info("[REDIS消息-分组信息新增] 虚拟组织父节点信息未入库, {}", groupMessage.toString()); + continue; + } + group.setParentId(parentGroup.getId()); + group.setParentDeviceId(parentGroup.getDeviceId()); + } + group.setCreateTime(DateUtil.getNow()); + group.setUpdateTime(DateUtil.getNow()); + groupService.add(group); + } + + break; + case "update": + if (!userSetting.isUseAliasForGroupSync()) { + if (groupMessage.getGroupGbId() == null) { + log.info("[REDIS消息-分组信息更新] 分组编号未设置,{}", groupMessage.toString()); + continue; + } + Group group = groupService.queryGroupByDeviceId(groupMessage.getGroupGbId()); + if (group == null) { + log.info("[REDIS消息-分组信息更新] 失败 {},编号不存在", groupMessage.getGroupGbId()); + continue; + } + group.setDeviceId(groupMessage.getGroupGbId()); + group.setAlias(groupMessage.getGroupAlias()); + group.setParentDeviceId(groupMessage.getParentGroupGbId()); + group.setBusinessGroup(groupMessage.getTopGroupGbId()); + group.setUpdateTime(DateUtil.getNow()); + groupService.update(group); + }else { + // 此处使用别名作为判断依据,别名此处常常是分组在第三方系统里的唯一ID + if (groupMessage.getGroupAlias() == null) { + log.info("[REDIS消息-分组信息更新] 消息关键字段缺失, {}", groupMessage.toString()); + continue; + } + Group group = groupService.queryGroupByAlias(groupMessage.getGroupAlias()); + if (group == null ) { + log.info("[REDIS消息-分组信息更新] 失败 {},别名不存在", groupMessage.getGroupAlias()); + continue; + } + group.setName(groupMessage.getGroupName()); + group.setUpdateTime(DateUtil.getNow()); + if (groupMessage.getParentGAlias() != null) { + Group parentGroup = groupService.queryGroupByAlias(groupMessage.getParentGAlias()); + if (parentGroup == null) { + log.info("[REDIS消息-分组信息更新] 虚拟组织父节点信息未入库, {}", groupMessage.toString()); + continue; + } + group.setParentId(parentGroup.getId()); + group.setParentDeviceId(parentGroup.getDeviceId()); + }else { + Group businessGroup = groupService.queryGroupByDeviceId(group.getBusinessGroup()); + if (businessGroup == null ) { + log.info("[REDIS消息-分组信息更新] 失败 {},业务分组不存在", groupMessage.getGroupAlias()); + continue; + } + group.setParentId(businessGroup.getId()); + group.setParentDeviceId(null); + } + groupService.update(group); + } + break; + case "delete": + if (!userSetting.isUseAliasForGroupSync()) { + if (groupMessage.getGroupGbId() == null) { + log.info("[REDIS消息-分组信息删除] 分组编号未设置,{}", groupMessage.toString()); + continue; + } + Group group = groupService.queryGroupByDeviceId(groupMessage.getGroupGbId()); + if (group == null) { + log.info("[REDIS消息-分组信息删除] 失败 {},编号不存在", groupMessage.getGroupGbId()); + continue; + } + groupService.delete(group.getId()); + }else { + // 此处使用别名作为判断依据,别名此处常常是分组在第三方系统里的唯一ID + if (groupMessage.getGroupAlias() == null) { + log.info("[REDIS消息-分组信息删除] 消息关键字段缺失, {}", groupMessage.toString()); + continue; + } + Group group = groupService.queryGroupByAlias(groupMessage.getGroupAlias()); + if (group == null) { + log.info("[REDIS消息-分组信息删除] 失败 {},别名不存在", groupMessage.getGroupAlias()); + continue; + } + groupService.delete(group.getId()); + } + break; + default: + log.info("[REDIS消息-分组信息改变] 未识别的消息类型 {},目前支持的消息类型为 add、update、delete", groupMessage.getMessageType()); + } + } + + } catch (Exception e) { + log.warn("[REDIS消息-业务分组同步回复] 发现未处理的异常, \r\n{}", new String(msg.getBody())); + log.error("[REDIS消息-业务分组同步回复] 异常内容: ", e); + } + } + + } + + /** + * 生成分组国标编号 + */ + private String buildGroupDeviceId(boolean isTop) { + try { + String deviceTemplate = userSetting.getGroupSyncDeviceTemplate(); + if (ObjectUtils.isEmpty(deviceTemplate) || !deviceTemplate.contains("%s")) { + String domain = sipConfig.getDomain(); + if (domain.length() != 10) { + domain = sipConfig.getId().substring(0, 10); + } + deviceTemplate = domain + "%s0%s"; + } + String codeType = "216"; + if (isTop) { + codeType = "215"; + } + return String.format(deviceTemplate, codeType, RandomStringUtils.secureStrong().next(6, false, true)); + }catch (Exception e) { + log.error("[REDIS消息-业务分组同步回复] 构建新的分组编号失败", e); + return null; + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGroupMsgListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGroupMsgListener.java new file mode 100755 index 0000000..bfa4a5d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGroupMsgListener.java @@ -0,0 +1,200 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.Group; +import com.genersoft.iot.vmp.gb28181.bean.RedisGroupMessage; +import com.genersoft.iot.vmp.gb28181.service.IGroupService; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.utils.DateUtil; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 接收redis发送的推流设备列表更新通知 + * 监听: SUBSCRIBE VM_MSG_GROUP_LIST_RESPONSE + * 发布 PUBLISH VM_MSG_GROUP_LIST_RESPONSE '[{"groupName":"研发AAS","topGroupGAlias":"6","groupAlias":"6"},{"groupName":"测试AAS","topGroupGAlias":"5","groupAlias":"5"},{"groupName":"研发2","topGroupGAlias":"4","groupAlias":"4"},{"groupName":"啊实打实的","topGroupGAlias":"4","groupAlias":"100000009"},{"groupName":"测试域","topGroupGAlias":"3","groupAlias":"3"},{"groupName":"结构1","topGroupGAlias":"3","groupAlias":"100000000"},{"groupName":"结构1-1","topGroupGAlias":"3","parentGAlias":"100000000","groupAlias":"100000001"},{"groupName":"结构2-2","topGroupGAlias":"3","groupAlias":"100000002"},{"groupName":"结构1-2","topGroupGAlias":"3","parentGAlias":"100000001","groupAlias":"100000003"},{"groupName":"结构1-3","topGroupGAlias":"3","parentGAlias":"100000003","groupAlias":"100000004"},{"groupName":"研发1","topGroupGAlias":"3","groupAlias":"100000005"},{"groupName":"研发3","topGroupGAlias":"3","parentGAlias":"100000005","groupAlias":"100000006"},{"groupName":"测试42","topGroupGAlias":"3","parentGAlias":"100000000","groupAlias":"100000010"},{"groupName":"测试2","topGroupGAlias":"3","parentGAlias":"100000000","groupAlias":"100000011"},{"groupName":"测试3","topGroupGAlias":"3","parentGAlias":"100000000","groupAlias":"100000007"},{"groupName":"测试4","topGroupGAlias":"3","parentGAlias":"100000007","groupAlias":"100000008"}]' + */ +@Slf4j +@Component +public class RedisGroupMsgListener implements MessageListener { + + @Resource + private IGroupService groupService; + + @Resource + private IStreamPushService streamPushService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private SipConfig sipConfig; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Override + public void onMessage(Message message, byte[] bytes) { + log.info("[REDIS: 业务分组同步回复] key: {}, : {}", VideoManagerConstants.VM_MSG_GROUP_LIST_RESPONSE, new String(message.getBody())); + taskQueue.offer(message); + } + + @Scheduled(fixedDelay = 100) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List messageDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + Message msg = taskQueue.poll(); + if (msg != null) { + messageDataList.add(msg); + } + } + if (messageDataList.isEmpty()) { + return; + } + if (userSetting.isUseAliasForGroupSync()) { + log.info("[REDIS消息-业务分组同步回复] 使用别名作为唯一ID解析分组消息"); + } + for (Message msg : messageDataList) { + try { + List groupMessages = JSON.parseArray(new String(msg.getBody()), RedisGroupMessage.class); + for (int i = 0; i < groupMessages.size(); i++) { + RedisGroupMessage groupMessage = groupMessages.get(i); + log.info("[REDIS消息-业务分组同步回复] {}", groupMessage.toString()); + if (!userSetting.isUseAliasForGroupSync()) { + if (groupMessage.getGroupGbId() == null) { + log.warn("[REDIS消息-业务分组同步回复] 分组编号未设置,{}", groupMessage.toString()); + continue; + } + Group group = groupService.queryGroupByDeviceId(groupMessage.getGroupGbId()); + if (group == null ) { + if (ObjectUtils.isEmpty(groupMessage.getGroupName()) + || ObjectUtils.isEmpty(groupMessage.getTopGroupGbId()) ){ + log.info("[REDIS消息-业务分组同步回复] 消息关键字段缺失, {}", groupMessage.toString()); + continue; + } + group = new Group(); + group.setDeviceId(groupMessage.getGroupGbId()); + group.setAlias(groupMessage.getGroupAlias()); + group.setParentDeviceId(groupMessage.getParentGroupGbId()); + group.setBusinessGroup(groupMessage.getTopGroupGbId()); + group.setCreateTime(DateUtil.getNow()); + group.setUpdateTime(DateUtil.getNow()); + groupService.add(group); + + }else { + group.setDeviceId(groupMessage.getGroupGbId()); + group.setAlias(groupMessage.getGroupAlias()); + group.setParentDeviceId(groupMessage.getParentGroupGbId()); + group.setBusinessGroup(groupMessage.getTopGroupGbId()); + group.setUpdateTime(DateUtil.getNow()); + groupService.update(group); + } + }else { + // 此处使用别名作为判断依据,别名此处常常是分组在第三方系统里的唯一ID + if (groupMessage.getGroupAlias() == null || ObjectUtils.isEmpty(groupMessage.getGroupName()) + || ObjectUtils.isEmpty(groupMessage.getTopGroupGAlias())) { + log.info("[REDIS消息-业务分组同步回复] 消息关键字段缺失, {}", groupMessage.toString()); + continue; + } + boolean isTop = groupMessage.getTopGroupGAlias().equals(groupMessage.getGroupAlias()); + Group group = groupService.queryGroupByAlias(groupMessage.getGroupAlias()); + if (group == null ) { + group = new Group(); + String deviceId = buildGroupDeviceId(isTop); + group.setDeviceId(deviceId); + group.setAlias(groupMessage.getGroupAlias()); + group.setName(groupMessage.getGroupName()); + group.setCreateTime(DateUtil.getNow()); + } + + if (!isTop) { + if (ObjectUtils.isEmpty(groupMessage.getTopGroupGAlias())) { + log.info("[REDIS消息-业务分组同步回复] 消息缺失业务分组别名, {}", groupMessage.toString()); + continue; + } + + Group topGroup = groupService.queryGroupByAlias(groupMessage.getTopGroupGAlias()); + if (topGroup == null) { + log.info("[REDIS消息-业务分组同步回复] 业务分组信息未入库, {}", groupMessage.toString()); + continue; + } + group.setBusinessGroup(topGroup.getDeviceId()); + if (groupMessage.getParentGAlias() != null) { + Group parentGroup = groupService.queryGroupByAlias(groupMessage.getParentGAlias()); + if (parentGroup == null) { + log.info("[REDIS消息-业务分组同步回复] 虚拟组织父节点信息未入库, {}", groupMessage.toString()); + continue; + } + group.setParentId(parentGroup.getId()); + group.setParentDeviceId(parentGroup.getDeviceId()); + }else { + group.setParentId(topGroup.getId()); + group.setParentDeviceId(null); + } + }else { + group.setParentId(null); + group.setParentDeviceId(null); + } + + group.setUpdateTime(DateUtil.getNow()); + if (group.getId() > 0) { + groupService.update(group); + }else { + groupService.add(group); + } + + } + } + + } catch (ControllerException e) { + log.warn("[REDIS消息-业务分组同步回复] 失败, \r\n{}", e.getMsg()); + }catch (Exception e) { + log.warn("[REDIS消息-业务分组同步回复] 发现未处理的异常, \r\n{}", new String(msg.getBody())); + log.error("[REDIS消息-业务分组同步回复] 异常内容: ", e); + } + } + + } + + /** + * 生成分组国标编号 + */ + private String buildGroupDeviceId(boolean isTop) { + try { + String deviceTemplate = userSetting.getGroupSyncDeviceTemplate(); + if (ObjectUtils.isEmpty(deviceTemplate) || !deviceTemplate.contains("%s")) { + String domain = sipConfig.getDomain(); + if (domain.length() != 10) { + domain = sipConfig.getId().substring(0, 10); + } + deviceTemplate = domain + "%s0%s"; + } + String codeType = "216"; + if (isTop) { + codeType = "215"; + } + return String.format(deviceTemplate, codeType, RandomStringUtils.secureStrong().next(6, false, true)); + }catch (Exception e) { + log.error("[REDIS消息-业务分组同步回复] 构建新的分组编号失败", e); + return null; + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamListMsgListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamListMsgListener.java new file mode 100755 index 0000000..954aa37 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamListMsgListener.java @@ -0,0 +1,130 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.streamPush.bean.RedisPushStreamMessage; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * @Auther: JiangFeng + * @Date: 2022/8/16 11:32 + * @Description: 接收redis发送的推流设备列表更新通知 + * 监听: SUBSCRIBE VM_MSG_PUSH_STREAM_LIST_CHANGE + * 发布 PUBLISH VM_MSG_PUSH_STREAM_LIST_CHANGE '[{"app":1000,"stream":10000000,"gbId":"12345678901234567890","name":"A6","status":false},{"app":1000,"stream":10000021,"gbId":"24212345671381000021","name":"终端9273","status":false},{"app":1000,"stream":10000022,"gbId":"24212345671381000022","name":"终端9434","status":true},{"app":1000,"stream":10000025,"gbId":"24212345671381000025","name":"华为M10","status":false},{"app":1000,"stream":10000051,"gbId":"11111111111381111122","name":"终端9720","status":false}]' + */ +@Slf4j +@Component +public class RedisPushStreamListMsgListener implements MessageListener { + + @Resource + private IMediaServerService mediaServerService; + + @Resource + private IStreamPushService streamPushService; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Override + public void onMessage(Message message, byte[] bytes) { + log.info("[REDIS: 推流设备列表更新]: {}", new String(message.getBody())); + taskQueue.offer(message); + } + + @Scheduled(fixedDelay = 100) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List messageDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + Message msg = taskQueue.poll(); + if (msg != null) { + messageDataList.add(msg); + } + } + if (messageDataList.isEmpty()) { + return; + } + for (Message msg : messageDataList) { + try { + List streamPushItems = JSON.parseArray(new String(msg.getBody()), RedisPushStreamMessage.class); + //查询全部的app+stream 用于判断是添加还是修改 + Map allAppAndStream = streamPushService.getAllAppAndStreamMap(); + Map allGBId = streamPushService.getAllGBId(); + + // 用于存储更具APP+Stream过滤后的数据,可以直接存入stream_push表与gb_stream表 + List streamPushItemForSave = new ArrayList<>(); + List streamPushItemForUpdate = new ArrayList<>(); + for (RedisPushStreamMessage pushStreamMessage : streamPushItems) { + String app = pushStreamMessage.getApp(); + String stream = pushStreamMessage.getStream(); + boolean contains = allAppAndStream.containsKey(app + stream); + //不存在就添加 + if (!contains) { + if (allGBId.containsKey(pushStreamMessage.getGbId())) { + StreamPush streamPushInDb = allGBId.get(pushStreamMessage.getGbId()); + log.warn("[REDIS消息-推流设备列表更新-INSERT] 国标编号重复: {}, 已分配给{}/{}", + streamPushInDb.getGbDeviceId(), streamPushInDb.getApp(), streamPushInDb.getStream()); + continue; + } + StreamPush streamPush = pushStreamMessage.buildstreamPush(); + streamPush.setCreateTime(DateUtil.getNow()); + streamPush.setUpdateTime(DateUtil.getNow()); + streamPush.setMediaServerId(mediaServerService.getDefaultMediaServer().getId()); + streamPushItemForSave.add(streamPush); + allGBId.put(streamPush.getGbDeviceId(), streamPush); + } else { + StreamPush streamPushForGbDeviceId = allGBId.get(pushStreamMessage.getGbId()); + if (streamPushForGbDeviceId != null + && (!streamPushForGbDeviceId.getApp().equals(pushStreamMessage.getApp()) + || !streamPushForGbDeviceId.getStream().equals(pushStreamMessage.getStream()))) { + StreamPush streamPushInDb = allGBId.get(pushStreamMessage.getGbId()); + log.warn("[REDIS消息-推流设备列表更新-UPDATE] 国标编号重复: {}, 已分配给{}/{}", + pushStreamMessage.getGbId(), streamPushInDb.getApp(), streamPushInDb.getStream()); + continue; + } + StreamPush streamPush = allAppAndStream.get(app + stream); + streamPush.setUpdateTime(DateUtil.getNow()); + streamPush.setGbDeviceId(pushStreamMessage.getGbId()); + streamPush.setGbName(pushStreamMessage.getName()); + if (pushStreamMessage.getStatus() != null) { + streamPush.setGbStatus(pushStreamMessage.getStatus() ? "ON" : "OFF"); + } + //存在就只修改 name和gbId + streamPushItemForUpdate.add(streamPush); + } + } + if (!streamPushItemForSave.isEmpty()) { + log.info("添加{}条", streamPushItemForSave.size()); + log.info(JSONObject.toJSONString(streamPushItemForSave)); + streamPushService.batchAdd(streamPushItemForSave); + + } + if (!streamPushItemForUpdate.isEmpty()) { + log.info("修改{}条", streamPushItemForUpdate.size()); + log.info(JSONObject.toJSONString(streamPushItemForUpdate)); + streamPushService.batchUpdate(streamPushItemForUpdate); + } + } catch (Exception e) { + log.warn("[REDIS消息-推流设备列表更新] 发现未处理的异常, \r\n{}", new String(msg.getBody())); + log.error("[REDIS消息-推流设备列表更新] 异常内容: ", e); + } + } + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamResponseListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamResponseListener.java new file mode 100644 index 0000000..f8a78dc --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamResponseListener.java @@ -0,0 +1,90 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.service.bean.MessageForPushChannelResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 接收redis返回的推流结果 + * + * @author lin + * PUBLISH VM_MSG_STREAM_PUSH_RESPONSE '{"code":0,"msg":"失败","app":"1000","stream":"10000022"}' + */ +@Slf4j +@Component +public class RedisPushStreamResponseListener implements MessageListener { + + private ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Qualifier("taskExecutor") + @Autowired + private ThreadPoolTaskExecutor taskExecutor; + + private final Map responseEvents = new ConcurrentHashMap<>(); + + public interface PushStreamResponseEvent { + void run(MessageForPushChannelResponse response); + } + + @Override + public void onMessage(Message message, byte[] bytes) { + log.info("[REDIS: 推流结果]: {}", new String(message.getBody())); + taskQueue.offer(message); + } + + @Scheduled(fixedDelay = 100) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List messageDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + Message msg = taskQueue.poll(); + if (msg != null) { + messageDataList.add(msg); + } + } + if (messageDataList.isEmpty()) { + return; + } + for (Message msg : messageDataList) { + try { + MessageForPushChannelResponse response = JSON.parseObject(new String(msg.getBody()), MessageForPushChannelResponse.class); + if (response == null || ObjectUtils.isEmpty(response.getApp()) || ObjectUtils.isEmpty(response.getStream())) { + log.info("[REDIS消息-请求推流结果]:参数不全"); + continue; + } + // 查看正在等待的invite消息 + if (responseEvents.get(response.getApp() + response.getStream()) != null) { + responseEvents.get(response.getApp() + response.getStream()).run(response); + } + } catch (Exception e) { + log.warn("[REDIS消息-请求推流结果] 发现未处理的异常, \r\n{}", JSON.toJSONString(msg)); + log.error("[REDIS消息-请求推流结果] 异常内容: ", e); + } + } + } + + public void addEvent(String app, String stream, PushStreamResponseEvent callback) { + responseEvents.put(app + stream, callback); + } + + public void removeEvent(String app, String stream) { + responseEvents.remove(app + stream); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamStatusMsgListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamStatusMsgListener.java new file mode 100755 index 0000000..e3d4e96 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamStatusMsgListener.java @@ -0,0 +1,122 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.service.bean.PushStreamStatusChangeFromRedisDto; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + + +/** + * 接收redis发送的推流设备上线下线通知 + * + * @author lin + * 发送 PUBLISH VM_MSG_PUSH_STREAM_STATUS_CHANGE '{"setAllOffline":false,"offlineStreams":[{"app":"1000","stream":"10000022","timeStamp":1726729716551}]}' + * 订阅 SUBSCRIBE VM_MSG_PUSH_STREAM_STATUS_CHANGE + */ +@Slf4j +@Component +public class RedisPushStreamStatusMsgListener implements MessageListener, ApplicationRunner { + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IStreamPushService streamPushService; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private UserSetting userSetting; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Override + public void onMessage(Message message, byte[] bytes) { + log.info("[REDIS: 推流设备状态变化]: {}", new String(message.getBody())); + taskQueue.offer(message); + } + + @Scheduled(fixedDelay = 100) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List messageDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + Message msg = taskQueue.poll(); + if (msg != null) { + messageDataList.add(msg); + } + } + if (messageDataList.isEmpty()) { + return; + } + for (Message msg : messageDataList) { + try { + PushStreamStatusChangeFromRedisDto streamStatusMessage = JSON.parseObject(msg.getBody(), PushStreamStatusChangeFromRedisDto.class); + if (streamStatusMessage == null) { + log.warn("[REDIS消息]推流设备状态变化消息解析失败"); + continue; + } + // 取消定时任务 + dynamicTask.stop(VideoManagerConstants.VM_MSG_GET_ALL_ONLINE_REQUESTED); + if (streamStatusMessage.isSetAllOffline()) { + // 所有设备离线 + streamPushService.allOffline(); + } + if (streamStatusMessage.getOfflineStreams() != null + && !streamStatusMessage.getOfflineStreams().isEmpty()) { + // 更新部分设备离线 + log.info("[REDIS: 推流设备状态变化] 更新部分设备离线: {}个", streamStatusMessage.getOfflineStreams().size()); + streamPushService.offline(streamStatusMessage.getOfflineStreams()); + } + if (streamStatusMessage.getOnlineStreams() != null && + !streamStatusMessage.getOnlineStreams().isEmpty()) { + // 更新部分设备上线 + log.info("[REDIS: 推流设备状态变化] 更新部分设备上线: {}个", streamStatusMessage.getOnlineStreams().size()); + streamPushService.online(streamStatusMessage.getOnlineStreams()); + } + } catch (Exception e) { + log.warn("[REDIS消息-推流设备状态变化] 发现未处理的异常, \r\n{}", JSON.parseObject(msg.getBody())); + log.error("[REDIS消息-推流设备状态变化] 异常内容: ", e); + } + } + } + + @Override + public void run(ApplicationArguments args) throws Exception { + if (userSetting.getUsePushingAsStatus()) { + return; + } + // 查询是否存在推流设备,没有则不发送 + List allAppAndStream = streamPushService.getAllAppAndStream(); + if (allAppAndStream == null || allAppAndStream.isEmpty()) { + return; + } + // 启动时设置所有推流通道离线,发起查询请求 + redisCatchStorage.sendStreamPushRequestedMsgForStatus(); + dynamicTask.startDelay(VideoManagerConstants.VM_MSG_GET_ALL_ONLINE_REQUESTED, () -> { + log.info("[REDIS消息]未收到redis回复推流设备状态,执行推流设备离线"); + // 五秒收不到请求就设置通道离线,然后通知上级离线 + streamPushService.allOffline(); + }, 5000); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java new file mode 100644 index 0000000..acd8e81 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java @@ -0,0 +1,345 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPTZService; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import javax.sip.message.Response; + +@Component +@Slf4j +@RedisRpcController("channel") +public class RedisRpcChannelPlayController extends RpcController { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IGbChannelPlayService channelPlayService; + + @Autowired + private IPTZService iptzService; + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message); + } + + + /** + * 点播国标设备 + */ + @RedisRpcMapping("play") + public RedisRpcResponse playChannel(RedisRpcRequest request) { + int channelId = Integer.parseInt(request.getParam().toString()); + RedisRpcResponse response = request.getResponse(); + + if (channelId <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + // 获取对应的设备和通道信息 + CommonGBChannel channel = channelService.getOne(channelId); + if (channel == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + + InviteMessageInfo inviteInfo = new InviteMessageInfo(); + inviteInfo.setSessionName("Play"); + channelPlayService.startInvite(channel, inviteInfo, null, (code, msg, data) ->{ + if (code == InviteErrorCode.SUCCESS.getCode()) { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(data); + }else { + response.setStatusCode(code); + } + // 手动发送结果 + sendResponse(response); + }); + return null; + } + + + /** + * 点播国标设备 + */ + @RedisRpcMapping("queryRecordInfo") + public RedisRpcResponse queryRecordInfo(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + int channelId = paramJson.getIntValue("channelId"); + String startTime = paramJson.getString("startTime"); + String endTime = paramJson.getString("endTime"); + RedisRpcResponse response = request.getResponse(); + + if (channelId <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + // 获取对应的设备和通道信息 + CommonGBChannel channel = channelService.getOne(channelId); + if (channel == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + + try { + channelPlayService.queryRecord(channel, startTime, endTime, (code, msg, data) ->{ + if (code == InviteErrorCode.SUCCESS.getCode()) { + response.setStatusCode(code); + response.setBody(data); + }else { + response.setStatusCode(code); + } + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(ErrorCode.ERROR100.getCode()); + response.setBody(e.getMessage()); + } + + return null; + } + + /** + * 暂停录像回放 + */ + @RedisRpcMapping("playbackPause") + public RedisRpcResponse playbackPause(RedisRpcRequest request) { + String streamId = request.getParam().toString(); + RedisRpcResponse response = request.getResponse(); + + if (streamId == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + + try { +// channelPlayService.playbackPause(streamId); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + }catch (ControllerException e) { + response.setStatusCode(ErrorCode.ERROR100.getCode()); + response.setBody(e.getMessage()); + } + + return response; + } + + /** + * 恢复录像回放 + */ + @RedisRpcMapping("playbackResume") + public RedisRpcResponse playbackResume(RedisRpcRequest request) { + String streamId = request.getParam().toString(); + RedisRpcResponse response = request.getResponse(); + + if (streamId == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + + try { +// channelPlayService.playbackResume(streamId); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + }catch (ControllerException e) { + response.setStatusCode(ErrorCode.ERROR100.getCode()); + response.setBody(e.getMessage()); + } + + return response; + } + + + /** + * 停止点播国标设备 + */ + @RedisRpcMapping("stop") + public RedisRpcResponse stop(RedisRpcRequest request) { + JSONObject jsonObject = JSONObject.parseObject(request.getParam().toString()); + + RedisRpcResponse response = request.getResponse(); + + Integer channelId = jsonObject.getIntValue("channelId"); + if (channelId == null || channelId <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + + String stream = jsonObject.getString("stream"); + InviteSessionType type = jsonObject.getObject("inviteSessionType", InviteSessionType.class); + + // 获取对应的设备和通道信息 + CommonGBChannel channel = channelService.getOne(channelId); + if (channel == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + channelPlayService.stopInvite(type, channel, stream); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + }catch (Exception e){ + response.setStatusCode(Response.SERVER_INTERNAL_ERROR); + response.setBody(e.getMessage()); + } + return response; + } + + /** + * 录像回放国标设备 + */ + @RedisRpcMapping("playback") + public RedisRpcResponse playbackChannel(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + int channelId = paramJson.getIntValue("channelId"); + String startTime = paramJson.getString("startTime"); + String endTime = paramJson.getString("endTime"); + RedisRpcResponse response = request.getResponse(); + + if (channelId <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + // 获取对应的设备和通道信息 + CommonGBChannel channel = channelService.getOne(channelId); + if (channel == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + + InviteMessageInfo inviteInfo = new InviteMessageInfo(); + inviteInfo.setSessionName("Playback"); + inviteInfo.setStartTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime)); + inviteInfo.setStopTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime)); + channelPlayService.startInvite(channel, inviteInfo, null, (code, msg, data) ->{ + if (code == InviteErrorCode.SUCCESS.getCode()) { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(data); + }else { + response.setStatusCode(code); + } + // 手动发送结果 + sendResponse(response); + }); + return null; + } + + /** + * 录像回放国标设备 + */ + @RedisRpcMapping("download") + public RedisRpcResponse downloadChannel(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + int channelId = paramJson.getIntValue("channelId"); + String startTime = paramJson.getString("startTime"); + String endTime = paramJson.getString("endTime"); + int downloadSpeed = paramJson.getIntValue("downloadSpeed"); + RedisRpcResponse response = request.getResponse(); + + if (channelId <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + // 获取对应的设备和通道信息 + CommonGBChannel channel = channelService.getOne(channelId); + if (channel == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + + InviteMessageInfo inviteInfo = new InviteMessageInfo(); + inviteInfo.setSessionName("Download"); + inviteInfo.setStartTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime)); + inviteInfo.setStopTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime)); + inviteInfo.setDownloadSpeed(downloadSpeed + ""); + channelPlayService.startInvite(channel, inviteInfo, null, (code, msg, data) ->{ + if (code == InviteErrorCode.SUCCESS.getCode()) { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(data); + }else { + response.setStatusCode(code); + } + // 手动发送结果 + sendResponse(response); + }); + return null; + } + + /** + * 云台控制 + */ + @RedisRpcMapping("ptz/frontEndCommand") + public RedisRpcResponse frontEndCommand(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + int channelId = paramJson.getIntValue("channelId"); + int cmdCode = paramJson.getIntValue("cmdCode"); + int parameter1 = paramJson.getIntValue("parameter1"); + int parameter2 = paramJson.getIntValue("parameter2"); + int combindCode2 = paramJson.getIntValue("combindCode2"); + + RedisRpcResponse response = request.getResponse(); + + if (channelId <= 0 || cmdCode < 0 || parameter1 < 0 || parameter2 < 0 || combindCode2 < 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + // 获取对应的设备和通道信息 + CommonGBChannel channel = channelService.getOne(channelId); + if (channel == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + iptzService.frontEndCommand(channel, cmdCode, parameter1, parameter2, combindCode2); + }catch (ControllerException e) { + response.setStatusCode(ErrorCode.ERROR100.getCode()); + response.setBody(e.getMessage()); + return response; + } + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcCloudRecordController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcCloudRecordController.java new file mode 100644 index 0000000..ae6c5a1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcCloudRecordController.java @@ -0,0 +1,66 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.service.ICloudRecordService; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RedisRpcController("cloudRecord") +public class RedisRpcCloudRecordController extends RpcController { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private ICloudRecordService cloudRecordService; + + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message); + } + + /** + * 播放 + */ + @RedisRpcMapping("play") + public RedisRpcResponse play(RedisRpcRequest request) { + int id = Integer.parseInt(request.getParam().toString()); + RedisRpcResponse response = request.getResponse(); + if (id <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + DownloadFileInfo downloadFileInfo = cloudRecordService.getPlayUrlPath(id); + if (downloadFileInfo == null) { + response.setStatusCode(ErrorCode.ERROR100.getCode()); + response.setBody("get play url error"); + return response; + } + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(JSONObject.toJSONString(downloadFileInfo)); + return response; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcDeviceController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcDeviceController.java new file mode 100644 index 0000000..ee6eec4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcDeviceController.java @@ -0,0 +1,503 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.BasicParam; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.SyncStatus; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RedisRpcController("device") +public class RedisRpcDeviceController extends RpcController { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IStreamProxyService streamProxyService; + + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message); + } + + /** + * 通道同步 + */ + @RedisRpcMapping("devicesSync") + public RedisRpcResponse devicesSync(RedisRpcRequest request) { + String deviceId = request.getParam().toString(); + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + if (device.getRegisterTime() == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("设备尚未注册过"); + return response; + } + WVPResult result = deviceService.devicesSync(device); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(JSONObject.toJSONString(result)); + return response; + } + + /** + * 获取通道同步状态 + */ + @RedisRpcMapping("getChannelSyncStatus") + public RedisRpcResponse getChannelSyncStatus(RedisRpcRequest request) { + String deviceId = request.getParam().toString(); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + SyncStatus channelSyncStatus = deviceService.getChannelSyncStatus(deviceId); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(JSONObject.toJSONString(channelSyncStatus)); + return response; + } + + @RedisRpcMapping("deviceBasicConfig") + public RedisRpcResponse deviceBasicConfig(RedisRpcRequest request) { + BasicParam basicParam = JSONObject.parseObject(request.getParam().toString(), BasicParam.class); + + Device device = deviceService.getDeviceByDeviceId(basicParam.getDeviceId()); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + deviceService.deviceBasicConfig(device, basicParam, (code, msg, data) -> { + response.setStatusCode(code); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + return null; + } + + @RedisRpcMapping("deviceConfigQuery") + public RedisRpcResponse deviceConfigQuery(RedisRpcRequest request) { + + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelId = paramJson.getString("channelId"); + String configType = paramJson.getString("configType"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + deviceService.deviceConfigQuery(device, channelId, configType, (code, msg, data) -> { + response.setStatusCode(code); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + return null; + } + + @RedisRpcMapping("teleboot") + public RedisRpcResponse teleboot(RedisRpcRequest request) { + String deviceId = request.getParam().toString(); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.teleboot(device); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + return response; + } + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(WVPResult.success()); + return response; + } + + @RedisRpcMapping("record") + public RedisRpcResponse record(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelId = paramJson.getString("channelId"); + String recordCmdStr = paramJson.getString("recordCmdStr"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.record(device, channelId, recordCmdStr, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("guard") + public RedisRpcResponse guard(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String guardCmdStr = paramJson.getString("guardCmdStr"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.guard(device, guardCmdStr, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("resetAlarm") + public RedisRpcResponse resetAlarm(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelId = paramJson.getString("channelId"); + String alarmMethod = paramJson.getString("alarmMethod"); + String alarmType = paramJson.getString("alarmType"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.resetAlarm(device, channelId, alarmMethod, alarmType, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("iFrame") + public RedisRpcResponse iFrame(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelId = paramJson.getString("channelId"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.iFrame(device, channelId); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("homePosition") + public RedisRpcResponse homePosition(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelId = paramJson.getString("channelId"); + + Boolean enabled = paramJson.getBoolean("enabled"); + Integer resetTime = paramJson.getInteger("resetTime"); + Integer presetIndex = paramJson.getInteger("presetIndex"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.homePosition(device, channelId, enabled, resetTime, presetIndex, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("dragZoomIn") + public RedisRpcResponse dragZoomIn(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelId = paramJson.getString("channelId"); + Integer length = paramJson.getInteger("length"); + Integer width = paramJson.getInteger("width"); + Integer midpointx = paramJson.getInteger("midpointx"); + Integer midpointy = paramJson.getInteger("midpointy"); + Integer lengthx = paramJson.getInteger("lengthx"); + Integer lengthy = paramJson.getInteger("lengthy"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.dragZoomIn(device, channelId, length, width, midpointx, midpointy, lengthx, lengthy, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("dragZoomOut") + public RedisRpcResponse dragZoomOut(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelId = paramJson.getString("channelId"); + Integer length = paramJson.getInteger("length"); + Integer width = paramJson.getInteger("width"); + Integer midpointx = paramJson.getInteger("midpointx"); + Integer midpointy = paramJson.getInteger("midpointy"); + Integer lengthx = paramJson.getInteger("lengthx"); + Integer lengthy = paramJson.getInteger("lengthy"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.dragZoomOut(device, channelId, length, width, midpointx, midpointy, lengthx, lengthy, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("alarm") + public RedisRpcResponse alarm(RedisRpcRequest request) { + + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String startPriority = paramJson.getString("startPriority"); + String endPriority = paramJson.getString("endPriority"); + String alarmMethod = paramJson.getString("alarmMethod"); + String alarmType = paramJson.getString("alarmType"); + String startTime = paramJson.getString("startTime"); + String endTime = paramJson.getString("endTime"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.alarm(device, startPriority, endPriority, alarmMethod, alarmType, startTime, endTime, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("deviceStatus") + public RedisRpcResponse deviceStatus(RedisRpcRequest request) { + String deviceId = request.getParam().toString(); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.deviceStatus(device, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("info") + public RedisRpcResponse info(RedisRpcRequest request) { + String deviceId = request.getParam().toString(); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.deviceInfo(device, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("info") + public RedisRpcResponse queryPreset(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelId = paramJson.getString("channelId"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.queryPreset(device, channelId, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcDevicePlayController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcDevicePlayController.java new file mode 100644 index 0000000..2e83cc7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcDevicePlayController.java @@ -0,0 +1,74 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RedisRpcController("devicePlay") +public class RedisRpcDevicePlayController extends RpcController { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IPlayService playService; + + + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message); + } + + /** + * 获取通道同步状态 + */ + @RedisRpcMapping("audioBroadcast") + public RedisRpcResponse audioBroadcast(RedisRpcRequest request) { + JSONObject paramJson = JSON.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelDeviceId = paramJson.getString("channelDeviceId"); + Boolean broadcastMode = paramJson.getBoolean("broadcastMode"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + AudioBroadcastResult audioBroadcastResult = playService.audioBroadcast(deviceId, channelDeviceId, broadcastMode); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(JSONObject.toJSONString(audioBroadcastResult)); + return response; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcGbDeviceController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcGbDeviceController.java new file mode 100644 index 0000000..798c938 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcGbDeviceController.java @@ -0,0 +1,99 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPTZService; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import javax.sip.message.Response; + +@Component +@Slf4j +@RedisRpcController("device") +public class RedisRpcGbDeviceController extends RpcController { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IDeviceService deviceService; + + + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message); + } + + + /** + * 目录订阅 + */ + @RedisRpcMapping("subscribeCatalog") + public RedisRpcResponse subscribeCatalog(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + int id = paramJson.getIntValue("id"); + int cycle = paramJson.getIntValue("cycle"); + + RedisRpcResponse response = request.getResponse(); + + if (id <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + deviceService.subscribeCatalog(id, cycle); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } + + /** + * 移动位置订阅 + */ + @RedisRpcMapping("subscribeMobilePosition") + public RedisRpcResponse subscribeMobilePosition(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + int id = paramJson.getIntValue("id"); + int cycle = paramJson.getIntValue("cycle"); + int interval = paramJson.getIntValue("interval"); + + RedisRpcResponse response = request.getResponse(); + + if (id <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + deviceService.subscribeMobilePosition(id, cycle, interval); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcPlatformController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcPlatformController.java new file mode 100644 index 0000000..a11561e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcPlatformController.java @@ -0,0 +1,83 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Slf4j +@RedisRpcController("platform") +public class RedisRpcPlatformController extends RpcController { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IPlatformService platformService; + + @Autowired + private IPlatformChannelService platformChannelService; + + @Autowired + private EventPublisher eventPublisher; + + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message); + } + + /** + * 更新 + */ + @RedisRpcMapping("update") + public RedisRpcResponse play(RedisRpcRequest request) { + Platform platform = JSONObject.parseObject(request.getParam().toString(), Platform.class); + RedisRpcResponse response = request.getResponse(); + boolean update = platformService.update(platform); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(Boolean.toString(update)); + return response; + } + + /** + * 目录更新推送 + */ + @RedisRpcMapping("catalogEventPublish") + public RedisRpcResponse catalogEventPublish(RedisRpcRequest request) { + JSONObject jsonObject = JSONObject.parseObject(request.getParam().toString()); + Platform platform = jsonObject.getObject("platform", Platform.class); + + List channels = jsonObject.getJSONArray("channels").toJavaList(CommonGBChannel.class); + String type = jsonObject.getString("type"); + eventPublisher.catalogEventPublish(platform, channels, type); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java new file mode 100644 index 0000000..d809d61 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java @@ -0,0 +1,163 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RedisRpcController("sendRtp") +public class RedisRpcSendRtpController extends RpcController { + + @Autowired + private SSRCFactory ssrcFactory; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private UserSetting userSetting; + + + /** + * 获取发流的信息 + */ + @RedisRpcMapping("getSendRtpItem") + public RedisRpcResponse getSendRtpItem(RedisRpcRequest request) { + String callId = request.getParam().toString(); + SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId); + if (sendRtpItem == null) { + log.info("[redis-rpc] 获取发流的信息, 未找到redis中的发流信息, callId:{}", callId); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } + log.info("[redis-rpc] 获取发流的信息: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort()); + // 查询本级是否有这个流 + MediaServer mediaServerItem = mediaServerService.getMediaServerByAppAndStream(sendRtpItem.getApp(), sendRtpItem.getStream()); + if (mediaServerItem == null) { + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + } + // 自平台内容 + int localPort = sendRtpServerService.getNextPort(mediaServerItem); + if (localPort <= 0) { + log.info("[redis-rpc] getSendRtpItem->服务器端口资源不足" ); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + } + // 写入redis, 超时时回复 + sendRtpItem.setStatus(1); + sendRtpItem.setServerId(userSetting.getServerId()); + sendRtpItem.setLocalIp(mediaServerItem.getSdpIp()); + if (sendRtpItem.getSsrc() == null) { + // 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式 + String ssrc = "Play".equalsIgnoreCase(sendRtpItem.getSessionName()) ? ssrcFactory.getPlaySsrc(mediaServerItem.getId()) : ssrcFactory.getPlayBackSsrc(mediaServerItem.getId()); + sendRtpItem.setSsrc(ssrc); + } + sendRtpServerService.update(sendRtpItem); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(callId); + return response; + } + + /** + * 开始发流 + */ + @RedisRpcMapping("startSendRtp") + public RedisRpcResponse startSendRtp(RedisRpcRequest request) { + String callId = request.getParam().toString(); + SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + if (sendRtpItem == null) { + log.info("[redis-rpc] 开始发流, 未找到redis中的发流信息, callId:{}", callId); + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到redis中的发流信息"); + response.setBody(wvpResult); + return response; + } + log.info("[redis-rpc] 开始发流: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort()); + MediaServer mediaServer = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + if (mediaServer == null) { + log.info("[redis-rpc] startSendRtp->未找到MediaServer: {}", sendRtpItem.getMediaServerId() ); + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到MediaServer"); + response.setBody(wvpResult); + return response; + } + MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, sendRtpItem.getApp(), sendRtpItem.getStream()); + if (mediaInfo == null) { + log.info("[redis-rpc] startSendRtp->流不在线: {}/{}", sendRtpItem.getApp(), sendRtpItem.getStream() ); + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "流不在线"); + response.setBody(wvpResult); + return response; + } + try { + mediaServerService.startSendRtp(mediaServer, sendRtpItem); + }catch (ControllerException exception) { + log.info("[redis-rpc] 发流失败: {}/{}, 目标地址: {}:{}, {}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort(), exception.getMsg()); + WVPResult wvpResult = WVPResult.fail(exception.getCode(), exception.getMsg()); + response.setBody(wvpResult); + return response; + } + log.info("[redis-rpc] 发流成功: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort()); + WVPResult wvpResult = WVPResult.success(); + response.setBody(wvpResult); + return response; + } + + /** + * 停止发流 + */ + @RedisRpcMapping("stopSendRtp") + public RedisRpcResponse stopSendRtp(RedisRpcRequest request) { + String callId = request.getParam().toString(); + SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + if (sendRtpItem == null) { + log.info("[redis-rpc] 停止推流, 未找到redis中的发流信息, key:{}", callId); + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到redis中的发流信息"); + response.setBody(wvpResult); + return response; + } + log.info("[redis-rpc] 停止推流: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort() ); + MediaServer mediaServer = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + if (mediaServer == null) { + log.info("[redis-rpc] stopSendRtp->未找到MediaServer: {}", sendRtpItem.getMediaServerId() ); + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到MediaServer"); + response.setBody(wvpResult); + return response; + } + try { + mediaServerService.stopSendRtp(mediaServer, sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getSsrc()); + }catch (ControllerException exception) { + log.info("[redis-rpc] 停止推流失败: {}/{}, 目标地址: {}:{}, code: {}, msg: {}", sendRtpItem.getApp(), + sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort(), exception.getCode(), exception.getMsg() ); + response.setBody(WVPResult.fail(exception.getCode(), exception.getMsg())); + return response; + } + log.info("[redis-rpc] 停止推流成功: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort() ); + response.setBody(WVPResult.success()); + return response; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamProxyController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamProxyController.java new file mode 100644 index 0000000..4a688c3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamProxyController.java @@ -0,0 +1,97 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyPlayService; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RedisRpcController("streamProxy") +public class RedisRpcStreamProxyController extends RpcController { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IStreamProxyPlayService streamProxyPlayService; + + @Autowired + private IStreamProxyService streamProxyService; + + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message); + } + + /** + * 播放 + */ + @RedisRpcMapping("play") + public RedisRpcResponse play(RedisRpcRequest request) { + int id = Integer.parseInt(request.getParam().toString()); + RedisRpcResponse response = request.getResponse(); + if (id <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + StreamProxy streamProxy = streamProxyService.getStreamProxy(id); + if (streamProxy == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + streamProxyPlayService.startProxy(streamProxy, (code, msg, streamInfo) -> { + response.setStatusCode(code); + response.setBody(JSONObject.toJSONString(streamInfo)); + sendResponse(response); + }); + + return null; + } + + /** + * 停止 + */ + @RedisRpcMapping("stop") + public RedisRpcResponse stop(RedisRpcRequest request) { + int id = Integer.parseInt(request.getParam().toString()); + RedisRpcResponse response = request.getResponse(); + if (id <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + StreamProxy streamProxy = streamProxyService.getStreamProxy(id); + if (streamProxy == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + streamProxyPlayService.stopProxy(streamProxy); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java new file mode 100644 index 0000000..babaa0f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java @@ -0,0 +1,208 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushPlayService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RedisRpcController("streamPush") +public class RedisRpcStreamPushController extends RpcController { + + @Autowired + private SSRCFactory ssrcFactory; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private HookSubscribe hookSubscribe; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IStreamPushPlayService streamPushPlayService; + + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message); + } + + /** + * 监听流上线 + */ + @RedisRpcMapping("waitePushStreamOnline") + public RedisRpcResponse waitePushStreamOnline(RedisRpcRequest request) { + SendRtpInfo sendRtpItem = JSONObject.parseObject(request.getParam().toString(), SendRtpInfo.class); + log.info("[redis-rpc] 监听流上线: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort()); + // 查询本级是否有这个流 + MediaServer mediaServer = mediaServerService.getMediaServerByAppAndStream(sendRtpItem.getApp(), sendRtpItem.getStream()); + if (mediaServer != null) { + log.info("[redis-rpc] 监听流上线时发现流已存在直接返回: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort() ); + // 读取redis中的上级点播信息,生成sendRtpItm发送出去 + if (sendRtpItem.getSsrc() == null) { + // 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式 + String ssrc = "Play".equalsIgnoreCase(sendRtpItem.getSessionName()) ? ssrcFactory.getPlaySsrc(mediaServer.getId()) : ssrcFactory.getPlayBackSsrc(mediaServer.getId()); + sendRtpItem.setSsrc(ssrc); + } + sendRtpItem.setMediaServerId(mediaServer.getId()); + sendRtpItem.setLocalIp(mediaServer.getSdpIp()); + sendRtpItem.setServerId(userSetting.getServerId()); + + sendRtpServerService.update(sendRtpItem); + RedisRpcResponse response = request.getResponse(); + response.setBody(sendRtpItem.getChannelId()); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + } + // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者 + Hook hook = Hook.getInstance(HookType.on_media_arrival, sendRtpItem.getApp(), sendRtpItem.getStream(), null); + hookSubscribe.addSubscribe(hook, (hookData) -> { + log.info("[redis-rpc] 监听流上线,流已上线: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort()); + // 读取redis中的上级点播信息,生成sendRtpItm发送出去 + if (sendRtpItem.getSsrc() == null) { + // 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式 + String ssrc = "Play".equalsIgnoreCase(sendRtpItem.getSessionName()) ? ssrcFactory.getPlaySsrc(hookData.getMediaServer().getId()) : ssrcFactory.getPlayBackSsrc(hookData.getMediaServer().getId()); + sendRtpItem.setSsrc(ssrc); + } + sendRtpItem.setMediaServerId(hookData.getMediaServer().getId()); + sendRtpItem.setLocalIp(hookData.getMediaServer().getSdpIp()); + sendRtpItem.setServerId(userSetting.getServerId()); + + redisTemplate.opsForValue().set(sendRtpItem.getChannelId(), sendRtpItem); + RedisRpcResponse response = request.getResponse(); + response.setBody(sendRtpItem.getChannelId()); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + // 手动发送结果 + sendResponse(response); + hookSubscribe.removeSubscribe(hook); + + }); + return null; + } + + /** + * 监听流上线 + */ + @RedisRpcMapping("onStreamOnlineEvent") + public RedisRpcResponse onStreamOnlineEvent(RedisRpcRequest request) { + StreamInfo streamInfo = JSONObject.parseObject(request.getParam().toString(), StreamInfo.class); + log.info("[redis-rpc] 监听流信息,等待流上线: {}/{}", streamInfo.getApp(), streamInfo.getStream()); + // 查询本级是否有这个流 + StreamInfo streamInfoInServer = mediaServerService.getMediaByAppAndStream(streamInfo.getApp(), streamInfo.getStream()); + if (streamInfoInServer != null) { + log.info("[redis-rpc] 监听流上线时发现流已存在直接返回: {}/{}", streamInfo.getApp(), streamInfo.getStream()); + RedisRpcResponse response = request.getResponse(); + response.setBody(JSONObject.toJSONString(streamInfoInServer)); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } + // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者 + Hook hook = Hook.getInstance(HookType.on_media_arrival, streamInfo.getApp(), streamInfo.getStream()); + hookSubscribe.addSubscribe(hook, (hookData) -> { + log.info("[redis-rpc] 监听流上线,流已上线: {}/{}", streamInfo.getApp(), streamInfo.getStream()); + // 读取redis中的上级点播信息,生成sendRtpItm发送出去 + RedisRpcResponse response = request.getResponse(); + StreamInfo streamInfoByAppAndStream = mediaServerService.getStreamInfoByAppAndStream(hookData.getMediaServer(), + streamInfo.getApp(), streamInfo.getStream(), hookData.getMediaInfo(), + hookData.getMediaInfo() != null ? hookData.getMediaInfo().getCallId() : null); + response.setBody(JSONObject.toJSONString(streamInfoByAppAndStream)); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + // 手动发送结果 + sendResponse(response); + hookSubscribe.removeSubscribe(hook); + }); + return null; + } + + /** + * 停止监听流上线 + */ + @RedisRpcMapping("stopWaitePushStreamOnline") + public RedisRpcResponse stopWaitePushStreamOnline(RedisRpcRequest request) { + SendRtpInfo sendRtpItem = JSONObject.parseObject(request.getParam().toString(), SendRtpInfo.class); + log.info("[redis-rpc] 停止监听流上线: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort() ); + // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者 + Hook hook = Hook.getInstance(HookType.on_media_arrival, sendRtpItem.getApp(), sendRtpItem.getStream(), null); + hookSubscribe.removeSubscribe(hook); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } + + /** + * 停止监听流上线 + */ + @RedisRpcMapping("unPushStreamOnlineEvent") + public RedisRpcResponse unPushStreamOnlineEvent(RedisRpcRequest request) { + StreamInfo streamInfo = JSONObject.parseObject(request.getParam().toString(), StreamInfo.class); + log.info("[redis-rpc] 停止监听流上线: {}/{}", streamInfo.getApp(), streamInfo.getStream()); + // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者 + Hook hook = Hook.getInstance(HookType.on_media_arrival, streamInfo.getApp(), streamInfo.getStream(), null); + hookSubscribe.removeSubscribe(hook); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } + + /** + * 停止监听流上线 + */ + @RedisRpcMapping("play") + public RedisRpcResponse play(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + int id = paramJson.getInteger("id"); + RedisRpcResponse response = request.getResponse(); + if (id <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + streamPushPlayService.start(id, (code, msg, data) -> { + if (code == ErrorCode.SUCCESS.getCode()) { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(JSONObject.toJSONString(data)); + sendResponse(response); + } + }, null, null); + }catch (IllegalArgumentException e) { + response.setStatusCode(ErrorCode.ERROR100.getCode()); + response.setBody(e.getMessage()); + sendResponse(response); + } + return null; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcController.java new file mode 100644 index 0000000..f314b0c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcController.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.service.redisMsg.dto; + +import java.lang.annotation.*; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RedisRpcController { + /** + * 请求路径 + */ + String value() default ""; +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcMapping.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcMapping.java new file mode 100644 index 0000000..61f51bb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcMapping.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.service.redisMsg.dto; + +import java.lang.annotation.*; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RedisRpcMapping { + /** + * 请求路径 + */ + String value() default ""; +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RpcController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RpcController.java new file mode 100644 index 0000000..30f8b88 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RpcController.java @@ -0,0 +1,33 @@ +package com.genersoft.iot.vmp.service.redisMsg.dto; + + +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcClassHandler; +import org.springframework.beans.factory.annotation.Autowired; + +import jakarta.annotation.PostConstruct; +import java.lang.reflect.Method; + +public class RpcController { + + @Autowired + private RedisRpcConfig redisRpcConfig; + + + @PostConstruct + public void init() { + String controllerPath = this.getClass().getAnnotation(RedisRpcController.class).value(); + // 扫描其下的方法 + Method[] methods = this.getClass().getDeclaredMethods(); + for (Method method : methods) { + RedisRpcMapping annotation = method.getAnnotation(RedisRpcMapping.class); + if (annotation != null) { + String methodPath = annotation.value(); + if (methodPath != null) { + redisRpcConfig.addHandler(controllerPath + "/" + methodPath, new RedisRpcClassHandler(this, method)); + } + } + + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java new file mode 100644 index 0000000..0e2e2e8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java @@ -0,0 +1,267 @@ +package com.genersoft.iot.vmp.service.redisMsg.service; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.RecordInfo; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService; +import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService { + + + @Autowired + private RedisRpcConfig redisRpcConfig; + + @Autowired + private UserSetting userSetting; + + + private RedisRpcRequest buildRequest(String uri, Object param) { + RedisRpcRequest request = new RedisRpcRequest(); + request.setFromId(userSetting.getServerId()); + request.setParam(param); + request.setUri(uri); + return request; + } + + @Override + public void play(String serverId, Integer channelId, ErrorCallback callback) { + RedisRpcRequest request = buildRequest("channel/play", channelId); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.MILLISECONDS); + if (response == null) { + callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null); + }else { + if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class); + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + }else { + callback.run(response.getStatusCode(), response.getBody().toString(), null); + } + } + } + + @Override + public void stop(String serverId, InviteSessionType type, int channelId, String stream) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("channelId", channelId); + jsonObject.put("stream", stream); + jsonObject.put("inviteSessionType", type); + RedisRpcRequest request = buildRequest("channel/stop", jsonObject.toJSONString()); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MICROSECONDS); + if (response == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg()); + }else { + if (response.getStatusCode() != ErrorCode.SUCCESS.getCode()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg()); + } + } + } + + @Override + public void queryRecordInfo(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback callback) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("channelId", channelId); + jsonObject.put("startTime", startTime); + jsonObject.put("endTime", endTime); + RedisRpcRequest request = buildRequest("channel/queryRecordInfo", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getRecordInfoTimeout(), TimeUnit.MILLISECONDS); + if (response == null) { + callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null); + }else { + if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + RecordInfo recordInfo = JSON.parseObject(response.getBody().toString(), RecordInfo.class); + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), recordInfo); + }else { + callback.run(response.getStatusCode(), response.getBody().toString(), null); + } + } + } + + @Override + public void playback(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback callback) { + + JSONObject jsonObject = new JSONObject(); + jsonObject.put("channelId", channelId); + jsonObject.put("startTime", startTime); + jsonObject.put("endTime", endTime); + RedisRpcRequest request = buildRequest("channel/playback", jsonObject.toString()); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.MILLISECONDS); + if (response == null) { + callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null); + }else { + if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class); + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + }else { + callback.run(response.getStatusCode(), response.getBody().toString(), null); + } + } + } + + @Override + public void playbackPause(String serverId, String streamId) { + RedisRpcRequest request = buildRequest("channel/playbackPause", streamId); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 5, TimeUnit.SECONDS); + if (response == null) { + log.info("[RPC 暂停回放] 失败, streamId: {}", streamId); + }else { + if (response.getStatusCode() != ErrorCode.SUCCESS.getCode()) { + log.info("[RPC 暂停回放] 失败, {}, streamId: {}", response.getBody(), streamId); + } + } + } + + @Override + public void playbackResume(String serverId, String streamId) { + RedisRpcRequest request = buildRequest("channel/playbackResume", streamId); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 5, TimeUnit.SECONDS); + if (response == null) { + log.info("[RPC 恢复回放] 失败, streamId: {}", streamId); + }else { + if (response.getStatusCode() != ErrorCode.SUCCESS.getCode()) { + log.info("[RPC 恢复回放] 失败, {}, streamId: {}", response.getBody(), streamId); + } + } + } + + @Override + public void download(String serverId, Integer channelId, String startTime, String endTime, int downloadSpeed, ErrorCallback callback) { + + JSONObject jsonObject = new JSONObject(); + jsonObject.put("channelId", channelId); + jsonObject.put("startTime", startTime); + jsonObject.put("endTime", endTime); + jsonObject.put("downloadSpeed", downloadSpeed); + RedisRpcRequest request = buildRequest("channel/download", jsonObject.toString()); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.MILLISECONDS); + if (response == null) { + callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null); + }else { + if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class); + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + }else { + callback.run(response.getStatusCode(), response.getBody().toString(), null); + } + } + } + + @Override + public String frontEndCommand(String serverId, Integer channelId, int cmdCode, int parameter1, int parameter2, int combindCode2) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("channelId", channelId); + jsonObject.put("cmdCode", cmdCode); + jsonObject.put("parameter1", parameter1); + jsonObject.put("parameter2", parameter2); + jsonObject.put("combindCode2", combindCode2); + RedisRpcRequest request = buildRequest("channel/ptz/frontEndCommand", jsonObject.toString()); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.MILLISECONDS); + if (response == null) { + return ErrorCode.ERROR100.getMsg(); + }else { + if (response.getStatusCode() != ErrorCode.SUCCESS.getCode()) { + return response.getBody().toString(); + } + } + return null; + } + + @Override + public void playPush(String serverId, Integer id, ErrorCallback callback) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("id", id); + RedisRpcRequest request = buildRequest("streamPush/play", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.SECONDS); + if (response == null) { + callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null); + }else { + if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class); + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + }else { + callback.run(response.getStatusCode(), response.getBody().toString(), null); + } + } + } + + @Override + public void playProxy(String serverId, int id, ErrorCallback callback) { + RedisRpcRequest request = buildRequest("streamProxy/play", id); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.SECONDS); + if (response == null) { + callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null); + }else { + if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class); + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + }else { + callback.run(response.getStatusCode(), response.getBody().toString(), null); + } + } + } + + @Override + public void stopProxy(String serverId, int id) { + RedisRpcRequest request = buildRequest("streamProxy/stop", id); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.SECONDS); + if (response != null && response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + log.info("[rpc 拉流代理] 停止成功: id: {}", id); + }else { + log.info("[rpc 拉流代理] 停止失败 id: {}", id); + } + } + + @Override + public DownloadFileInfo getRecordPlayUrl(String serverId, Integer recordId) { + RedisRpcRequest request = buildRequest("cloudRecord/play", recordId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.SECONDS); + if (response != null && response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + return JSON.parseObject(response.getBody().toString(), DownloadFileInfo.class); + } + return null; + } + + @Override + public AudioBroadcastResult audioBroadcast(String serverId, String deviceId, String channelDeviceId, Boolean broadcastMode) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("deviceId", deviceId); + jsonObject.put("channelDeviceId", channelDeviceId); + jsonObject.put("broadcastMode", broadcastMode); + RedisRpcRequest request = buildRequest("devicePlay/audioBroadcast", jsonObject.toString()); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.SECONDS); + if (response != null && response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + return JSON.parseObject(response.getBody().toString(), AudioBroadcastResult.class); + } + return null; + } +} + diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcServiceImpl.java new file mode 100644 index 0000000..5c82c27 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcServiceImpl.java @@ -0,0 +1,437 @@ +package com.genersoft.iot.vmp.service.redisMsg.service; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class RedisRpcServiceImpl implements IRedisRpcService { + + + @Autowired + private RedisRpcConfig redisRpcConfig; + + @Autowired + private UserSetting userSetting; + + @Autowired + private HookSubscribe hookSubscribe; + + @Autowired + private SSRCFactory ssrcFactory; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + private RedisRpcRequest buildRequest(String uri, Object param) { + RedisRpcRequest request = new RedisRpcRequest(); + request.setFromId(userSetting.getServerId()); + request.setParam(param); + request.setUri(uri); + return request; + } + + @Override + public SendRtpInfo getSendRtpItem(String callId) { + RedisRpcRequest request = buildRequest("sendRtp/getSendRtpItem", callId); + RedisRpcResponse response = redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + if (response.getBody() == null) { + return null; + } + return (SendRtpInfo)redisTemplate.opsForValue().get(response.getBody().toString()); + } + + @Override + public WVPResult startSendRtp(String callId, SendRtpInfo sendRtpItem) { + log.info("[请求其他WVP] 开始推流,wvp:{}, {}/{}", sendRtpItem.getServerId(), sendRtpItem.getApp(), sendRtpItem.getStream()); + RedisRpcRequest request = buildRequest("sendRtp/startSendRtp", callId); + request.setToId(sendRtpItem.getServerId()); + RedisRpcResponse response = redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public WVPResult stopSendRtp(String callId) { + SendRtpInfo sendRtpItem = (SendRtpInfo)redisTemplate.opsForValue().get(callId); + if (sendRtpItem == null) { + log.info("[请求其他WVP] 停止推流, 未找到redis中的发流信息, key:{}", callId); + return WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到发流信息"); + } + log.info("[请求其他WVP] 停止推流,wvp:{}, {}/{}", sendRtpItem.getServerId(), sendRtpItem.getApp(), sendRtpItem.getStream()); + RedisRpcRequest request = buildRequest("sendRtp/stopSendRtp", callId); + request.setToId(sendRtpItem.getServerId()); + RedisRpcResponse response = redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public long waitePushStreamOnline(SendRtpInfo sendRtpItem, CommonCallback callback) { + log.info("[请求所有WVP监听流上线] {}/{}", sendRtpItem.getApp(), sendRtpItem.getStream()); + // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者 + Hook hook = Hook.getInstance(HookType.on_media_arrival, sendRtpItem.getApp(), sendRtpItem.getStream(), null); + RedisRpcRequest request = buildRequest("streamPush/waitePushStreamOnline", sendRtpItem); + request.setToId(sendRtpItem.getServerId()); + hookSubscribe.addSubscribe(hook, (hookData) -> { + + // 读取redis中的上级点播信息,生成sendRtpItm发送出去 + if (sendRtpItem.getSsrc() == null) { + // 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式 + String ssrc = "Play".equalsIgnoreCase(sendRtpItem.getSessionName()) ? ssrcFactory.getPlaySsrc(hookData.getMediaServer().getId()) : ssrcFactory.getPlayBackSsrc(hookData.getMediaServer().getId()); + sendRtpItem.setSsrc(ssrc); + } + sendRtpItem.setMediaServerId(hookData.getMediaServer().getId()); + sendRtpItem.setLocalIp(hookData.getMediaServer().getSdpIp()); + sendRtpItem.setServerId(userSetting.getServerId()); + sendRtpServerService.update(sendRtpItem); + if (callback != null) { + callback.run(sendRtpItem.getChannelId()); + } + hookSubscribe.removeSubscribe(hook); + redisRpcConfig.removeCallback(request.getSn()); + }); + + redisRpcConfig.request(request, response -> { + if (response.getBody() == null) { + log.info("[请求所有WVP监听流上线] 流上线,但是未找到发流信息:{}/{}", sendRtpItem.getApp(), sendRtpItem.getStream()); + return; + } + log.info("[请求所有WVP监听流上线] 流上线 {}/{}->{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.toString()); + + if (callback != null) { + callback.run(Integer.parseInt(response.getBody().toString())); + } + hookSubscribe.removeSubscribe(hook); + }); + return request.getSn(); + } + + @Override + public void stopWaitePushStreamOnline(SendRtpInfo sendRtpItem) { + log.info("[停止WVP监听流上线] {}/{}", sendRtpItem.getApp(), sendRtpItem.getStream()); + Hook hook = Hook.getInstance(HookType.on_media_arrival, sendRtpItem.getApp(), sendRtpItem.getStream(), null); + hookSubscribe.removeSubscribe(hook); + RedisRpcRequest request = buildRequest("streamPush/stopWaitePushStreamOnline", sendRtpItem); + request.setToId(sendRtpItem.getServerId()); + redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + } + + @Override + public void rtpSendStopped(String callId) { + SendRtpInfo sendRtpItem = (SendRtpInfo)redisTemplate.opsForValue().get(callId); + if (sendRtpItem == null) { + log.info("[停止WVP监听流上线] 未找到redis中的发流信息, key:{}", callId); + return; + } + RedisRpcRequest request = buildRequest("streamPush/rtpSendStopped", callId); + request.setToId(sendRtpItem.getServerId()); + redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + } + + @Override + public void removeCallback(long key) { + redisRpcConfig.removeCallback(key); + } + + @Override + public long onStreamOnlineEvent(String app, String stream, CommonCallback callback) { + + log.info("[请求所有WVP监听流上线] {}/{}", app, stream); + // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者 + Hook hook = Hook.getInstance(HookType.on_media_arrival, app, stream); + StreamInfo streamInfoParam = new StreamInfo(); + streamInfoParam.setApp(app); + streamInfoParam.setStream(stream); + RedisRpcRequest request = buildRequest("streamPush/onStreamOnlineEvent", streamInfoParam); + hookSubscribe.addSubscribe(hook, (hookData) -> { + log.info("[请求所有WVP监听流上线] 监听流上线 {}/{}", app, stream); + if (callback != null) { + callback.run(mediaServerService.getStreamInfoByAppAndStream(hookData.getMediaServer(), + app, stream, hookData.getMediaInfo(), + hookData.getMediaInfo() != null ? hookData.getMediaInfo().getCallId() : null)); + } + hookSubscribe.removeSubscribe(hook); + redisRpcConfig.removeCallback(request.getSn()); + }); + + redisRpcConfig.request(request, response -> { + if (response.getBody() == null) { + log.info("[请求所有WVP监听流上线] 流上线,但是未找到发流信息:{}/{}", app, stream); + return; + } + log.info("[请求所有WVP监听流上线] 流上线 {}/{}", app, stream); + + if (callback != null) { + callback.run(JSON.parseObject(response.getBody().toString(), StreamInfo.class)); + } + hookSubscribe.removeSubscribe(hook); + }); + return request.getSn(); + } + + @Override + public void unPushStreamOnlineEvent(String app, String stream) { + StreamInfo streamInfoParam = new StreamInfo(); + streamInfoParam.setApp(app); + streamInfoParam.setStream(stream); + RedisRpcRequest request = buildRequest("streamPush/unPushStreamOnlineEvent", streamInfoParam); + redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + } + + @Override + public void subscribeCatalog(int id, int cycle) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("id", id); + jsonObject.put("cycle", cycle); + RedisRpcRequest request = buildRequest("device/subscribeCatalog", jsonObject); + redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + } + + @Override + public void subscribeMobilePosition(int id, int cycle, int interval) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("id", id); + jsonObject.put("cycle", cycle); + jsonObject.put("interval", cycle); + RedisRpcRequest request = buildRequest("device/subscribeMobilePosition", jsonObject); + redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + } + + @Override + public boolean updatePlatform(String serverId, Platform platform) { + RedisRpcRequest request = buildRequest("platform/update", platform); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 40, TimeUnit.MILLISECONDS); + if(response == null) { + return false; + } + return Boolean.parseBoolean(response.getBody().toString()); + } + + @Override + public void catalogEventPublish(String serverId, CatalogEvent event) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("platform", event.getPlatform()); + jsonObject.put("channels", event.getChannels()); + jsonObject.put("type", event.getType()); + RedisRpcRequest request = buildRequest("platform/catalogEventPublish", jsonObject); + if (serverId != null) { + request.setToId(serverId); + } + redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + } + + @Override + public WVPResult devicesSync(String serverId, String deviceId) { + RedisRpcRequest request = buildRequest("device/devicesSync", deviceId); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 100, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public SyncStatus getChannelSyncStatus(String serverId, String deviceId) { + RedisRpcRequest request = buildRequest("device/getChannelSyncStatus", deviceId); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 100, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), SyncStatus.class); + } + + @Override + public WVPResult deviceBasicConfig(String serverId, Device device, BasicParam basicParam) { + RedisRpcRequest request = buildRequest("device/deviceBasicConfig", JSONObject.toJSONString(basicParam)); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public WVPResult deviceConfigQuery(String serverId, Device device, String channelId, String configType) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("channelId", channelId); + jsonObject.put("configType", configType); + RedisRpcRequest request = buildRequest("device/deviceConfigQuery", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public void teleboot(String serverId, Device device) { + RedisRpcRequest request = buildRequest("device/teleboot", device.getDeviceId()); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + if (response.getStatusCode() != ErrorCode.SUCCESS.getCode()) { + throw new ControllerException(response.getStatusCode(), response.getBody().toString()); + } + } + + @Override + public WVPResult recordControl(String serverId, Device device, String channelId, String recordCmdStr) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("channelId", channelId); + jsonObject.put("recordCmdStr", recordCmdStr); + RedisRpcRequest request = buildRequest("device/record", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public WVPResult guard(String serverId, Device device, String guardCmdStr) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("guardCmdStr", guardCmdStr); + RedisRpcRequest request = buildRequest("device/guard", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public WVPResult resetAlarm(String serverId, Device device, String channelId, String alarmMethod, String alarmType) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("channelId", channelId); + jsonObject.put("alarmMethod", alarmMethod); + jsonObject.put("alarmType", alarmType); + RedisRpcRequest request = buildRequest("device/resetAlarm", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public void iFrame(String serverId, Device device, String channelId) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("channelId", channelId); + RedisRpcRequest request = buildRequest("device/iFrame", jsonObject); + request.setToId(serverId); + redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + } + + @Override + public WVPResult homePosition(String serverId, Device device, String channelId, Boolean enabled, Integer resetTime, Integer presetIndex) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("channelId", channelId); + jsonObject.put("enabled", enabled); + jsonObject.put("resetTime", resetTime); + jsonObject.put("presetIndex", presetIndex); + RedisRpcRequest request = buildRequest("device/homePosition", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public void dragZoomIn(String serverId, Device device, String channelId, int length, int width, int midpointx, + int midpointy, int lengthx, int lengthy) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("channelId", channelId); + jsonObject.put("length", length); + jsonObject.put("width", width); + jsonObject.put("midpointx", midpointx); + jsonObject.put("midpointy", midpointy); + jsonObject.put("lengthx", lengthx); + jsonObject.put("lengthy", lengthy); + RedisRpcRequest request = buildRequest("device/dragZoomIn", jsonObject); + request.setToId(serverId); + redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + } + + @Override + public void dragZoomOut(String serverId, Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("channelId", channelId); + jsonObject.put("length", length); + jsonObject.put("width", width); + jsonObject.put("midpointx", midpointx); + jsonObject.put("midpointy", midpointy); + jsonObject.put("lengthx", lengthx); + jsonObject.put("lengthy", lengthy); + RedisRpcRequest request = buildRequest("device/dragZoomOut", jsonObject); + request.setToId(serverId); + redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + } + + @Override + public WVPResult deviceStatus(String serverId, Device device) { + RedisRpcRequest request = buildRequest("device/deviceStatus", device.getDeviceId()); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public WVPResult deviceInfo(String serverId, Device device) { + RedisRpcRequest request = buildRequest("device/info", device.getDeviceId()); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public WVPResult> queryPreset(String serverId, Device device, String channelId) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("channelId", channelId); + RedisRpcRequest request = buildRequest("device/queryPreset", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 60000, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public WVPResult alarm(String serverId, Device device, String startPriority, String endPriority, + String alarmMethod, String alarmType, String startTime, String endTime) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); +// jsonObject.put("channelId", channelId); + jsonObject.put("startPriority", startPriority); + jsonObject.put("endPriority", endPriority); + jsonObject.put("alarmMethod", alarmMethod); + jsonObject.put("alarmType", alarmType); + jsonObject.put("startTime", startTime); + jsonObject.put("endTime", endTime); + RedisRpcRequest request = buildRequest("device/alarm", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java b/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java new file mode 100755 index 0000000..e7bdffa --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java @@ -0,0 +1,182 @@ +package com.genersoft.iot.vmp.storager; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.ServerInfo; +import com.genersoft.iot.vmp.common.SystemAllInfo; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import com.genersoft.iot.vmp.service.bean.MessageForPushChannel; + +import java.util.List; +import java.util.Map; + +public interface IRedisCatchStorage { + + /** + * 计数器。为cseq进行计数 + * + * @return + */ + Long getCSEQ(); + + /** + * 在redis添加wvp的信息 + */ + void updateWVPInfo(ServerInfo serverInfo, int time); + + void removeOfflineWVPInfo(String serverId); + + /** + * 发送推流生成与推流消失消息 + * @param jsonObject 消息内容 + */ + void sendStreamChangeMsg(String type, JSONObject jsonObject); + + /** + * 发送报警消息 + * @param msg 消息内容 + */ + void sendAlarmMsg(AlarmChannelMessage msg); + + /** + * 添加流信息到redis + * @param mediaServerItem + * @param app + * @param streamId + */ + void addStream(MediaServer mediaServerItem, String type, String app, String streamId, MediaInfo item); + + /** + * 移除流信息从redis + * @param mediaServerId + * @param app + * @param streamId + */ + void removeStream(String mediaServerId, String type, String app, String streamId); + + + /** + * 移除流信息从redis + * @param mediaServerId + */ + void removeStream(String mediaServerId, String type); + + List getStreams(String mediaServerId, String pull); + + /** + * 将device信息写入redis + * @param device + */ + void updateDevice(Device device); + + void removeDevice(String deviceId); + + /** + * 获取Device + */ + Device getDevice(String deviceId); + + void resetAllCSEQ(); + + void updateGpsMsgInfo(GPSMsgInfo gpsMsgInfo); + + GPSMsgInfo getGpsMsgInfo(String gbId); + + List getAllGpsMsgInfo(); + + MediaInfo getStreamInfo(String app, String streamId, String mediaServerId); + + MediaInfo getProxyStream(String app, String streamId); + + void addCpuInfo(double cpuInfo); + + void addMemInfo(double memInfo); + + void addNetInfo(Map networkInterfaces); + + void sendStreamPushRequestedMsg(MessageForPushChannel messageForPushChannel); + + /** + * 判断设备状态 + */ + boolean deviceIsOnline(String deviceId); + + /** + * 存储推流的鉴权信息 + * @param app 应用名 + * @param stream 流 + * @param streamAuthorityInfo 鉴权信息 + */ + void updateStreamAuthorityInfo(String app, String stream, StreamAuthorityInfo streamAuthorityInfo); + + /** + * 移除推流的鉴权信息 + * @param app 应用名 + * @param streamId 流 + */ + void removeStreamAuthorityInfo(String app, String streamId); + + /** + * 获取推流的鉴权信息 + * @param app 应用名 + * @param stream 流 + * @return + */ + StreamAuthorityInfo getStreamAuthorityInfo(String app, String stream); + + List getAllStreamAuthorityInfo(); + + /** + * 发送redis消息 查询所有推流设备的状态 + */ + void sendStreamPushRequestedMsgForStatus(); + + SystemAllInfo getSystemInfo(); + + int getPushStreamCount(String id); + + int getProxyStreamCount(String id); + + int getGbSendCount(String id); + + void addDiskInfo(List> diskInfo); + + List queryAllSendRTPServer(); + + List getAllDevices(); + + void removeAllDevice(); + + void sendDeviceOrChannelStatus(String deviceId, String channelId, boolean online); + + void sendChannelAddOrDelete(String deviceId, String channelId, boolean add); + + void sendPlatformStartPlayMsg(SendRtpInfo sendRtpItem, DeviceChannel channel, Platform platform); + + void sendPlatformStopPlayMsg(SendRtpInfo sendRtpItem, Platform platform, CommonGBChannel channel); + + void addPushListItem(String app, String stream, MediaInfo param); + + MediaInfo getPushListItem(String app, String stream); + + void removePushListItem(String app, String stream, String mediaServerId); + + void sendPushStreamClose(MessageForPushChannel messageForPushChannel); + + void addWaiteSendRtpItem(SendRtpInfo sendRtpItem, int platformPlayTimeout); + + SendRtpInfo getWaiteSendRtpItem(String app, String stream); + + void sendStartSendRtp(SendRtpInfo sendRtpItem); + + void sendPushStreamOnline(SendRtpInfo sendRtpItem); + + ServerInfo queryServerInfo(String serverId); + + String chooseOneServer(String serverId); + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/CloudRecordServiceMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/CloudRecordServiceMapper.java new file mode 100644 index 0000000..7789200 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/CloudRecordServiceMapper.java @@ -0,0 +1,172 @@ +package com.genersoft.iot.vmp.storager.dao; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.CloudRecordItem; +import org.apache.ibatis.annotations.*; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface CloudRecordServiceMapper { + + @Insert(" ") + int add(CloudRecordItem cloudRecordItem); + + @Select(" ") + List getList(@Param("query") String query, @Param("app") String app, @Param("stream") String stream, + @Param("startTimeStamp")Long startTimeStamp, @Param("endTimeStamp")Long endTimeStamp, + @Param("callId")String callId, List mediaServerItemList, + List ids, @Param("ascOrder") Boolean ascOrder); + + + @Select(" ") + List queryRecordFilePathList(@Param("app") String app, @Param("stream") String stream, + @Param("startTimeStamp")Long startTimeStamp, @Param("endTimeStamp")Long endTimeStamp, + @Param("callId")String callId, List mediaServerItemList); + + @Update(" ") + int updateCollectList(@Param("collect") boolean collect, List cloudRecordItemList); + + @Delete(" ") + void deleteByFileList(List filePathList, @Param("mediaServerId") String mediaServerId); + + + @Select(" ") + List queryRecordListForDelete(@Param("endTimeStamp")Long endTimeStamp, String mediaServerId); + + @Update(" ") + int changeCollectById(@Param("collect") boolean collect, @Param("recordId") Integer recordId); + + @Delete(" ") + int deleteList(List cloudRecordItemIdList); + + @Select(" ") + List getListByCallId(@Param("callId") String callId); + + @Select(" ") + CloudRecordItem queryOne(@Param("id") Integer id); + + @Select(" ") + CloudRecordItem getListByFileName(@Param("app") String app, @Param("stream") String stream, @Param("fileName") String fileName); + + @Update(" ") + void updateTimeLen(@Param("id") int id, @Param("time") Long time, @Param("endTime") long endTime); + + @Select(" ") + List queryMediaServerId(@Param("app") String app, + @Param("stream") String stream, + @Param("startTimeStamp")Long startTimeStamp, + @Param("endTimeStamp")Long endTimeStamp); + + @Select(" ") + List queryRecordByIds(Collection ids); + + @Select(" ") + List queryRecordByAppStreamAndCallId(String app, String stream, String callId); +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java new file mode 100755 index 0000000..f892be2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java @@ -0,0 +1,174 @@ +package com.genersoft.iot.vmp.storager.dao; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import org.apache.ibatis.annotations.*; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + + +@Mapper +@Repository +public interface MediaServerMapper { + + @Insert("INSERT INTO wvp_media_server (" + + "id,"+ + "ip,"+ + "hook_ip,"+ + "sdp_ip,"+ + "stream_ip,"+ + "http_port,"+ + "http_ssl_port,"+ + "rtmp_port,"+ + "rtmp_ssl_port,"+ + "rtp_proxy_port,"+ + "jtt_proxy_port,"+ + "rtsp_port,"+ + "flv_port," + + "mp4_port," + + "flv_ssl_port," + + "ws_flv_port," + + "ws_flv_ssl_port," + + "rtsp_ssl_port,"+ + "auto_config,"+ + "secret,"+ + "rtp_enable,"+ + "rtp_port_range,"+ + "send_rtp_port_range,"+ + "record_assist_port,"+ + "record_day,"+ + "record_path,"+ + "default_server,"+ + "type,"+ + "create_time,"+ + "update_time,"+ + "transcode_suffix,"+ + "server_id,"+ + "hook_alive_interval"+ + ") VALUES " + + "(" + + "#{id}, " + + "#{ip}, " + + "#{hookIp}, " + + "#{sdpIp}, " + + "#{streamIp}, " + + "#{httpPort}, " + + "#{httpSSlPort}, " + + "#{rtmpPort}, " + + "#{rtmpSSlPort}, " + + "#{rtpProxyPort}, " + + "#{jttProxyPort}, " + + "#{rtspPort}, " + + "#{flvPort}, " + + "#{mp4Port}, " + + "#{flvSSLPort}, " + + "#{wsFlvPort}, " + + "#{wsFlvSSLPort}, " + + "#{rtspSSLPort}, " + + "#{autoConfig}, " + + "#{secret}, " + + "#{rtpEnable}, " + + "#{rtpPortRange}, " + + "#{sendRtpPortRange}, " + + "#{recordAssistPort}, " + + "#{recordDay}, " + + "#{recordPath}, " + + "#{defaultServer}, " + + "#{type}, " + + "#{createTime}, " + + "#{updateTime}, " + + "#{transcodeSuffix}, " + + "#{serverId}, " + + "#{hookAliveInterval})") + int add(MediaServer mediaServerItem); + + @Update(value = {" "}) + int update(MediaServer mediaServerItem); + + @Update(value = {" "}) + int updateByHostAndPort(MediaServer mediaServerItem); + + @Select("SELECT * FROM wvp_media_server WHERE id=#{id} and server_id = #{serverId}") + MediaServer queryOne(@Param("id") String id, @Param("serverId") String serverId); + + @Select("SELECT * FROM wvp_media_server where server_id = #{serverId}") + List queryAll(@Param("serverId") String serverId); + + @Delete("DELETE FROM wvp_media_server WHERE id=#{id} and server_id = #{serverId}") + void delOne(String id, @Param("serverId") String serverId); + + @Select("SELECT * FROM wvp_media_server WHERE ip=#{host} and http_port=#{port} and server_id = #{serverId}") + MediaServer queryOneByHostAndPort(@Param("host") String host, @Param("port") int port, @Param("serverId") String serverId); + + @Select("SELECT * FROM wvp_media_server WHERE default_server=true and server_id = #{serverId}") + MediaServer queryDefault(@Param("serverId") String serverId); + + @Select("SELECT * FROM wvp_media_server WHERE record_assist_port > 0 and server_id = #{serverId}") + List queryAllWithAssistPort(@Param("serverId") String serverId); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java new file mode 100644 index 0000000..ae0649a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java @@ -0,0 +1,67 @@ +package com.genersoft.iot.vmp.storager.dao; + +import com.genersoft.iot.vmp.service.bean.RecordPlan; +import com.genersoft.iot.vmp.service.bean.RecordPlanItem; +import org.apache.ibatis.annotations.*; + +import java.util.List; + +@Mapper +public interface RecordPlanMapper { + + @Insert(" ") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void add(RecordPlan plan); + + @Insert(" ") + void batchAddItem(@Param("planId") int planId, List planItemList); + + @Select("select * from wvp_record_plan where id = #{planId}") + RecordPlan get(@Param("planId") Integer planId); + + @Select(" ") + List query(@Param("query") String query); + + @Update("UPDATE wvp_record_plan SET update_time=#{updateTime}, name=#{name}, snap=#{snap} WHERE id=#{id}") + void update(RecordPlan plan); + + @Delete("DELETE FROM wvp_record_plan WHERE id=#{planId}") + void delete(@Param("planId") Integer planId); + + @Select("select * from wvp_record_plan_item where plan_id = #{planId}") + List getItemList(@Param("planId") Integer planId); + + @Delete("DELETE FROM wvp_record_plan_item WHERE plan_id = #{planId}") + void cleanItems(@Param("planId") Integer planId); + + @Select(" ") + List queryRecordIng(@Param("week") int week, @Param("index") int index); +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/RoleMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/RoleMapper.java new file mode 100755 index 0000000..3df7469 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/RoleMapper.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.storager.dao; + +import com.genersoft.iot.vmp.storager.dao.dto.Role; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Mapper +@Repository +public interface RoleMapper { + + @Insert("INSERT INTO wvp_user_role (name, authority, create_time, update_time) VALUES" + + "(#{name}, #{authority}, #{createTime}, #{updateTime})") + int add(Role role); + + @Update(value = {" "}) + int update(Role role); + + @Delete("DELETE from wvp_user_role WHERE id != 1 and id=#{id}") + int delete(int id); + + @Select("select * from wvp_user_role WHERE id=#{id}") + Role selectById(int id); + + @Select("select * from wvp_user_role") + List selectAll(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/UserApiKeyMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/UserApiKeyMapper.java new file mode 100644 index 0000000..18539f1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/UserApiKeyMapper.java @@ -0,0 +1,61 @@ +package com.genersoft.iot.vmp.storager.dao; + +import com.genersoft.iot.vmp.storager.dao.dto.UserApiKey; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Mapper +@Repository +public interface UserApiKeyMapper { + + @SelectKey(databaseId = "postgresql", statement = "SELECT currval('wvp_user_api_key_id_seq'::regclass) AS id", keyProperty = "id", before = false, resultType = Integer.class) + @SelectKey(databaseId = "mysql", statement = "SELECT LAST_INSERT_ID() AS id", keyProperty = "id", before = false, resultType = Integer.class) + @Insert("INSERT INTO wvp_user_api_key (user_id, app, api_key, expired_at, remark, enable, create_time, update_time) VALUES" + + "(#{userId}, #{app}, #{apiKey}, #{expiredAt}, #{remark}, #{enable}, #{createTime}, #{updateTime})") + int add(UserApiKey userApiKey); + + @Update(value = {""}) + int update(UserApiKey userApiKey); + + @Update("UPDATE wvp_user_api_key SET enable = true WHERE id = #{id}") + int enable(@Param("id") int id); + + @Update("UPDATE wvp_user_api_key SET enable = false WHERE id = #{id}") + int disable(@Param("id") int id); + + @Update("UPDATE wvp_user_api_key SET api_key = #{apiKey} WHERE id = #{id}") + int apiKey(@Param("id") int id, @Param("apiKey") String apiKey); + + @Update("UPDATE wvp_user_api_key SET remark = #{remark} WHERE id = #{id}") + int remark(@Param("id") int id, @Param("remark") String remark); + + @Delete("DELETE FROM wvp_user_api_key WHERE id = #{id}") + int delete(@Param("id") int id); + + @Select("SELECT uak.id, uak.user_id, uak.app, uak.api_key, uak.expired_at, uak.remark, uak.enable, uak.create_time, uak.update_time, u.username AS username FROM wvp_user_api_key uak LEFT JOIN wvp_user u on u.id = uak.user_id WHERE uak.id = #{id}") + UserApiKey selectById(@Param("id") int id); + + @Select("SELECT uak.id, uak.user_id, uak.app, uak.api_key, uak.expired_at, uak.remark, uak.enable, uak.create_time, uak.update_time, u.username AS username FROM wvp_user_api_key uak LEFT JOIN wvp_user u on u.id = uak.user_id WHERE uak.api_key = #{apiKey}") + UserApiKey selectByApiKey(@Param("apiKey") String apiKey); + + @Select("SELECT uak.id, uak.user_id, uak.app, uak.api_key, uak.expired_at, uak.remark, uak.enable, uak.create_time, uak.update_time, u.username AS username FROM wvp_user_api_key uak LEFT JOIN wvp_user u on u.id = uak.user_id") + List selectAll(); + + @Select("SELECT uak.id, uak.user_id, uak.app, uak.api_key, uak.expired_at, uak.remark, uak.enable, uak.create_time, uak.update_time, u.username AS username FROM wvp_user_api_key uak LEFT JOIN wvp_user u on u.id = uak.user_id") + List getUserApiKeys(); + + @Select("SELECT COUNT(0) FROM wvp_user_api_key WHERE api_key = #{apiKey}") + boolean isApiKeyExists(@Param("apiKey") String apiKey); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/UserMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/UserMapper.java new file mode 100755 index 0000000..8bfeab7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/UserMapper.java @@ -0,0 +1,60 @@ +package com.genersoft.iot.vmp.storager.dao; + +import com.genersoft.iot.vmp.storager.dao.dto.User; +import org.apache.ibatis.annotations.*; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Mapper +@Repository +public interface UserMapper { + + @Insert("INSERT INTO wvp_user (username, password, role_id, push_key, create_time, update_time) VALUES" + + "(#{username}, #{password}, #{role.id}, #{pushKey}, #{createTime}, #{updateTime})") + int add(User user); + + @Update(value = {" "}) + int update(User user); + + @Delete("DELETE from wvp_user WHERE id != 1 and id=#{id}") + int delete(int id); + + @Select("select u.*, r.name as roleName, r.authority as roleAuthority , r.create_time as role_create_time , r.update_time as role_update_time from wvp_user u, wvp_user_role r WHERE u.role_id=r.id and u.username=#{username} AND u.password=#{password}") + @Results(id = "roleMap", value = { + @Result(column = "role_id", property = "role.id"), + @Result(column = "role_name", property = "role.name"), + @Result(column = "role_authority", property = "role.authority"), + @Result(column = "role_create_time", property = "role.createTime"), + @Result(column = "role_update_time", property = "role.updateTime") + }) + User select(@Param("username") String username, @Param("password") String password); + + @Select("select u.*, r.name as role_name, r.authority as role_authority , r.create_time as role_create_time , r.update_time as role_update_time from wvp_user u, wvp_user_role r WHERE u.role_id=r.id and u.id=#{id}") + @ResultMap(value="roleMap") + User selectById(int id); + + @Select("select u.*, r.name as role_name, r.authority as role_authority , r.create_time as role_create_time , r.update_time as role_update_time from wvp_user u, wvp_user_role r WHERE u.role_id=r.id and u.username=#{username}") + @ResultMap(value="roleMap") + User getUserByUsername(String username); + + @Select("select u.*, r.name as role_name, r.authority as role_authority , r.create_time as role_create_time , r.update_time as role_update_time from wvp_user u, wvp_user_role r WHERE u.role_id=r.id") + @ResultMap(value="roleMap") + List selectAll(); + + @Select("select u.id, u.username,u.push_key,u.role_id, r.name as role_name, r.authority as role_authority , r.create_time as role_create_time , r.update_time as role_update_time from wvp_user u join wvp_user_role r on u.role_id=r.id") + @ResultMap(value="roleMap") + List getUsers(); + + @Update("UPDATE wvp_user set push_key=#{pushKey} where id=#{id}") + int changePushKey(@Param("id") int id, @Param("pushKey") String pushKey); +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/ChannelSourceInfo.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/ChannelSourceInfo.java new file mode 100755 index 0000000..e8b91e7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/ChannelSourceInfo.java @@ -0,0 +1,22 @@ +package com.genersoft.iot.vmp.storager.dao.dto; + +public class ChannelSourceInfo { + private String name; + private int count; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/LogDto.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/LogDto.java new file mode 100755 index 0000000..cfe29f5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/LogDto.java @@ -0,0 +1,86 @@ +package com.genersoft.iot.vmp.storager.dao.dto; + +public class LogDto { + + private int id; + private String name; + private String type; + private String uri; + private String address; + private String result; + private long timing; + private String username; + private String createTime; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getResult() { + return result; + } + + public void setResult(String result) { + this.result = result; + } + + public long getTiming() { + return timing; + } + + public void setTiming(long timing) { + this.timing = timing; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getCreateTime() { + return createTime; + } + + public void setCreateTime(String createTime) { + this.createTime = createTime; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/PlatformRegisterInfo.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/PlatformRegisterInfo.java new file mode 100755 index 0000000..16f6636 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/PlatformRegisterInfo.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.storager.dao.dto; + +/** + * 平台发送注册/注销消息时缓存此消息 + * @author lin + */ +public class PlatformRegisterInfo { + + /** + * 平台Id + */ + private String platformId; + + /** + * 是否时注册,false为注销 + */ + private boolean register; + + public static PlatformRegisterInfo getInstance(String platformId, boolean register) { + PlatformRegisterInfo platformRegisterInfo = new PlatformRegisterInfo(); + platformRegisterInfo.setPlatformId(platformId); + platformRegisterInfo.setRegister(register); + return platformRegisterInfo; + } + + public String getPlatformId() { + return platformId; + } + + public void setPlatformId(String platformId) { + this.platformId = platformId; + } + + public boolean isRegister() { + return register; + } + + public void setRegister(boolean register) { + this.register = register; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/RecordInfo.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/RecordInfo.java new file mode 100755 index 0000000..278e3e4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/RecordInfo.java @@ -0,0 +1,133 @@ +package com.genersoft.iot.vmp.storager.dao.dto; + +/** + * 录像记录 + */ +public class RecordInfo { + + /** + * ID + */ + private int id; + + /** + * 应用名 + */ + private String app; + + /** + * 流ID + */ + private String stream; + + /** + * 对应的zlm流媒体的ID + */ + private String mediaServerId; + + /** + * 创建时间 + */ + private String createTime; + + /** + * 类型 对应zlm的 originType + * unknown = 0, + * rtmp_push=1, + * rtsp_push=2, + * rtp_push=3, + * pull=4, + * ffmpeg_pull=5, + * mp4_vod=6, + * device_chn=7, + * rtc_push=8 + */ + private int type; + + /** + * 国标录像时的设备ID + */ + private String deviceId; + + /** + * 国标录像时的通道ID + */ + private String channelId; + + /** + * 拉流代理录像时的名称 + */ + private String name; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getMediaServerId() { + return mediaServerId; + } + + public void setMediaServerId(String mediaServerId) { + this.mediaServerId = mediaServerId; + } + + public String getCreateTime() { + return createTime; + } + + public void setCreateTime(String createTime) { + this.createTime = createTime; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/Role.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/Role.java new file mode 100755 index 0000000..44631f8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/Role.java @@ -0,0 +1,50 @@ +package com.genersoft.iot.vmp.storager.dao.dto; + +public class Role { + + private int id; + private String name; + private String authority; + private String createTime; + private String updateTime; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAuthority() { + return authority; + } + + public void setAuthority(String authority) { + this.authority = authority; + } + + public String getCreateTime() { + return createTime; + } + + public void setCreateTime(String createTime) { + this.createTime = createTime; + } + + public String getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(String updateTime) { + this.updateTime = updateTime; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/User.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/User.java new file mode 100755 index 0000000..c9b4002 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/User.java @@ -0,0 +1,68 @@ +package com.genersoft.iot.vmp.storager.dao.dto; + +public class User { + + private int id; + private String username; + private String password; + private String createTime; + private String updateTime; + private String pushKey; + private Role role; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getCreateTime() { + return createTime; + } + + public void setCreateTime(String createTime) { + this.createTime = createTime; + } + + public String getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(String updateTime) { + this.updateTime = updateTime; + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public String getPushKey() { + return pushKey; + } + + public void setPushKey(String pushKey) { + this.pushKey = pushKey; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/UserApiKey.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/UserApiKey.java new file mode 100644 index 0000000..b631295 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/UserApiKey.java @@ -0,0 +1,151 @@ +package com.genersoft.iot.vmp.storager.dao.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.io.Serializable; + +/** + * 用户信息 + */ +@Schema(description = "用户ApiKey信息") +public class UserApiKey implements Serializable { + + /** + * Id + */ + @Schema(description = "Id") + private int id; + + /** + * 用户Id + */ + @Schema(description = "用户Id") + private int userId; + + /** + * 应用名 + */ + @Schema(description = "应用名") + private String app; + + /** + * ApiKey + */ + @Schema(description = "ApiKey") + private String apiKey; + + /** + * 过期时间(null=永不过期) + */ + @Schema(description = "过期时间(null=永不过期)") + private long expiredAt; + + /** + * 备注信息 + */ + @Schema(description = "备注信息") + private String remark; + + /** + * 是否启用 + */ + @Schema(description = "是否启用") + private boolean enable; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + private String createTime; + + /** + * 更新时间 + */ + @Schema(description = "更新时间") + private String updateTime; + + /** + * 用户名 + */ + private String username; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public long getExpiredAt() { + return expiredAt; + } + + public void setExpiredAt(long expiredAt) { + this.expiredAt = expiredAt; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public boolean isEnable() { + return enable; + } + + public void setEnable(boolean enable) { + this.enable = enable; + } + + public String getCreateTime() { + return createTime; + } + + public void setCreateTime(String createTime) { + this.createTime = createTime; + } + + public String getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(String updateTime) { + this.updateTime = updateTime; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java b/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java new file mode 100755 index 0000000..7e45bc5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java @@ -0,0 +1,531 @@ +package com.genersoft.iot.vmp.storager.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.ServerInfo; +import com.genersoft.iot.vmp.common.SystemAllInfo; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.dao.DeviceChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.DeviceMapper; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import com.genersoft.iot.vmp.service.bean.MessageForPushChannel; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.JsonUtil; +import com.genersoft.iot.vmp.utils.SystemInfoUtils; +import com.genersoft.iot.vmp.utils.redis.RedisUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.*; + +@SuppressWarnings("rawtypes") +@Slf4j +@Component +public class RedisCatchStorageImpl implements IRedisCatchStorage { + + + @Autowired + private DeviceChannelMapper deviceChannelMapper; + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Override + public List queryAllSendRTPServer() { + return Collections.emptyList(); + } + + @Override + public Long getCSEQ() { + String key = VideoManagerConstants.SIP_CSEQ_PREFIX + userSetting.getServerId(); + + Long result = redisTemplate.opsForValue().increment(key, 1L); + if (result != null && result > Integer.MAX_VALUE) { + redisTemplate.opsForValue().set(key, 1); + result = 1L; + } + return result; + } + + @Override + public void resetAllCSEQ() { + String key = VideoManagerConstants.SIP_CSEQ_PREFIX + userSetting.getServerId(); + redisTemplate.opsForValue().set(key, 1); + } + + + @Override + public void updateWVPInfo(ServerInfo serverInfo, int time) { + String key = VideoManagerConstants.WVP_SERVER_PREFIX + userSetting.getServerId(); + Duration duration = Duration.ofSeconds(time); + redisTemplate.opsForValue().set(key, serverInfo, duration); + // 设置平台的分数值 + String setKey = VideoManagerConstants.WVP_SERVER_LIST; + // 首次设置就设置为0, 后续值越小说明越是最近启动的 + redisTemplate.opsForZSet().add(setKey, userSetting.getServerId(), System.currentTimeMillis()); + } + + @Override + public void removeOfflineWVPInfo(String serverId) { + String setKey = VideoManagerConstants.WVP_SERVER_LIST; + // 首次设置就设置为0, 后续值越小说明越是最近启动的 + redisTemplate.opsForZSet().remove(setKey, serverId); + } + + @Override + public void sendStreamChangeMsg(String type, JSONObject jsonObject) { + String key = VideoManagerConstants.WVP_MSG_STREAM_CHANGE_PREFIX + type; + log.info("[redis 流变化事件] 发送 {}: {}", key, jsonObject.toString()); + redisTemplate.convertAndSend(key, jsonObject); + } + + @Override + public void addStream(MediaServer mediaServerItem, String type, String app, String streamId, MediaInfo mediaInfo) { + // 查找是否使用了callID + StreamAuthorityInfo streamAuthorityInfo = getStreamAuthorityInfo(app, streamId); + String key = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetting.getServerId() + "_" + type.toUpperCase() + "_" + app + "_" + streamId + "_" + mediaServerItem.getId(); + if (streamAuthorityInfo != null) { + mediaInfo.setCallId(streamAuthorityInfo.getCallId()); + } + redisTemplate.opsForValue().set(key, JSON.toJSONString(mediaInfo)); + } + + @Override + public void removeStream(String mediaServerId, String type, String app, String streamId) { + String key = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetting.getServerId() + "_" + type.toUpperCase() + "_" + app + "_" + streamId + "_" + mediaServerId; + redisTemplate.delete(key); + } + + @Override + public void removeStream(String mediaServerId, String type) { + String key = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetting.getServerId() + "_" + type.toUpperCase() + "_*_*_" + mediaServerId; + List streams = RedisUtil.scan(redisTemplate, key); + for (Object stream : streams) { + redisTemplate.delete(stream); + } + } + + @Override + public List getStreams(String mediaServerId, String type) { + List result = new ArrayList<>(); + String key = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetting.getServerId() + "_" + type.toUpperCase() + "_*_*_" + mediaServerId; + List streams = RedisUtil.scan(redisTemplate, key); + for (Object stream : streams) { + String mediaInfoJson = (String)redisTemplate.opsForValue().get(stream); + MediaInfo mediaInfo = JSON.parseObject(mediaInfoJson, MediaInfo.class); + result.add(mediaInfo); + } + return result; + } + + @Override + public void updateDevice(Device device) { + String key = VideoManagerConstants.DEVICE_PREFIX; + redisTemplate.opsForHash().put(key, device.getDeviceId(), device); + } + + @Override + public void removeDevice(String deviceId) { + String key = VideoManagerConstants.DEVICE_PREFIX; + redisTemplate.opsForHash().delete(key, deviceId); + } + + @Override + public void removeAllDevice() { + String key = VideoManagerConstants.DEVICE_PREFIX; + redisTemplate.delete(key); + } + + @Override + public List getAllDevices() { + String key = VideoManagerConstants.DEVICE_PREFIX; + List result = new ArrayList<>(); + List values = redisTemplate.opsForHash().values(key); + for (Object value : values) { + if (Objects.nonNull(value)) { + result.add((Device)value); + } + } + return result; + } + + @Override + public Device getDevice(String deviceId) { + String key = VideoManagerConstants.DEVICE_PREFIX; + Device device; + Object object = redisTemplate.opsForHash().get(key, deviceId); + if (object == null){ + device = deviceMapper.getDeviceByDeviceId(deviceId); + if (device != null) { + updateDevice(device); + } + }else { + device = (Device)object; + } + return device; + } + + @Override + public void updateGpsMsgInfo(GPSMsgInfo gpsMsgInfo) { + String key = VideoManagerConstants.WVP_STREAM_GPS_MSG_PREFIX + userSetting.getServerId(); + Duration duration = Duration.ofSeconds(60L); + gpsMsgInfo.setStored(false); + redisTemplate.opsForHash().put(key, gpsMsgInfo.getId(),gpsMsgInfo); + redisTemplate.expire(key, duration); + // 默认GPS消息保存1分钟 + } + + @Override + public GPSMsgInfo getGpsMsgInfo(String channelId) { + String key = VideoManagerConstants.WVP_STREAM_GPS_MSG_PREFIX + userSetting.getServerId(); + return (GPSMsgInfo) redisTemplate.opsForHash().get(key, channelId); + } + + @Override + public List getAllGpsMsgInfo() { + String key = VideoManagerConstants.WVP_STREAM_GPS_MSG_PREFIX + userSetting.getServerId(); + List result = new ArrayList<>(); + List values = redisTemplate.opsForHash().values(key); + for (Object value : values) { + result.add((GPSMsgInfo)value); + } + return result; + } + + @Override + public void updateStreamAuthorityInfo(String app, String stream, StreamAuthorityInfo streamAuthorityInfo) { + String key = VideoManagerConstants.MEDIA_STREAM_AUTHORITY; + String objectKey = app+ "_" + stream; + redisTemplate.opsForHash().put(key, objectKey, streamAuthorityInfo); + } + + @Override + public void removeStreamAuthorityInfo(String app, String stream) { + String key = VideoManagerConstants.MEDIA_STREAM_AUTHORITY; + String objectKey = app+ "_" + stream; + redisTemplate.opsForHash().delete(key, objectKey); + } + + @Override + public StreamAuthorityInfo getStreamAuthorityInfo(String app, String stream) { + String key = VideoManagerConstants.MEDIA_STREAM_AUTHORITY; + String objectKey = app+ "_" + stream; + return (StreamAuthorityInfo)redisTemplate.opsForHash().get(key, objectKey); + + } + + @Override + public List getAllStreamAuthorityInfo() { + String key = VideoManagerConstants.MEDIA_STREAM_AUTHORITY; + List result = new ArrayList<>(); + List values = redisTemplate.opsForHash().values(key); + for (Object value : values) { + result.add((StreamAuthorityInfo)value); + } + return result; + } + + + @Override + public MediaInfo getStreamInfo(String app, String streamId, String mediaServerId) { + String scanKey = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetting.getServerId() + "_*_" + app + "_" + streamId + "_" + mediaServerId; + + MediaInfo result = null; + List keys = RedisUtil.scan(redisTemplate, scanKey); + if (keys.size() > 0) { + String key = (String) keys.get(0); + String mediaInfoJson = (String)redisTemplate.opsForValue().get(key); + result = JSON.parseObject(mediaInfoJson, MediaInfo.class); + } + + return result; + } + + @Override + public MediaInfo getProxyStream(String app, String streamId) { + String scanKey = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetting.getServerId() + "_PULL_" + app + "_" + streamId + "_*"; + + MediaInfo result = null; + List keys = RedisUtil.scan(redisTemplate, scanKey); + if (keys.size() > 0) { + String key = (String) keys.get(0); + String mediaInfoJson = (String)redisTemplate.opsForValue().get(key); + result = JSON.parseObject(mediaInfoJson, MediaInfo.class); + } + + return result; + } + + @Override + public void addCpuInfo(double cpuInfo) { + String key = VideoManagerConstants.SYSTEM_INFO_CPU_PREFIX + userSetting.getServerId(); + Map infoMap = new HashMap<>(); + infoMap.put("time", DateUtil.getNow()); + infoMap.put("data", String.valueOf(cpuInfo)); + redisTemplate.opsForList().rightPush(key, infoMap); + // 每秒一个,最多只存30个 + Long size = redisTemplate.opsForList().size(key); + if (size != null && size >= 30) { + for (int i = 0; i < size - 30; i++) { + redisTemplate.opsForList().leftPop(key); + } + } + } + + @Override + public void addMemInfo(double memInfo) { + String key = VideoManagerConstants.SYSTEM_INFO_MEM_PREFIX + userSetting.getServerId(); + Map infoMap = new HashMap<>(); + infoMap.put("time", DateUtil.getNow()); + infoMap.put("data", String.valueOf(memInfo)); + redisTemplate.opsForList().rightPush(key, infoMap); + // 每秒一个,最多只存30个 + Long size = redisTemplate.opsForList().size(key); + if (size != null && size >= 30) { + for (int i = 0; i < size - 30; i++) { + redisTemplate.opsForList().leftPop(key); + } + } + } + + @Override + public void addNetInfo(Map networkInterfaces) { + String key = VideoManagerConstants.SYSTEM_INFO_NET_PREFIX + userSetting.getServerId(); + Map infoMap = new HashMap<>(); + infoMap.put("time", DateUtil.getNow()); + for (String netKey : networkInterfaces.keySet()) { + infoMap.put(netKey, networkInterfaces.get(netKey)); + } + redisTemplate.opsForList().rightPush(key, infoMap); + // 每秒一个,最多只存30个 + Long size = redisTemplate.opsForList().size(key); + if (size != null && size >= 30) { + for (int i = 0; i < size - 30; i++) { + redisTemplate.opsForList().leftPop(key); + } + } + } + + @Override + public void addDiskInfo(List> diskInfo) { + + String key = VideoManagerConstants.SYSTEM_INFO_DISK_PREFIX + userSetting.getServerId(); + redisTemplate.opsForValue().set(key, diskInfo); + } + + @Override + public SystemAllInfo getSystemInfo() { + String cpuKey = VideoManagerConstants.SYSTEM_INFO_CPU_PREFIX + userSetting.getServerId(); + String memKey = VideoManagerConstants.SYSTEM_INFO_MEM_PREFIX + userSetting.getServerId(); + String netKey = VideoManagerConstants.SYSTEM_INFO_NET_PREFIX + userSetting.getServerId(); + String diskKey = VideoManagerConstants.SYSTEM_INFO_DISK_PREFIX + userSetting.getServerId(); + SystemAllInfo systemAllInfo = new SystemAllInfo(); + systemAllInfo.setCpu(redisTemplate.opsForList().range(cpuKey, 0, -1)); + systemAllInfo.setMem(redisTemplate.opsForList().range(memKey, 0, -1)); + systemAllInfo.setNet(redisTemplate.opsForList().range(netKey, 0, -1)); + + systemAllInfo.setDisk(redisTemplate.opsForValue().get(diskKey)); + systemAllInfo.setNetTotal(SystemInfoUtils.getNetworkTotal()); + return systemAllInfo; + } + + @Override + public void sendStreamPushRequestedMsg(MessageForPushChannel msg) { + String key = VideoManagerConstants.VM_MSG_STREAM_PUSH_REQUESTED; + log.info("[redis发送通知] 发送 推流被请求 {}: {}/{}", key, msg.getApp(), msg.getStream()); + redisTemplate.convertAndSend(key, JSON.toJSON(msg)); + } + + @Override + public void sendAlarmMsg(AlarmChannelMessage msg) { + // 此消息用于对接第三方服务下级来的消息内容 + String key = VideoManagerConstants.VM_MSG_SUBSCRIBE_ALARM; + log.info("[redis发送通知] 发送 报警{}: {}", key, JSON.toJSON(msg)); + redisTemplate.convertAndSend(key, JSON.toJSON(msg)); + } + + @Override + public boolean deviceIsOnline(String deviceId) { + return getDevice(deviceId).isOnLine(); + } + + + @Override + public void sendStreamPushRequestedMsgForStatus() { + String key = VideoManagerConstants.VM_MSG_GET_ALL_ONLINE_REQUESTED; + log.info("[redis通知] 发送 获取所有推流设备的状态"); + JSONObject jsonObject = new JSONObject(); + jsonObject.put(key, key); + redisTemplate.convertAndSend(key, jsonObject); + } + + @Override + public int getPushStreamCount(String id) { + String key = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetting.getServerId() + "_PUSH_*_*_" + id; + return RedisUtil.scan(redisTemplate, key).size(); + } + + @Override + public int getProxyStreamCount(String id) { + String key = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetting.getServerId() + "_PULL_*_*_" + id; + return RedisUtil.scan(redisTemplate, key).size(); + } + + @Override + public int getGbSendCount(String id) { + String key = VideoManagerConstants.SEND_RTP_INFO_CALLID; + return redisTemplate.opsForHash().size(key).intValue(); + } + + @Override + public void sendDeviceOrChannelStatus(String deviceId, String channelId, boolean online) { + String key = VideoManagerConstants.VM_MSG_SUBSCRIBE_DEVICE_STATUS; + StringBuilder msg = new StringBuilder(); + msg.append(deviceId); + if (channelId != null) { + msg.append(":").append(channelId); + } + msg.append(" ").append(online? "ON":"OFF"); + log.info("[redis通知] 推送设备/通道状态-> {} ", msg); + // 使用 RedisTemplate 发送字符串消息会导致发送的消息多带了双引号 + stringRedisTemplate.convertAndSend(key, msg.toString()); + } + + @Override + public void sendChannelAddOrDelete(String deviceId, String channelId, boolean add) { + String key = VideoManagerConstants.VM_MSG_SUBSCRIBE_DEVICE_STATUS; + + + StringBuilder msg = new StringBuilder(); + msg.append(deviceId); + if (channelId != null) { + msg.append(":").append(channelId); + } + msg.append(" ").append(add? "ADD":"DELETE"); + log.info("[redis通知] 推送通道-> {}", msg); + // 使用 RedisTemplate 发送字符串消息会导致发送的消息多带了双引号 + stringRedisTemplate.convertAndSend(key, msg.toString()); + } + + @Override + public void sendPlatformStartPlayMsg(SendRtpInfo sendRtpItem, DeviceChannel channel, Platform platform) { + if (platform == null) { + log.info("[redis发送通知] 失败, 平台信息为NULL"); + return; + } + if (sendRtpItem.getPlayType() != InviteStreamType.PUSH) { + log.info("[redis发送通知] 取消, 流来源通道不是推流设备"); + return; + } + MessageForPushChannel messageForPushChannel = MessageForPushChannel.getInstance(0, sendRtpItem.getApp(), sendRtpItem.getStream(), + channel.getDeviceId(), platform.getServerGBId(), platform.getName(), userSetting.getServerId(), + sendRtpItem.getMediaServerId()); + messageForPushChannel.setPlatFormIndex(platform.getId()); + String key = VideoManagerConstants.VM_MSG_STREAM_START_PLAY_NOTIFY; + log.info("[redis发送通知] 发送 推流被上级平台观看 {}: {}/{}->{}", key, sendRtpItem.getApp(), sendRtpItem.getStream(), platform.getServerGBId()); + redisTemplate.convertAndSend(key, JSON.toJSON(messageForPushChannel)); + } + + @Override + public void sendPlatformStopPlayMsg(SendRtpInfo sendRtpItem, Platform platform, CommonGBChannel channel) { + + MessageForPushChannel msg = MessageForPushChannel.getInstance(0, + sendRtpItem.getApp(), sendRtpItem.getStream(), channel.getGbDeviceId(), + sendRtpItem.getTargetId(), platform.getName(), userSetting.getServerId(), sendRtpItem.getMediaServerId()); + msg.setPlatFormIndex(platform.getId()); + + String key = VideoManagerConstants.VM_MSG_STREAM_STOP_PLAY_NOTIFY; + log.info("[redis发送通知] 发送 上级平台停止观看 {}: {}/{}->{}", key, sendRtpItem.getApp(), sendRtpItem.getStream(), platform.getServerGBId()); + redisTemplate.convertAndSend(key, JSON.toJSON(msg)); + } + + @Override + public void addPushListItem(String app, String stream, MediaInfo mediaInfo) { + String key = VideoManagerConstants.PUSH_STREAM_LIST + app + "_" + stream; + redisTemplate.opsForValue().set(key, mediaInfo); + } + + @Override + public MediaInfo getPushListItem(String app, String stream) { + String key = VideoManagerConstants.PUSH_STREAM_LIST + app + "_" + stream; + return (MediaInfo)redisTemplate.opsForValue().get(key); + } + + @Override + public void removePushListItem(String app, String stream, String mediaServerId) { + String key = VideoManagerConstants.PUSH_STREAM_LIST + app + "_" + stream; + MediaInfo param = (MediaInfo)redisTemplate.opsForValue().get(key); + if (param != null) { + redisTemplate.delete(key); + } + } + + @Override + public void sendPushStreamClose(MessageForPushChannel msg) { + String key = VideoManagerConstants.VM_MSG_STREAM_PUSH_CLOSE_REQUESTED; + log.info("[redis发送通知] 发送 停止向上级推流 {}: {}/{}->{}", key, msg.getApp(), msg.getStream(), msg.getPlatFormId()); + redisTemplate.convertAndSend(key, JSON.toJSON(msg)); + } + + @Override + public void addWaiteSendRtpItem(SendRtpInfo sendRtpItem, int platformPlayTimeout) { + String key = VideoManagerConstants.WAITE_SEND_PUSH_STREAM + sendRtpItem.getApp() + "_" + sendRtpItem.getStream(); + redisTemplate.opsForValue().set(key, sendRtpItem); + } + + @Override + public SendRtpInfo getWaiteSendRtpItem(String app, String stream) { + String key = VideoManagerConstants.WAITE_SEND_PUSH_STREAM + app + "_" + stream; + return JsonUtil.redisJsonToObject(redisTemplate, key, SendRtpInfo.class); + } + + @Override + public void sendStartSendRtp(SendRtpInfo sendRtpItem) { + String key = VideoManagerConstants.START_SEND_PUSH_STREAM + sendRtpItem.getApp() + "_" + sendRtpItem.getStream(); + log.info("[redis发送通知] 通知其他WVP推流 {}: {}/{}->{}", key, sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getTargetId()); + redisTemplate.convertAndSend(key, JSON.toJSON(sendRtpItem)); + } + + @Override + public void sendPushStreamOnline(SendRtpInfo sendRtpItem) { + String key = VideoManagerConstants.VM_MSG_STREAM_PUSH_CLOSE_REQUESTED; + log.info("[redis发送通知] 流上线 {}: {}/{}->{}", key, sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getTargetId()); + redisTemplate.convertAndSend(key, JSON.toJSON(sendRtpItem)); + } + + @Override + public ServerInfo queryServerInfo(String serverId) { + String key = VideoManagerConstants.WVP_SERVER_PREFIX + serverId; + return (ServerInfo)redisTemplate.opsForValue().get(key); + } + + @Override + public String chooseOneServer(String serverId) { + String key = VideoManagerConstants.WVP_SERVER_LIST; + redisTemplate.opsForZSet().remove(key, serverId); + Set range = redisTemplate.opsForZSet().range(key, 0, 0); + if (range == null || range.isEmpty()) { + return null; + } + return (String) range.iterator().next(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/bean/StreamProxy.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/bean/StreamProxy.java new file mode 100755 index 0000000..14f2a6d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/bean/StreamProxy.java @@ -0,0 +1,83 @@ +package com.genersoft.iot.vmp.streamProxy.bean; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.util.ObjectUtils; + +/** + * @author lin + */ +@Data +@Schema(description = "拉流代理的信息") +@EqualsAndHashCode(callSuper = true) +public class StreamProxy extends CommonGBChannel { + + /** + * 数据库自增ID + */ + @Schema(description = "数据库自增ID") + private int id; + + @Schema(description = "类型,取值,default: 流媒体直接拉流(默认),ffmpeg: ffmpeg实现拉流") + private String type; + + @Schema(description = "应用名") + private String app; + + @Schema(description = "流ID") + private String stream; + + @Schema(description = "当前拉流使用的流媒体服务ID") + private String mediaServerId; + + @Schema(description = "固定选择的流媒体服务ID") + private String relatesMediaServerId; + + @Schema(description = "服务ID") + private String serverId; + + @Schema(description = "拉流地址") + private String srcUrl; + + @Schema(description = "超时时间:秒") + private int timeout; + + @Schema(description = "ffmpeg模板KEY") + private String ffmpegCmdKey; + + @Schema(description = "rtsp拉流时,拉流方式,0:tcp,1:udp,2:组播") + private String rtspType; + + @Schema(description = "是否启用") + private boolean enable; + + @Schema(description = "是否启用音频") + private boolean enableAudio; + + @Schema(description = "是否启用MP4") + private boolean enableMp4; + + @Schema(description = "是否 无人观看时自动停用") + private boolean enableDisableNoneReader; + + @Schema(description = "拉流代理时zlm返回的key,用于停止拉流代理") + private String streamKey; + + @Schema(description = "拉流状态") + private Boolean pulling; + + public CommonGBChannel buildCommonGBChannel() { + if (ObjectUtils.isEmpty(this.getGbDeviceId())) { + return null; + } + if (ObjectUtils.isEmpty(this.getGbName())) { + this.setGbName( app+ "-" +stream); + } + this.setDataType(ChannelDataType.STREAM_PROXY); + this.setDataDeviceId(this.getId()); + return this; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/bean/StreamProxyParam.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/bean/StreamProxyParam.java new file mode 100755 index 0000000..3954fdf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/bean/StreamProxyParam.java @@ -0,0 +1,71 @@ +package com.genersoft.iot.vmp.streamProxy.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @author lin + */ +@Data +@Schema(description = "拉流代理的信息") +public class StreamProxyParam { + + @Schema(description = "类型,取值,default: 流媒体直接拉流(默认),ffmpeg: ffmpeg实现拉流") + private String type; + + @Schema(description = "应用名") + private String app; + + @Schema(description = "名称") + private String name; + + @Schema(description = "流ID") + private String stream; + + @Schema(description = "流媒体服务ID") + private String mediaServerId; + + @Schema(description = "拉流地址") + private String url; + + @Schema(description = "超时时间:秒") + private int timeoutMs; + + @Schema(description = "ffmpeg模板KEY") + private String ffmpegCmdKey; + + @Schema(description = "rtsp拉流时,拉流方式,0:tcp,1:udp,2:组播") + private String rtpType; + + @Schema(description = "是否启用") + private boolean enable; + + @Schema(description = "是否启用音频") + private boolean enableAudio; + + @Schema(description = "是否启用MP4") + private boolean enableMp4; + + @Schema(description = "是否 无人观看时自动停用") + private boolean enableDisableNoneReader; + + + public StreamProxy buildStreamProxy(String serverId) { + StreamProxy streamProxy = new StreamProxy(); + streamProxy.setApp(app); + streamProxy.setStream(stream); + streamProxy.setRelatesMediaServerId(mediaServerId); + streamProxy.setServerId(serverId); + streamProxy.setSrcUrl(url); + streamProxy.setTimeout(timeoutMs/1000); + streamProxy.setRtspType(rtpType); + streamProxy.setEnable(enable); + streamProxy.setEnableAudio(enableAudio); + streamProxy.setEnableMp4(enableMp4); + streamProxy.setEnableDisableNoneReader(enableDisableNoneReader); + streamProxy.setFfmpegCmdKey(ffmpegCmdKey); + streamProxy.setGbName(name); + return streamProxy; + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/controller/StreamProxyController.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/controller/StreamProxyController.java new file mode 100755 index 0000000..a2cce93 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/controller/StreamProxyController.java @@ -0,0 +1,223 @@ +package com.genersoft.iot.vmp.streamProxy.controller; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyPlayService; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; + +@SuppressWarnings("rawtypes") +/** + * 拉流代理接口 + */ +@Tag(name = "拉流代理", description = "") +@RestController +@Slf4j +@RequestMapping(value = "/api/proxy") +public class StreamProxyController { + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private IStreamProxyService streamProxyService; + + @Autowired + private IStreamProxyPlayService streamProxyPlayService; + + @Autowired + private UserSetting userSetting; + + + @Operation(summary = "分页查询流代理", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页") + @Parameter(name = "count", description = "每页查询数量") + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "pulling", description = "是否正在拉流") + @Parameter(name = "mediaServerId", description = "流媒体ID") + @GetMapping(value = "/list") + @ResponseBody + public PageInfo list(@RequestParam(required = false)Integer page, + @RequestParam(required = false)Integer count, + @RequestParam(required = false)String query, + @RequestParam(required = false)Boolean pulling, + @RequestParam(required = false)String mediaServerId){ + + if (ObjectUtils.isEmpty(mediaServerId)) { + mediaServerId = null; + } + if (ObjectUtils.isEmpty(query)) { + query = null; + } + return streamProxyService.getAll(page, count, query, pulling, mediaServerId); + } + + @Operation(summary = "查询流代理", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "app", description = "应用名") + @Parameter(name = "stream", description = "流Id") + @GetMapping(value = "/one") + @ResponseBody + public StreamProxy one(String app, String stream){ + + return streamProxyService.getStreamProxyByAppAndStream(app, stream); + } + + @Operation(summary = "新增代理", security = @SecurityRequirement(name = JwtUtils.HEADER), parameters = { + @Parameter(name = "param", description = "代理参数", required = true), + }) + @PostMapping(value = "/add") + @ResponseBody + public StreamProxy add(@RequestBody StreamProxy param){ + log.info("添加代理: " + JSONObject.toJSONString(param)); + if (ObjectUtils.isEmpty(param.getRelatesMediaServerId())) { + param.setRelatesMediaServerId(null); + } + if (ObjectUtils.isEmpty(param.getType())) { + param.setType("default"); + } + if (ObjectUtils.isEmpty(param.getGbId())) { + param.setGbDeviceId(null); + } + param.setServerId(userSetting.getServerId()); + streamProxyService.add(param); + return param; + } + + @Operation(summary = "更新代理", security = @SecurityRequirement(name = JwtUtils.HEADER), parameters = { + @Parameter(name = "param", description = "代理参数", required = true), + }) + @PostMapping(value = "/update") + @ResponseBody + public StreamProxy update(@RequestBody StreamProxy param){ + log.info("更新代理: " + JSONObject.toJSONString(param)); + if (param.getId() == 0) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "缺少代理信息的ID"); + } + if (ObjectUtils.isEmpty(param.getRelatesMediaServerId())) { + param.setRelatesMediaServerId(null); + } + if (ObjectUtils.isEmpty(param.getGbId())) { + param.setGbDeviceId(null); + } + streamProxyService.update(param); + return param; + } + + @GetMapping(value = "/ffmpeg_cmd/list") + @ResponseBody + @Operation(summary = "获取ffmpeg.cmd模板", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "mediaServerId", description = "流媒体ID", required = true) + public Map getFFmpegCMDs(@RequestParam String mediaServerId){ + log.debug("获取节点[ {} ]ffmpeg.cmd模板", mediaServerId ); + + MediaServer mediaServerItem = mediaServerService.getOne(mediaServerId); + if (mediaServerItem == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "流媒体: " + mediaServerId + "未找到"); + } + return streamProxyService.getFFmpegCMDs(mediaServerItem); + } + + @DeleteMapping(value = "/del") + @ResponseBody + @Operation(summary = "移除代理", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流id", required = true) + public void del(@RequestParam String app, @RequestParam String stream){ + log.info("移除代理: " + app + "/" + stream); + if (app == null || stream == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), app == null ?"app不能为null":"stream不能为null"); + }else { + streamProxyService.delteByAppAndStream(app, stream); + } + } + + @DeleteMapping(value = "/delete") + @ResponseBody + @Operation(summary = "移除代理", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "代理ID", required = true) + public void delte(int id){ + log.info("移除代理: {}", id); + streamProxyService.delete(id); + } + + @GetMapping(value = "/start") + @ResponseBody + @Operation(summary = "播放代理", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "代理Id", required = true) + public DeferredResult> start(HttpServletRequest request, int id){ + log.info("播放代理: {}", id); + StreamProxy streamProxy = streamProxyService.getStreamProxy(id); + Assert.notNull(streamProxy, "代理信息不存在"); + + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + + ErrorCallback callback = (code, msg, streamInfo) -> { + if (code == InviteErrorCode.SUCCESS.getCode()) { + WVPResult wvpResult = WVPResult.success(); + if (streamInfo != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + if (!ObjectUtils.isEmpty(streamInfo.getMediaServer().getTranscodeSuffix()) + && !"null".equalsIgnoreCase(streamInfo.getMediaServer().getTranscodeSuffix())) { + streamInfo.setStream(streamInfo.getStream() + "_" + streamInfo.getMediaServer().getTranscodeSuffix()); + } + wvpResult.setData(new StreamContent(streamInfo)); + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + + result.setResult(wvpResult); + }else { + result.setResult(WVPResult.fail(code, msg)); + } + }; + + streamProxyPlayService.start(id, null, callback); + return result; + } + + @GetMapping(value = "/stop") + @ResponseBody + @Operation(summary = "停止播放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "代理Id", required = true) + public void stop(int id){ + log.info("停止播放: {}", id); + streamProxyPlayService.stop(id); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/dao/StreamProxyMapper.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/dao/StreamProxyMapper.java new file mode 100755 index 0000000..e9970b6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/dao/StreamProxyMapper.java @@ -0,0 +1,95 @@ +package com.genersoft.iot.vmp.streamProxy.dao; + +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.streamProxy.dao.provider.StreamProxyProvider; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Mapper +@Repository +public interface StreamProxyMapper { + + @Insert("INSERT INTO wvp_stream_proxy (type, app, stream,relates_media_server_id, src_url, " + + "timeout, ffmpeg_cmd_key, rtsp_type, enable_audio, enable_mp4, enable, pulling, " + + "enable_disable_none_reader, server_id, create_time) VALUES" + + "(#{type}, #{app}, #{stream}, #{relatesMediaServerId}, #{srcUrl}, " + + "#{timeout}, #{ffmpegCmdKey}, #{rtspType}, #{enableAudio}, #{enableMp4}, #{enable}, #{pulling}, " + + "#{enableDisableNoneReader}, #{serverId}, #{createTime} )") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int add(StreamProxy streamProxyDto); + + @Update("UPDATE wvp_stream_proxy " + + "SET type=#{type}, " + + "app=#{app}," + + "stream=#{stream}," + + "relates_media_server_id=#{relatesMediaServerId}, " + + "src_url=#{srcUrl}," + + "timeout=#{timeout}, " + + "ffmpeg_cmd_key=#{ffmpegCmdKey}, " + + "rtsp_type=#{rtspType}, " + + "enable_audio=#{enableAudio}, " + + "enable=#{enable}, " + + "pulling=#{pulling}, " + + "enable_disable_none_reader=#{enableDisableNoneReader}, " + + "enable_mp4=#{enableMp4} " + + "WHERE id=#{id}") + int update(StreamProxy streamProxyDto); + + @Delete("DELETE FROM wvp_stream_proxy WHERE app=#{app} AND stream=#{stream}") + int delByAppAndStream(String app, String stream); + + @SelectProvider(type = StreamProxyProvider.class, method = "selectAll") + List selectAll(@Param("query") String query, @Param("pulling") Boolean pulling, @Param("mediaServerId") String mediaServerId); + + @SelectProvider(type = StreamProxyProvider.class, method = "selectOneByAppAndStream") + StreamProxy selectOneByAppAndStream(@Param("app") String app, @Param("stream") String stream); + + @SelectProvider(type = StreamProxyProvider.class, method = "selectForPushingInMediaServer") + List selectForPushingInMediaServer(@Param("mediaServerId") String mediaServerId, @Param("enable") boolean enable); + + + @Select("select count(1) from wvp_stream_proxy") + int getAllCount(); + + @Select("select count(1) from wvp_stream_proxy where pulling = true") + int getOnline(); + + @Delete("DELETE FROM wvp_stream_proxy WHERE id=#{id}") + int delete(@Param("id") int id); + + @Delete(value = "") + void deleteByList(List streamProxiesForRemove); + + @Update("UPDATE wvp_stream_proxy " + + "SET pulling=true " + + "WHERE id=#{id}") + int online(@Param("id") int id); + + @Update("UPDATE wvp_stream_proxy " + + "SET pulling=false " + + "WHERE id=#{id}") + int offline(@Param("id") int id); + + @SelectProvider(type = StreamProxyProvider.class, method = "select") + StreamProxy select(@Param("id") int id); + + @Update("UPDATE wvp_stream_proxy " + + " SET pulling=false, media_server_id = null," + + " stream_key = null " + + " WHERE id=#{id}") + void removeStream(@Param("id")int id); + + @Update("UPDATE wvp_stream_proxy " + + " SET pulling=#{pulling}, media_server_id = #{mediaServerId}, " + + " stream_key = #{streamKey} " + + " WHERE id=#{id}") + void updateStream(StreamProxy streamProxy); +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/dao/provider/StreamProxyProvider.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/dao/provider/StreamProxyProvider.java new file mode 100644 index 0000000..e61c710 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/dao/provider/StreamProxyProvider.java @@ -0,0 +1,65 @@ +package com.genersoft.iot.vmp.streamProxy.dao.provider; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; + +import java.util.Map; + +public class StreamProxyProvider { + + public String getBaseSelectSql(){ + return "SELECT " + + " st.*, " + + ChannelDataType.STREAM_PROXY + " as data_type, " + + " st.id as data_device_id, " + + " wdc.*, " + + " wdc.id as gb_id" + + " FROM wvp_stream_proxy st " + + " LEFT join wvp_device_channel wdc " + + " on wdc.data_type = 3 and st.id = wdc.data_device_id "; + } + + public String select(Map params ){ + return getBaseSelectSql() + " WHERE st.id = " + params.get("id"); + } + + public String selectForPushingInMediaServer(Map params ){ + return getBaseSelectSql() + " WHERE st.pulling=true and st.media_server_id=#{mediaServerId} order by st.create_time desc"; + } + + public String selectOneByAppAndStream(Map params ){ + return getBaseSelectSql() + String.format(" WHERE st.app='%s' AND st.stream='%s' order by st.create_time desc", + params.get("app"), params.get("stream")); + } + + public String selectAll(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(getBaseSelectSql()); + sqlBuild.append(" WHERE 1=1 "); + if (params.get("query") != null) { + sqlBuild.append(" AND ") + .append(" (") + .append(" st.app LIKE ").append("'%").append(params.get("query")).append("%' escape '/'") + .append(" OR") + .append(" st.stream LIKE ").append("'%").append(params.get("query")).append("%' escape '/'") + .append(" OR") + .append(" wdc.gb_device_id LIKE ").append("'%").append(params.get("query")).append("%' escape '/'") + .append(" OR") + .append(" wdc.gb_name LIKE ").append("'%").append(params.get("query")).append("%' escape '/'") + .append(" )") + ; + } + Object pulling = params.get("pulling"); + if (pulling != null) { + if ((Boolean) pulling) { + sqlBuild.append(" AND st.pulling=1 "); + }else { + sqlBuild.append(" AND st.pulling=0 "); + } + } + if (params.get("mediaServerId") != null) { + sqlBuild.append(" AND st.media_server_id='").append(params.get("mediaServerId")).append("'"); + } + sqlBuild.append(" order by st.create_time desc"); + return sqlBuild.toString(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/service/IStreamProxyPlayService.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/IStreamProxyPlayService.java new file mode 100755 index 0000000..a96532d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/IStreamProxyPlayService.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.streamProxy.service; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import jakarta.validation.constraints.NotNull; + + +public interface IStreamProxyPlayService { + + void start(int id, Boolean record, ErrorCallback callback); + + void startProxy(@NotNull StreamProxy streamProxy, ErrorCallback callback); + + void stop(int id); + + void stopProxy(StreamProxy streamProxy); +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/service/IStreamProxyService.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/IStreamProxyService.java new file mode 100755 index 0000000..ce3fef1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/IStreamProxyService.java @@ -0,0 +1,90 @@ +package com.genersoft.iot.vmp.streamProxy.service; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo; +import com.github.pagehelper.PageInfo; + +import java.util.Map; + +public interface IStreamProxyService { + + /** + * 分页查询 + * @param page + * @param count + * @return + */ + PageInfo getAll(Integer page, Integer count, String query, Boolean pulling,String mediaServerId); + + /** + * 删除视频代理 + * @param app + * @param stream + */ + void delteByAppAndStream(String app, String stream); + + /** + * 启用视频代理 + * @param app + * @param stream + * @return + */ + void startByAppAndStream(String app, String stream, ErrorCallback callback); + + /** + * 停用用视频代理 + * @param app + * @param stream + * @return + */ + void stopByAppAndStream(String app, String stream); + + /** + * 获取ffmpeg.cmd模板 + * + * @return + */ + Map getFFmpegCMDs(MediaServer mediaServerItem); + + /** + * 根据app与stream获取streamProxy + * @return + */ + StreamProxy getStreamProxyByAppAndStream(String app, String streamId); + + + /** + * 新的节点加入 + * @param mediaServer + * @return + */ + void zlmServerOnline(MediaServer mediaServer); + + /** + * 节点离线 + * @param mediaServer + * @return + */ + void zlmServerOffline(MediaServer mediaServer); + + /** + * 更新代理流 + */ + boolean update(StreamProxy streamProxyItem); + + /** + * 获取统计信息 + * @return + */ + ResourceBaseInfo getOverview(); + + void add(StreamProxy streamProxy); + + StreamProxy getStreamProxy(int id); + + void delete(int id); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/SourcePlayServiceForStreamProxyImpl.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/SourcePlayServiceForStreamProxyImpl.java new file mode 100644 index 0000000..984d565 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/SourcePlayServiceForStreamProxyImpl.java @@ -0,0 +1,42 @@ +package com.genersoft.iot.vmp.streamProxy.service.impl; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.ISourcePlayService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyPlayService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sip.message.Response; + +@Slf4j +@Service(ChannelDataType.PLAY_SERVICE + ChannelDataType.STREAM_PROXY) +public class SourcePlayServiceForStreamProxyImpl implements ISourcePlayService { + + @Autowired + private IStreamProxyPlayService playService; + + @Override + public void play(CommonGBChannel channel, Platform platform, Boolean record, ErrorCallback callback) { + // 拉流代理通道 + try { + playService.start(channel.getDataDeviceId(), record, callback); + }catch (Exception e) { + callback.run(Response.BUSY_HERE, "busy here", null); + } + } + + @Override + public void stopPlay(CommonGBChannel channel) { + // 拉流代理通道 + try { + playService.stop(channel.getDataDeviceId()); + }catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/StreamProxyPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/StreamProxyPlayServiceImpl.java new file mode 100755 index 0000000..9e0d444 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/StreamProxyPlayServiceImpl.java @@ -0,0 +1,161 @@ +package com.genersoft.iot.vmp.streamProxy.service.impl; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.streamProxy.dao.StreamProxyMapper; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyPlayService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import java.util.UUID; + +/** + * 视频代理业务 + */ +@Slf4j +@Service +public class StreamProxyPlayServiceImpl implements IStreamProxyPlayService { + + @Autowired + private StreamProxyMapper streamProxyMapper; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private HookSubscribe subscribe; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IRedisRpcPlayService redisRpcPlayService; + + @Override + public void start(int id, Boolean record, ErrorCallback callback) { + log.info("[拉流代理], 开始拉流,ID:{}", id); + StreamProxy streamProxy = streamProxyMapper.select(id); + if (streamProxy == null) { + throw new ControllerException(ErrorCode.ERROR404.getCode(), "代理信息未找到"); + } + log.info("[拉流代理] 类型: {}, app:{}, stream: {}, 流地址: {}", streamProxy.getType(), streamProxy.getApp(), streamProxy.getStream(), streamProxy.getSrcUrl()); + if (record != null) { + streamProxy.setEnableMp4(record); + } + + startProxy(streamProxy, callback); + } + + @Override + public void startProxy(@NotNull StreamProxy streamProxy, ErrorCallback callback){ + if (!streamProxy.isEnable()) { + callback.run(ErrorCode.ERROR100.getCode(), "代理未启用", null); + return; + } + if (streamProxy.getServerId() == null) { + streamProxy.setServerId(userSetting.getServerId()); + } + if (!userSetting.getServerId().equals(streamProxy.getServerId())) { + log.info("[拉流代理] 由其他服务{}管理", streamProxy.getServerId()); + redisRpcPlayService.playProxy(streamProxy.getServerId(), streamProxy.getId(), callback); + return; + } + + if (streamProxy.getMediaServerId() != null) { + StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStreamWithCheck(streamProxy.getApp(), streamProxy.getStream(), streamProxy.getMediaServerId(), null, false); + if (streamInfo != null) { + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo); + return; + } + } + + MediaServer mediaServer; + String mediaServerId = streamProxy.getRelatesMediaServerId(); + if (mediaServerId == null) { + mediaServer = mediaServerService.getMediaServerForMinimumLoad(null); + }else { + mediaServer = mediaServerService.getOne(mediaServerId); + } + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), mediaServerId == null?"未找到可用的媒体节点":"未找到节点" + mediaServerId); + } + + // 设置流超时的定时任务 + String timeOutTaskKey = UUID.randomUUID().toString(); + Hook rtpHook = Hook.getInstance(HookType.on_media_arrival, streamProxy.getApp(), streamProxy.getStream(), mediaServer.getId()); + dynamicTask.startDelay(timeOutTaskKey, () -> { + log.info("[拉流代理] 收流超时,app:{},stream: {}", streamProxy.getApp(), streamProxy.getStream()); + // 收流超时 + subscribe.removeSubscribe(rtpHook); + callback.run(InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null); + }, userSetting.getPlayTimeout()); + + // 开启流到来的监听 + subscribe.addSubscribe(rtpHook, (hookData) -> { + log.info("[拉流代理] 收流成功,app:{},stream: {}", hookData.getApp(), hookData.getStream()); + dynamicTask.stop(timeOutTaskKey); + StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServer, hookData.getApp(), hookData.getStream(), hookData.getMediaInfo(), null); + // hook响应 + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + subscribe.removeSubscribe(rtpHook); + streamProxy.setPulling(true); + streamProxyMapper.updateStream(streamProxy); + }); + + String key = mediaServerService.startProxy(mediaServer, streamProxy); + streamProxy.setStreamKey(key); + streamProxy.setMediaServerId(mediaServer.getId()); + streamProxyMapper.updateStream(streamProxy); + } + + @Override + public void stop(int id) { + StreamProxy streamProxy = streamProxyMapper.select(id); + if (streamProxy == null) { + throw new ControllerException(ErrorCode.ERROR404.getCode(), "代理信息未找到"); + } + if (!userSetting.getServerId().equals(streamProxy.getServerId())) { + redisRpcPlayService.stopProxy(streamProxy.getServerId(), streamProxy.getId()); + return; + } + stopProxy(streamProxy); + } + + @Override + public void stopProxy(StreamProxy streamProxy){ + + String mediaServerId = streamProxy.getMediaServerId(); + Assert.notNull(mediaServerId, "代理节点不存在"); + + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "媒体节点不存在"); + } + if (ObjectUtils.isEmpty(streamProxy.getStreamKey())) { + mediaServerService.closeStreams(mediaServer, streamProxy.getApp(), streamProxy.getStream()); + }else { + mediaServerService.stopProxy(mediaServer, streamProxy.getStreamKey(), streamProxy.getType()); + } + streamProxyMapper.removeStream(streamProxy.getId()); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/StreamProxyServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/StreamProxyServiceImpl.java new file mode 100755 index 0000000..ca51412 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/StreamProxyServiceImpl.java @@ -0,0 +1,383 @@ +package com.genersoft.iot.vmp.streamProxy.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.media.event.media.MediaNotFoundEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerOfflineEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerOnlineEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OriginType; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.streamProxy.dao.StreamProxyMapper; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyPlayService; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyService; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 视频代理业务 + */ +@Slf4j +@Service +public class StreamProxyServiceImpl implements IStreamProxyService { + + @Autowired + private StreamProxyMapper streamProxyMapper; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IStreamProxyPlayService playService; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private IGbChannelService gbChannelService; + + @Autowired + DataSourceTransactionManager dataSourceTransactionManager; + + @Autowired + TransactionDefinition transactionDefinition; + + /** + * 流到来的处理 + */ + @Async("taskExecutor") + @Transactional + @org.springframework.context.event.EventListener + public void onApplicationEvent(MediaArrivalEvent event) { + if ("rtsp".equals(event.getSchema())) { + streamChangeHandler(event.getApp(), event.getStream(), event.getMediaServer().getId(), true); + } + } + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaDepartureEvent event) { + if ("rtsp".equals(event.getSchema())) { + streamChangeHandler(event.getApp(), event.getStream(), event.getMediaServer().getId(), false); + } + } + + /** + * 流未找到的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaNotFoundEvent event) { + if ("rtp".equals(event.getApp())) { + return; + } + // 拉流代理 + StreamProxy streamProxyByAppAndStream = getStreamProxyByAppAndStream(event.getApp(), event.getStream()); + if (streamProxyByAppAndStream != null && streamProxyByAppAndStream.isEnableDisableNoneReader()) { + startByAppAndStream(event.getApp(), event.getStream(), ((code, msg, data) -> { + log.info("[拉流代理] 自动点播成功, app: {}, stream: {}", event.getApp(), event.getStream()); + })); + } + } + + /** + * 流媒体节点上线 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaServerOnlineEvent event) { + zlmServerOnline(event.getMediaServer()); + } + + /** + * 流媒体节点离线 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaServerOfflineEvent event) { + zlmServerOffline(event.getMediaServer()); + } + + + @Override + @Transactional + public void add(StreamProxy streamProxy) { + StreamProxy streamProxyInDb = streamProxyMapper.selectOneByAppAndStream(streamProxy.getApp(), streamProxy.getStream()); + if (streamProxyInDb != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "APP+STREAM已经存在"); + } + if (streamProxy.getGbDeviceId() != null) { + gbChannelService.add(streamProxy.buildCommonGBChannel()); + } + streamProxy.setCreateTime(DateUtil.getNow()); + streamProxy.setUpdateTime(DateUtil.getNow()); + streamProxyMapper.add(streamProxy); + streamProxy.setDataType(ChannelDataType.STREAM_PROXY); + streamProxy.setDataDeviceId(streamProxy.getId()); + } + + @Override + public void delete(int id) { + StreamProxy streamProxy = getStreamProxy(id); + if (streamProxy == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "代理不存在"); + } + delete(streamProxy); + } + + private void delete(StreamProxy streamProxy) { + Assert.notNull(streamProxy, "代理不可为NULL"); + if (streamProxy.getPulling() != null && streamProxy.getPulling()) { + playService.stopProxy(streamProxy); + } + if (streamProxy.getGbId() > 0) { + gbChannelService.delete(streamProxy.getGbId()); + } + streamProxyMapper.delete(streamProxy.getId()); + } + + @Override + @Transactional + public void delteByAppAndStream(String app, String stream) { + StreamProxy streamProxy = streamProxyMapper.selectOneByAppAndStream(app, stream); + if (streamProxy == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "代理不存在"); + } + delete(streamProxy); + } + + /** + * 更新代理流 + */ + @Override + public boolean update(StreamProxy streamProxy) { + streamProxy.setUpdateTime(DateUtil.getNow()); + StreamProxy streamProxyInDb = streamProxyMapper.select(streamProxy.getId()); + if (streamProxyInDb == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "代理不存在"); + } + int updateResult = streamProxyMapper.update(streamProxy); + if (updateResult > 0 && !ObjectUtils.isEmpty(streamProxy.getGbDeviceId())) { + if (streamProxy.getGbId() > 0) { + gbChannelService.update(streamProxy.buildCommonGBChannel()); + } else { + gbChannelService.add(streamProxy.buildCommonGBChannel()); + } + } + return true; + } + + @Override + public PageInfo getAll(Integer page, Integer count, String query, Boolean pulling, String mediaServerId) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = streamProxyMapper.selectAll(query, pulling, mediaServerId); + return new PageInfo<>(all); + } + + + @Override + public void startByAppAndStream(String app, String stream, ErrorCallback callback) { + StreamProxy streamProxy = streamProxyMapper.selectOneByAppAndStream(app, stream); + if (streamProxy == null) { + throw new ControllerException(ErrorCode.ERROR404.getCode(), "代理信息未找到"); + } + playService.startProxy(streamProxy, callback); + } + + @Override + public void stopByAppAndStream(String app, String stream) { + StreamProxy streamProxy = streamProxyMapper.selectOneByAppAndStream(app, stream); + if (streamProxy == null) { + throw new ControllerException(ErrorCode.ERROR404.getCode(), "代理信息未找到"); + } + playService.stopProxy(streamProxy); + } + + + @Override + public Map getFFmpegCMDs(MediaServer mediaServer) { + return mediaServerService.getFFmpegCMDs(mediaServer); + } + + + @Override + public StreamProxy getStreamProxyByAppAndStream(String app, String stream) { + return streamProxyMapper.selectOneByAppAndStream(app, stream); + } + + @Override + @Transactional + public void zlmServerOnline(MediaServer mediaServer) { + if (mediaServer == null) { + return; + } + // 这里主要是控制数据库/redis缓存/以及zlm中存在的代理流 三者状态一致。以数据库中数据为根本 + redisCatchStorage.removeStream(mediaServer.getId(), "PULL"); + + List streamProxies = streamProxyMapper.selectForPushingInMediaServer(mediaServer.getId(), true); + if (streamProxies.isEmpty()) { + return; + } + Map streamProxyMapForDb = new HashMap<>(); + for (StreamProxy streamProxy : streamProxies) { + streamProxyMapForDb.put(streamProxy.getApp() + "_" + streamProxy.getStream(), streamProxy); + } + + List streamInfoList = mediaServerService.getMediaList(mediaServer, null, null, null); + + List channelListForOnline = new ArrayList<>(); + for (StreamInfo streamInfo : streamInfoList) { + String key = streamInfo.getApp() + streamInfo.getStream(); + StreamProxy streamProxy = streamProxyMapForDb.get(key); + if (streamProxy == null) { + // 流媒体存在,数据库中不存在 + continue; + } + if (streamInfo.getOriginType() == OriginType.PULL.ordinal() + || streamInfo.getOriginType() == OriginType.FFMPEG_PULL.ordinal()) { + if (streamProxyMapForDb.get(key) != null) { + redisCatchStorage.addStream(mediaServer, "pull", streamInfo.getApp(), streamInfo.getStream(), streamInfo.getMediaInfo()); + if ("OFF".equalsIgnoreCase(streamProxy.getGbStatus()) && streamProxy.getGbId() > 0) { + streamProxy.setGbStatus("ON"); + channelListForOnline.add(streamProxy.buildCommonGBChannel()); + } + streamProxyMapForDb.remove(key); + } + } + } + + if (!channelListForOnline.isEmpty()) { + gbChannelService.online(channelListForOnline); + } + List channelListForOffline = new ArrayList<>(); + List streamProxiesForRemove = new ArrayList<>(); + if (!streamProxyMapForDb.isEmpty()) { + for (StreamProxy streamProxy : streamProxyMapForDb.values()) { + if ("ON".equalsIgnoreCase(streamProxy.getGbStatus()) && streamProxy.getGbId() > 0) { + streamProxy.setGbStatus("OFF"); + channelListForOffline.add(streamProxy.buildCommonGBChannel()); + } + } + } + if (!channelListForOffline.isEmpty()) { + gbChannelService.offline(channelListForOffline); + } + if (!streamProxiesForRemove.isEmpty()) { + streamProxyMapper.deleteByList(streamProxiesForRemove); + } + + if (!streamProxyMapForDb.isEmpty()) { + for (StreamProxy streamProxy : streamProxyMapForDb.values()) { + streamProxyMapper.offline(streamProxy.getId()); + } + } + } + + @Override + public void zlmServerOffline(MediaServer mediaServer) { + List streamProxies = streamProxyMapper.selectForPushingInMediaServer(mediaServer.getId(), true); + + // 清理redis相关的缓存 + redisCatchStorage.removeStream(mediaServer.getId(), "PULL"); + + if (streamProxies.isEmpty()) { + return; + } + List streamProxiesForSendMessage = new ArrayList<>(); + List channelListForOffline = new ArrayList<>(); + + for (StreamProxy streamProxy : streamProxies) { + if (streamProxy.getGbId() > 0 && "ON".equalsIgnoreCase(streamProxy.getGbStatus())) { + channelListForOffline.add(streamProxy.buildCommonGBChannel()); + } + if ("ON".equalsIgnoreCase(streamProxy.getGbStatus())) { + streamProxiesForSendMessage.add(streamProxy); + } + } + if (!channelListForOffline.isEmpty()) { + // 修改国标关联的国标通道的状态 + gbChannelService.offline(channelListForOffline); + } + if (!streamProxiesForSendMessage.isEmpty()) { + for (StreamProxy streamProxy : streamProxiesForSendMessage) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("serverId", userSetting.getServerId()); + jsonObject.put("app", streamProxy.getApp()); + jsonObject.put("stream", streamProxy.getStream()); + jsonObject.put("register", false); + jsonObject.put("mediaServerId", mediaServer); + redisCatchStorage.sendStreamChangeMsg("pull", jsonObject); + } + } + } + + @Transactional + public void streamChangeHandler(String app, String stream, String mediaServerId, boolean status) { + // 状态变化时推送到国标上级 + StreamProxy streamProxy = streamProxyMapper.selectOneByAppAndStream(app, stream); + if (streamProxy == null) { + return; + } + streamProxy.setPulling(status); + streamProxy.setMediaServerId(mediaServerId); + streamProxy.setUpdateTime(DateUtil.getNow()); + streamProxyMapper.updateStream(streamProxy); + } + + @Override + public ResourceBaseInfo getOverview() { + + int total = streamProxyMapper.getAllCount(); + int online = streamProxyMapper.getOnline(); + + return new ResourceBaseInfo(total, online); + } + + @Override + public StreamProxy getStreamProxy(int id) { + return streamProxyMapper.select(id); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/bean/BatchRemoveParam.java b/src/main/java/com/genersoft/iot/vmp/streamPush/bean/BatchRemoveParam.java new file mode 100644 index 0000000..307ef54 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/bean/BatchRemoveParam.java @@ -0,0 +1,10 @@ +package com.genersoft.iot.vmp.streamPush.bean; + +import lombok.Data; + +import java.util.Set; + +@Data +public class BatchRemoveParam { + private Set ids; +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/bean/RedisPushStreamMessage.java b/src/main/java/com/genersoft/iot/vmp/streamPush/bean/RedisPushStreamMessage.java new file mode 100644 index 0000000..a459b97 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/bean/RedisPushStreamMessage.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.streamPush.bean; + +import lombok.Data; + +@Data +public class RedisPushStreamMessage { + + private String gbId; + private String app; + private String stream; + private String name; + private Boolean status; + // 终端所属的虚拟组织 + private String groupGbId; + // 终端所属的虚拟组织别名 可选,可作为地方同步组织结构到wvp时的关联关系 + private String groupAlias; + // 生产商 + private String manufacturer; + // 设备型号 + private String model; + // 摄像机类型 + private Integer ptzType; + + public StreamPush buildstreamPush() { + StreamPush push = new StreamPush(); + push.setApp(app); + push.setStream(stream); + push.setGbName(name); + push.setGbDeviceId(gbId); + push.setStartOfflinePush(true); + push.setGbManufacturer(manufacturer); + push.setGbModel(model); + push.setGbPtzType(ptzType); + if (status != null) { + push.setGbStatus(status?"ON":"OFF"); + } + push.setEnableBroadcast(0); + return push; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/bean/StreamPush.java b/src/main/java/com/genersoft/iot/vmp/streamPush/bean/StreamPush.java new file mode 100755 index 0000000..6e460d8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/bean/StreamPush.java @@ -0,0 +1,155 @@ +package com.genersoft.iot.vmp.streamPush.bean; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.springframework.util.ObjectUtils; + + +@Data +@Schema(description = "推流信息") +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +public class StreamPush extends CommonGBChannel implements Comparable{ + + /** + * id + */ + @Schema(description = "id") + private Integer id; + + /** + * 应用名 + */ + @Schema(description = "应用名") + private String app; + + /** + * 流id + */ + @Schema(description = "流id") + private String stream; + + /** + * 使用的流媒体ID + */ + @Schema(description = "使用的流媒体ID") + private String mediaServerId; + + /** + * 使用的服务ID + */ + @Schema(description = "使用的服务ID") + private String serverId; + + /** + * 推流时间 + */ + @Schema(description = "推流时间") + private String pushTime; + + /** + * 更新时间 + */ + @Schema(description = "更新时间") + private String updateTime; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + private String createTime; + + /** + * 是否正在推流 + */ + @Schema(description = "是否正在推流") + private boolean pushing; + + /** + * 拉起离线推流 + */ + @Schema(description = "拉起离线推流") + private boolean startOfflinePush; + + /** + * 速度,单位:km/h (可选) + */ + @Schema(description = "GPS的速度") + private Double gpsSpeed; + + /** + * 方向,取值为当前摄像头方向与正北方的顺时针夹角,取值范围0°~360°,单位:(°)(可选) + */ + @Schema(description = "GPS的方向") + private Double gpsDirection; + + /** + * 海拔高度,单位:m(可选) + */ + @Schema(description = "GPS的海拔高度") + private Double gpsAltitude; + + /** + * GPS的更新时间 + */ + @Schema(description = "GPS的更新时间") + private String gpsTime; + + private String uniqueKey; + + private Integer dataType = ChannelDataType.STREAM_PUSH; + + + @Override + public int compareTo(@NotNull StreamPush streamPushItem) { + return Long.valueOf(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(this.createTime) + - DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(streamPushItem.getCreateTime())).intValue(); + } + + public static StreamPush getInstance(StreamInfo streamInfo) { + StreamPush streamPush = new StreamPush(); + streamPush.setApp(streamInfo.getApp()); + if (streamInfo.getMediaServer() != null) { + streamPush.setMediaServerId(streamInfo.getMediaServer().getId()); + } + + streamPush.setStream(streamInfo.getStream()); + streamPush.setCreateTime(DateUtil.getNow()); + streamPush.setServerId(streamInfo.getServerId()); + return streamPush; + + } + + public static StreamPush getInstance(MediaArrivalEvent event, String serverId){ + StreamPush streamPushItem = new StreamPush(); + streamPushItem.setApp(event.getApp()); + streamPushItem.setMediaServerId(event.getMediaServer().getId()); + streamPushItem.setStream(event.getStream()); + streamPushItem.setCreateTime(DateUtil.getNow()); + streamPushItem.setServerId(serverId); + return streamPushItem; + } + + public CommonGBChannel buildCommonGBChannel() { + if (ObjectUtils.isEmpty(this.getGbDeviceId())) { + return null; + } + if (ObjectUtils.isEmpty(this.getGbName())) { + this.setGbName( app+ "-" +stream); + } + this.setDataType(ChannelDataType.STREAM_PUSH); + this.setDataDeviceId(this.getId()); + return this; + } + + +} + diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/bean/StreamPushExcelDto.java b/src/main/java/com/genersoft/iot/vmp/streamPush/bean/StreamPushExcelDto.java new file mode 100755 index 0000000..4ccc751 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/bean/StreamPushExcelDto.java @@ -0,0 +1,30 @@ +package com.genersoft.iot.vmp.streamPush.bean; + +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class StreamPushExcelDto { + + @ExcelProperty("名称") + private String name; + + @ExcelProperty("应用名") + private String app; + + @ExcelProperty("流ID") + private String stream; + + @ExcelProperty("国标ID") + private String gbDeviceId; + + @ExcelProperty("在线状态") + private boolean status; + + @Schema(description = "经度 WGS-84坐标系") + private Double longitude; + + @Schema(description = "纬度 WGS-84坐标系") + private Double latitude; +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/controller/StreamPushController.java b/src/main/java/com/genersoft/iot/vmp/streamPush/controller/StreamPushController.java new file mode 100755 index 0000000..136dd67 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/controller/StreamPushController.java @@ -0,0 +1,295 @@ +package com.genersoft.iot.vmp.streamPush.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelReader; +import com.alibaba.excel.exception.ExcelDataConvertException; +import com.alibaba.excel.read.metadata.ReadSheet; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.IMediaService; +import com.genersoft.iot.vmp.streamPush.bean.BatchRemoveParam; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.streamPush.bean.StreamPushExcelDto; +import com.genersoft.iot.vmp.streamPush.enent.StreamPushUploadFileHandler; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushPlayService; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Tag(name = "推流信息管理") +@RestController +@Slf4j +@RequestMapping(value = "/api/push") +public class StreamPushController { + + @Autowired + private IStreamPushService streamPushService; + + @Autowired + private IStreamPushPlayService streamPushPlayService; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private DeferredResultHolder resultHolder; + + @Autowired + private IMediaService mediaService; + + @Autowired + private UserSetting userSetting; + + @GetMapping(value = "/list") + @ResponseBody + @Operation(summary = "推流列表查询", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页") + @Parameter(name = "count", description = "每页查询数量") + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "pushing", description = "是否正在推流") + @Parameter(name = "mediaServerId", description = "流媒体ID") + public PageInfo list(@RequestParam(required = false)Integer page, + @RequestParam(required = false)Integer count, + @RequestParam(required = false)String query, + @RequestParam(required = false)Boolean pushing, + @RequestParam(required = false)String mediaServerId ){ + + if (ObjectUtils.isEmpty(query)) { + query = null; + } + if (ObjectUtils.isEmpty(mediaServerId)) { + mediaServerId = null; + } + PageInfo pushList = streamPushService.getPushList(page, count, query, pushing, mediaServerId); + return pushList; + } + + + @PostMapping(value = "/remove") + @ResponseBody + @Operation(summary = "删除", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "应用名", required = true) + public void delete(int id){ + if (streamPushService.delete(id) <= 0){ + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @PostMapping(value = "upload") + @ResponseBody + public DeferredResult>> uploadChannelFile(@RequestParam(value = "file") MultipartFile file){ + + // 最多处理文件一个小时 + DeferredResult>> result = new DeferredResult<>(60*60*1000L); + // 录像查询以channelId作为deviceId查询 + String key = DeferredResultHolder.UPLOAD_FILE_CHANNEL; + String uuid = UUID.randomUUID().toString(); + log.info("通道导入文件类型: {}",file.getContentType() ); + if (file.isEmpty()) { + log.warn("通道导入文件为空"); + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(-1); + wvpResult.setMsg("文件为空"); + result.setResult(ResponseEntity.status(HttpStatus.BAD_REQUEST).body(wvpResult)); + return result; + } + if (file.getContentType() == null) { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(-1); + wvpResult.setMsg("无法识别文件类型"); + result.setResult(ResponseEntity.status(HttpStatus.BAD_REQUEST).body(wvpResult)); + return result; + } + // 同时只处理一个文件 + if (resultHolder.exist(key, null)) { + log.warn("已有导入任务正在执行"); + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(-1); + wvpResult.setMsg("已有导入任务正在执行"); + result.setResult(ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(wvpResult)); + return result; + } + + resultHolder.put(key, uuid, result); + result.onTimeout(()->{ + log.warn("通道导入超时,可能文件过大"); + RequestMessage msg = new RequestMessage(); + msg.setKey(key); + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(-1); + wvpResult.setMsg("导入超时,可能文件过大"); + msg.setData(wvpResult); + resultHolder.invokeAllResult(msg); + }); + //获取文件流 + InputStream inputStream = null; + try { + String name = file.getName(); + inputStream = file.getInputStream(); + } catch (IOException e) { + log.error("未处理的异常 ", e); + } + try { + //传入参数 + ExcelReader excelReader = EasyExcel.read(inputStream, StreamPushExcelDto.class, + new StreamPushUploadFileHandler(streamPushService, mediaServerService.getDefaultMediaServer().getId(), (errorStreams, errorGBs)->{ + log.info("通道导入成功,存在重复App+Stream为{}个,存在国标ID为{}个", errorStreams.size(), errorGBs.size()); + RequestMessage msg = new RequestMessage(); + msg.setKey(key); + WVPResult>> wvpResult = new WVPResult<>(); + if (errorStreams.isEmpty() && errorGBs.isEmpty()) { + wvpResult.setCode(0); + wvpResult.setMsg("成功"); + }else { + wvpResult.setCode(1); + wvpResult.setMsg("导入成功。但是存在重复数据"); + Map> errorData = new HashMap<>(); + errorData.put("gbId", errorGBs); + errorData.put("stream", errorStreams); + wvpResult.setData(errorData); + } + msg.setData(wvpResult); + resultHolder.invokeAllResult(msg); + })).build(); + ReadSheet readSheet = EasyExcel.readSheet(0).build(); + excelReader.read(readSheet); + excelReader.finish(); + }catch (ExcelDataConvertException e) { + log.error("通道导入失败:行: {}, 列: {}, 内容: {}", e.getRowIndex(), e.getColumnIndex(), e.getCellData().getStringValue()); + RequestMessage msg = new RequestMessage(); + msg.setKey(key); + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("数据异常: " + e.getRowIndex() +"行" + e.getColumnIndex() + "列, 内容:" + e.getCellData().getStringValue() ); + msg.setData(wvpResult); + resultHolder.invokeAllResult(msg); + }catch (Exception e) { + log.warn("通道导入失败:", e); + RequestMessage msg = new RequestMessage(); + msg.setKey(key); + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("通道导入失败: " + e.getMessage() ); + msg.setData(wvpResult); + resultHolder.invokeAllResult(msg); + } + + + return result; + } + + /** + * 添加推流信息 + * @param stream 推流信息 + * @return + */ + @PostMapping(value = "/add") + @ResponseBody + @Operation(summary = "添加推流信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public StreamPush add(@RequestBody StreamPush stream){ + if (ObjectUtils.isEmpty(stream.getGbId())) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "国标ID不可为空"); + } + if (ObjectUtils.isEmpty(stream.getApp()) && ObjectUtils.isEmpty(stream.getStream())) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "app或stream不可为空"); + } + stream.setGbStatus("OFF"); + stream.setPushing(false); + if (!streamPushService.add(stream)) { + throw new ControllerException(ErrorCode.ERROR100); + } + stream.setDataType(ChannelDataType.STREAM_PUSH); + stream.setDataDeviceId(stream.getId()); + return stream; + } + + @PostMapping(value = "/update") + @ResponseBody + @Operation(summary = "更新推流信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public void update(@RequestBody StreamPush stream){ + if (ObjectUtils.isEmpty(stream.getId())) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "ID不可为空"); + } + if (!streamPushService.update(stream)) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @DeleteMapping(value = "/batchRemove") + @ResponseBody + @Operation(summary = "删除多个推流", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public void batchStop(@RequestBody BatchRemoveParam ids){ + if(ids.getIds().isEmpty()) { + return; + } + streamPushService.batchRemove(ids.getIds()); + } + + @GetMapping(value = "/start") + @ResponseBody + @Operation(summary = "开始播放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public DeferredResult> start(HttpServletRequest request, Integer id){ + Assert.notNull(id, "推流ID不可为NULL"); + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + result.onTimeout(()->{ + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100.getCode(), "等待推流超时"); + result.setResult(fail); + }); + streamPushPlayService.start(id, (code, msg, streamInfo) -> { + if (code == 0 && streamInfo != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + WVPResult success = WVPResult.success(new StreamContent(streamInfo)); + result.setResult(success); + } + }, null, null); + return result; + } + + @GetMapping(value = "/forceClose") + @ResponseBody + @Operation(summary = "强制停止推流", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public void stop(String app, String stream){ + + streamPushPlayService.stop(app, stream); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/dao/StreamPushMapper.java b/src/main/java/com/genersoft/iot/vmp/streamPush/dao/StreamPushMapper.java new file mode 100755 index 0000000..39b7d13 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/dao/StreamPushMapper.java @@ -0,0 +1,160 @@ +package com.genersoft.iot.vmp.streamPush.dao; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.service.bean.StreamPushItemFromRedis; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Mapper +@Repository +public interface StreamPushMapper { + + Integer dataType = ChannelDataType.GB28181; + + @Insert("INSERT INTO wvp_stream_push (app, stream, media_server_id, server_id, push_time, update_time, create_time, pushing, start_offline_push) VALUES" + + "(#{app}, #{stream}, #{mediaServerId} , #{serverId} , #{pushTime} ,#{updateTime}, #{createTime}, #{pushing}, #{startOfflinePush})") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int add(StreamPush streamPushItem); + + + @Update(value = {" "}) + int update(StreamPush streamPushItem); + + @Delete("DELETE FROM wvp_stream_push WHERE id=#{id}") + int del(@Param("id") int id); + + @Select(value = {" "}) + List selectAll(@Param("query") String query, @Param("pushing") Boolean pushing, @Param("mediaServerId") String mediaServerId); + + @Select("SELECT st.*, st.id as data_device_id, wdc.*, wdc.id as gb_id FROM wvp_stream_push st LEFT join wvp_device_channel wdc on wdc.data_type = 2 and st.id = wdc.data_device_id WHERE st.app=#{app} AND st.stream=#{stream}") + StreamPush selectByAppAndStream(@Param("app") String app, @Param("stream") String stream); + + @Insert("") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int addAll(List streamPushItems); + + @Select("SELECT st.*, st.id as data_device_id, wdc.*, wdc.id as gb_id FROM wvp_stream_push st LEFT join wvp_device_channel wdc on wdc.data_type = 2 and st.id = wdc.data_device_id WHERE st.media_server_id=#{mediaServerId}") + List selectAllByMediaServerId(String mediaServerId); + + @Select("SELECT st.*, st.id as data_device_id, wdc.*, wdc.id as gb_id FROM wvp_stream_push st LEFT join wvp_device_channel wdc on wdc.data_type = 2 and st.id = wdc.data_device_id WHERE st.media_server_id=#{mediaServerId} and wdc.gb_device_id is null") + List selectAllByMediaServerIdWithOutGbID(String mediaServerId); + + @Update("UPDATE wvp_stream_push " + + "SET pushing=#{pushing}, server_id=#{serverId}, media_server_id=#{mediaServerId} " + + "WHERE id=#{id}") + int updatePushStatus(StreamPush streamPush); + + @Select("") + List getListInList(List offlineStreams); + + + @Select("SELECT CONCAT(app,stream) from wvp_stream_push") + List getAllAppAndStream(); + + @Select("select count(1) from wvp_stream_push ") + int getAllCount(); + + @Select(value = {" "}) + int getAllPushing(Boolean usePushingAsStatus); + + @MapKey("uniqueKey") + @Select("SELECT CONCAT(wsp.app, wsp.stream) as unique_key, wsp.*, wdc.* , " + + " wdc.id as gb_id " + + " from wvp_stream_push wsp " + + " LEFT join wvp_device_channel wdc on wdc.data_type = 2 and wsp.id = wdc.data_device_id") + Map getAllAppAndStreamMap(); + + + @MapKey("gbDeviceId") + @Select("SELECT wdc.gb_device_id, wsp.id as data_device_id, wsp.*, wsp.* , wdc.id as gb_id " + + " from wvp_stream_push wsp " + + " LEFT join wvp_device_channel wdc on wdc.data_type = 2 and wsp.id = wdc.data_device_id") + Map getAllGBId(); + + @Select("SELECT st.*, st.id as data_device_id, wdc.*, wdc.id as gb_id FROM wvp_stream_push st LEFT join wvp_device_channel wdc on wdc.data_type = 2 and st.id = wdc.data_device_id WHERE st.id=#{id}") + StreamPush queryOne(@Param("id") int id); + + @Select("") + List selectInSet(Set ids); + + @Delete("") + void batchDel(List streamPushList); + + + @Update({""}) + int batchUpdate(List streamPushItemForUpdate); +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/enent/StreamPushUploadFileHandler.java b/src/main/java/com/genersoft/iot/vmp/streamPush/enent/StreamPushUploadFileHandler.java new file mode 100755 index 0000000..9ccc8c8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/enent/StreamPushUploadFileHandler.java @@ -0,0 +1,140 @@ +package com.genersoft.iot.vmp.streamPush.enent; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.streamPush.bean.StreamPushExcelDto; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import org.springframework.util.ObjectUtils; + +import java.util.*; + +public class StreamPushUploadFileHandler extends AnalysisEventListener { + + /** + * 错误数据的回调,用于将错误数据发送给页面 + */ + private final ErrorDataHandler errorDataHandler; + + /** + * 推流的业务类用于存储数据 + */ + private final IStreamPushService pushService; + + /** + * 默认流媒体节点ID + */ + private final String defaultMediaServerId; + + /** + * 用于存储更具APP+Stream过滤后的数据,可以直接存入stream_push表与gb_stream表 + */ + private final Map streamPushItemForSave = new HashMap<>(); + + /** + * 用于存储APP+Stream->国标ID 的数据结构, 数据一一对应,全局判断APP+Stream->国标ID是否存在不对应 + */ + private final BiMap gBMap = HashBiMap.create(); + + /** + * 用于存储APP+Stream-> 在数据库中的数据 + */ + private final BiMap pushMapInDb = HashBiMap.create(); + + /** + * 记录错误的APP+Stream + */ + private final List errorStreamList = new ArrayList<>(); + + + /** + * 记录错误的国标ID + */ + private final List errorInfoList = new ArrayList<>(); + + /** + * 读取数量计数器 + */ + private int loadedSize = 0; + + public StreamPushUploadFileHandler(IStreamPushService pushService, String defaultMediaServerId, ErrorDataHandler errorDataHandler) { + this.pushService = pushService; + this.defaultMediaServerId = defaultMediaServerId; + this.errorDataHandler = errorDataHandler; + // 获取数据库已有的数据,已经存在的则忽略 + List allAppAndStreams = pushService.getAllAppAndStream(); + if (!allAppAndStreams.isEmpty()) { + for (String allAppAndStream : allAppAndStreams) { + pushMapInDb.put(allAppAndStream, allAppAndStream); + } + } + } + + public interface ErrorDataHandler{ + void handle(List streams, List gbId); + } + + @Override + public void invoke(StreamPushExcelDto streamPushExcelDto, AnalysisContext analysisContext) { + if (ObjectUtils.isEmpty(streamPushExcelDto.getApp()) + || ObjectUtils.isEmpty(streamPushExcelDto.getStream()) + || ObjectUtils.isEmpty(streamPushExcelDto.getGbDeviceId())) { + return; + } + Integer rowIndex = analysisContext.readRowHolder().getRowIndex(); + + if (gBMap.get(streamPushExcelDto.getApp() + streamPushExcelDto.getStream()) == null) { + try { + gBMap.put(streamPushExcelDto.getApp() + streamPushExcelDto.getStream(), streamPushExcelDto.getGbDeviceId()); + }catch (IllegalArgumentException e) { + errorInfoList.add("行:" + rowIndex + ", " + streamPushExcelDto.getGbDeviceId() + " 国标ID重复使用"); + return; + } + }else { + if (!gBMap.get(streamPushExcelDto.getApp() + streamPushExcelDto.getStream()).equals(streamPushExcelDto.getGbDeviceId())) { + errorInfoList.add("行:" + rowIndex + ", " + streamPushExcelDto.getGbDeviceId() + " 同样的应用名和流ID使用了不同的国标ID"); + return; + } + } + + StreamPush streamPush = new StreamPush(); + streamPush.setApp(streamPushExcelDto.getApp()); + streamPush.setStream(streamPushExcelDto.getStream()); + streamPush.setGbDeviceId(streamPushExcelDto.getGbDeviceId()); + streamPush.setGbStatus(streamPushExcelDto.isStatus()?"ON":"OFF"); + streamPush.setCreateTime(DateUtil.getNow()); + streamPush.setMediaServerId(defaultMediaServerId); + streamPush.setGbName(streamPushExcelDto.getName()); + streamPush.setGbLongitude(streamPushExcelDto.getLongitude()); + streamPush.setGbLatitude(streamPushExcelDto.getLatitude()); + streamPush.setUpdateTime(DateUtil.getNow()); + streamPushItemForSave.put(streamPush.getApp() + streamPush.getStream(), streamPush); + + loadedSize ++; + if (loadedSize > 1000) { + saveData(); + streamPushItemForSave.clear(); + loadedSize = 0; + } + + } + + @Override + public void doAfterAllAnalysed(AnalysisContext analysisContext) { + // 这里也要保存数据,确保最后遗留的数据也存储到数据库 + saveData(); + streamPushItemForSave.clear(); + gBMap.clear(); + errorDataHandler.handle(errorStreamList, errorInfoList); + } + + private void saveData(){ + if (!streamPushItemForSave.isEmpty()) { + // 向数据库查询是否存在重复的app + pushService.batchAdd(new ArrayList<>(streamPushItemForSave.values())); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/service/IStreamPushPlayService.java b/src/main/java/com/genersoft/iot/vmp/streamPush/service/IStreamPushPlayService.java new file mode 100644 index 0000000..657e5cb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/service/IStreamPushPlayService.java @@ -0,0 +1,12 @@ +package com.genersoft.iot.vmp.streamPush.service; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +public interface IStreamPushPlayService { + void start(Integer id, ErrorCallback callback, String platformDeviceId, String platformName ); + + void stop(String app, String stream); + + void stop(Integer id); +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/service/IStreamPushService.java b/src/main/java/com/genersoft/iot/vmp/streamPush/service/IStreamPushService.java new file mode 100755 index 0000000..9621d8c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/service/IStreamPushService.java @@ -0,0 +1,100 @@ +package com.genersoft.iot.vmp.streamPush.service; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.StreamPushItemFromRedis; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo; +import com.github.pagehelper.PageInfo; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author lin + */ +public interface IStreamPushService { + + /** + * 获取 + */ + PageInfo getPushList(Integer page, Integer count, String query, Boolean pushing, String mediaServerId); + + List getPushList(String mediaSererId); + + StreamPush getPush(String app, String streamId); + + boolean stop(StreamPush streamPush); + + /** + * 停止一路推流 + * @param app 应用名 + * @param stream 流ID + */ + boolean stopByAppAndStream(String app, String stream); + + /** + * 新的节点加入 + */ + void zlmServerOnline(MediaServer mediaServer); + + /** + * 节点离线 + */ + void zlmServerOffline(MediaServer mediaServer); + + /** + * 批量添加 + */ + void batchAdd(List streamPushExcelDtoList); + + + /** + * 全部离线 + */ + void allOffline(); + + /** + * 推流离线 + */ + void offline(List offlineStreams); + + /** + * 推流上线 + */ + void online(List onlineStreams); + + /** + * 增加推流 + */ + boolean add(StreamPush stream); + + boolean update(StreamPush stream); + + /** + * 获取全部的app+Streanm 用于判断推流列表是新增还是修改 + * @return + */ + List getAllAppAndStream(); + + /** + * 获取统计信息 + * @return + */ + ResourceBaseInfo getOverview(); + + Map getAllAppAndStreamMap(); + + Map getAllGBId(); + + void deleteByAppAndStream(String app, String stream); + + void updatePushStatus(StreamPush streamPush); + + void batchUpdate(List streamPushItemForUpdate); + + int delete(int id); + + void batchRemove(Set ids); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/SourcePlayServiceForStreamPushImpl.java b/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/SourcePlayServiceForStreamPushImpl.java new file mode 100644 index 0000000..5197687 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/SourcePlayServiceForStreamPushImpl.java @@ -0,0 +1,53 @@ +package com.genersoft.iot.vmp.streamPush.service.impl; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.PlayException; +import com.genersoft.iot.vmp.gb28181.service.ISourcePlayService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushPlayService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sip.message.Response; + +@Slf4j +@Service(ChannelDataType.PLAY_SERVICE + ChannelDataType.STREAM_PUSH) +public class SourcePlayServiceForStreamPushImpl implements ISourcePlayService { + + @Autowired + private IStreamPushPlayService playService; + + @Override + public void play(CommonGBChannel channel, Platform platform, Boolean record, ErrorCallback callback) { + String serverGBId = null; + String platformName = null; + if (platform != null) { + // 推流 + serverGBId = platform.getServerGBId(); + platformName = platform.getName(); + } + // 推流 + try { + playService.start(channel.getDataDeviceId(), callback, serverGBId, platformName); + }catch (PlayException e) { + callback.run(e.getCode(), e.getMsg(), null); + }catch (Exception e) { + log.error("[点播推流通道失败] 通道: {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + callback.run(Response.BUSY_HERE, "busy here", null); + } + } + + @Override + public void stopPlay(CommonGBChannel channel) { + // 推流 + try { + playService.stop(channel.getDataDeviceId()); + }catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushPlayServiceImpl.java new file mode 100644 index 0000000..d8adfa3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushPlayServiceImpl.java @@ -0,0 +1,143 @@ +package com.genersoft.iot.vmp.streamPush.service.impl; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.MessageForPushChannel; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcService; +import com.genersoft.iot.vmp.service.redisMsg.RedisPushStreamResponseListener; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.streamPush.dao.StreamPushMapper; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushPlayService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import java.util.UUID; + +@Service +@Slf4j +public class StreamPushPlayServiceImpl implements IStreamPushPlayService { + + @Autowired + private StreamPushMapper streamPushMapper; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private UserSetting userSetting; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private IRedisRpcService redisRpcService; + + @Autowired + private IRedisRpcPlayService redisRpcPlayService; + + @Autowired + private RedisPushStreamResponseListener redisPushStreamResponseListener; + + @Override + public void start(Integer id, ErrorCallback callback, String platformDeviceId, String platformName ) { + StreamPush streamPush = streamPushMapper.queryOne(id); + Assert.notNull(streamPush, "推流信息未找到"); + + if (streamPush.isPushing() && !userSetting.getServerId().equals(streamPush.getServerId())) { + redisRpcPlayService.playPush(streamPush.getServerId(), id, callback); + return; + } + + MediaServer mediaServer = mediaServerService.getOne(streamPush.getMediaServerId()); + if (mediaServer != null) { + MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, streamPush.getApp(), streamPush.getStream()); + if (mediaInfo != null) { + String callId = null; + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(streamPush.getApp(), streamPush.getStream()); + if (streamAuthorityInfo != null) { + callId = streamAuthorityInfo.getCallId(); + } + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), mediaServerService.getStreamInfoByAppAndStream(mediaServer, + streamPush.getApp(), streamPush.getStream(), mediaInfo, callId)); + if (!streamPush.isPushing()) { + streamPush.setPushing(true); + streamPushMapper.update(streamPush); + } + return; + } + } + Assert.isTrue(streamPush.isStartOfflinePush(), "通道未推流"); + // 发送redis消息以使设备上线,流上线后被 + log.info("[ app={}, stream={} ]通道未推流,发送redis信息控制设备开始推流", streamPush.getApp(), streamPush.getStream()); + MessageForPushChannel messageForPushChannel = MessageForPushChannel.getInstance(1, + streamPush.getApp(), streamPush.getStream(), streamPush.getGbDeviceId(), platformDeviceId, + platformName, userSetting.getServerId(), null); + redisCatchStorage.sendStreamPushRequestedMsg(messageForPushChannel); + // 设置超时 + String timeOutTaskKey = UUID.randomUUID().toString(); + dynamicTask.startDelay(timeOutTaskKey, () -> { + redisRpcService.unPushStreamOnlineEvent(streamPush.getApp(), streamPush.getStream()); + log.info("[ app={}, stream={} ] 等待设备开始推流超时", streamPush.getApp(), streamPush.getStream()); + callback.run(ErrorCode.ERROR100.getCode(), "timeout", null); + + }, userSetting.getPlatformPlayTimeout()); + // + long key = redisRpcService.onStreamOnlineEvent(streamPush.getApp(), streamPush.getStream(), (streamInfo) -> { + dynamicTask.stop(timeOutTaskKey); + if (streamInfo == null) { + log.warn("等待推流得到结果未空: {}/{}", streamPush.getApp(), streamPush.getStream()); + callback.run(ErrorCode.ERROR100.getCode(), "fail", null); + }else { + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo); + } + }); + // 添加回复的拒绝或者错误的通知 + // redis消息例如: PUBLISH VM_MSG_STREAM_PUSH_RESPONSE '{"code":1,"msg":"失败","app":"1","stream":"2"}' + redisPushStreamResponseListener.addEvent(streamPush.getApp(), streamPush.getStream(), response -> { + if (response.getCode() != 0) { + dynamicTask.stop(timeOutTaskKey); + redisRpcService.unPushStreamOnlineEvent(streamPush.getApp(), streamPush.getStream()); + redisRpcService.removeCallback(key); + callback.run(response.getCode(), response.getMsg(), null); + } + }); + } + + @Override + public void stop(String app, String stream) { + StreamPush streamPush = streamPushMapper.selectByAppAndStream(app, stream); + if (streamPush == null || !streamPush.isPushing()) { + return; + } + String mediaServerId = streamPush.getMediaServerId(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + Assert.notNull(mediaServer, "未找到使用的节点"); + mediaServerService.closeStreams(mediaServer, app, stream); + } + + @Override + public void stop(Integer id) { + StreamPush streamPush = streamPushMapper.queryOne(id); + if (streamPush == null || !streamPush.isPushing()) { + return; + } + String mediaServerId = streamPush.getMediaServerId(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + Assert.notNull(mediaServer, "未找到使用的节点"); + mediaServerService.closeStreams(mediaServer, streamPush.getApp(), streamPush.getStream()); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushServiceImpl.java new file mode 100755 index 0000000..a33d7ca --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushServiceImpl.java @@ -0,0 +1,600 @@ +package com.genersoft.iot.vmp.streamPush.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerOfflineEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerOnlineEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OriginType; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.bean.StreamPushItemFromRedis; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.streamPush.dao.StreamPushMapper; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import java.util.*; + +@Service +@Slf4j +public class StreamPushServiceImpl implements IStreamPushService { + + @Autowired + private StreamPushMapper streamPushMapper; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private UserSetting userSetting; + + @Autowired + + private IMediaServerService mediaServerService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private IGbChannelService gbChannelService; + + /** + * 流到来的处理 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaArrivalEvent event) { + MediaInfo mediaInfo = event.getMediaInfo(); + if (mediaInfo == null) { + return; + } + if (mediaInfo.getOriginType() != OriginType.RTMP_PUSH.ordinal() + && mediaInfo.getOriginType() != OriginType.RTSP_PUSH.ordinal() + && mediaInfo.getOriginType() != OriginType.RTC_PUSH.ordinal()) { + return; + } + + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(event.getApp(), event.getStream()); + if (streamAuthorityInfo == null) { + streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(event); + } else { + streamAuthorityInfo.setOriginType(mediaInfo.getOriginType()); + } + redisCatchStorage.updateStreamAuthorityInfo(event.getApp(), event.getStream(), streamAuthorityInfo); + + StreamPush streamPushInDb = getPush(event.getApp(), event.getStream()); + if (streamPushInDb == null) { + StreamPush streamPush = StreamPush.getInstance(event, userSetting.getServerId()); + streamPush.setPushing(true); + streamPush.setServerId(userSetting.getServerId()); + streamPush.setUpdateTime(DateUtil.getNow()); + streamPush.setPushTime(DateUtil.getNow()); + add(streamPush); + }else { + streamPushInDb.setPushTime(DateUtil.getNow()); + streamPushInDb.setPushing(true); + streamPushInDb.setServerId(userSetting.getServerId()); + streamPushInDb.setMediaServerId(mediaInfo.getMediaServer().getId()); + updatePushStatus(streamPushInDb); + } + // 冗余数据,自己系统中自用 + if (!"broadcast".equals(event.getApp()) && !"talk".equals(event.getApp())) { + redisCatchStorage.addPushListItem(event.getApp(), event.getStream(), event.getMediaInfo()); + } + + // 发送流变化redis消息 + JSONObject jsonObject = new JSONObject(); + jsonObject.put("serverId", userSetting.getServerId()); + jsonObject.put("app", event.getApp()); + jsonObject.put("stream", event.getStream()); + jsonObject.put("register", true); + jsonObject.put("mediaServerId", event.getMediaServer().getId()); + redisCatchStorage.sendStreamChangeMsg(OriginType.values()[event.getMediaInfo().getOriginType()].getType(), jsonObject); + } + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaDepartureEvent event) { + + // 兼容流注销时类型从redis记录获取 + MediaInfo mediaInfo = redisCatchStorage.getPushListItem(event.getApp(), event.getStream()); + + if (mediaInfo != null) { + log.info("[推流信息] 查询到redis存在推流缓存, 开始清理,{}/{}", event.getApp(), event.getStream()); + String type = OriginType.values()[mediaInfo.getOriginType()].getType(); + // 冗余数据,自己系统中自用 + redisCatchStorage.removePushListItem(event.getApp(), event.getStream(), event.getMediaServer().getId()); + // 发送流变化redis消息 + JSONObject jsonObject = new JSONObject(); + jsonObject.put("serverId", userSetting.getServerId()); + jsonObject.put("app", event.getApp()); + jsonObject.put("stream", event.getStream()); + jsonObject.put("register", false); + jsonObject.put("mediaServerId", event.getMediaServer().getId()); + redisCatchStorage.sendStreamChangeMsg(type, jsonObject); + } + StreamPush streamPush = getPush(event.getApp(), event.getStream()); + if (streamPush == null) { + return; + } + if (streamPush.getGbDeviceId() != null) { + streamPush.setPushing(false); + updatePushStatus(streamPush); + }else { + deleteByAppAndStream(event.getApp(), event.getStream()); + } + } + + /** + * 流媒体节点上线 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaServerOnlineEvent event) { + zlmServerOnline(event.getMediaServer()); + } + + /** + * 流媒体节点离线 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaServerOfflineEvent event) { + zlmServerOffline(event.getMediaServer()); + } + + @Override + public PageInfo getPushList(Integer page, Integer count, String query, Boolean pushing, String mediaServerId) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = streamPushMapper.selectAll(query, pushing, mediaServerId); + return new PageInfo<>(all); + } + + @Override + public List getPushList(String mediaServerId) { + return streamPushMapper.selectAllByMediaServerIdWithOutGbID(mediaServerId); + } + + + @Override + public StreamPush getPush(String app, String stream) { + return streamPushMapper.selectByAppAndStream(app, stream); + } + + @Override + @Transactional + public boolean add(StreamPush stream) { + log.info("[添加推流] app: {}, stream: {}, 国标编号: {}", stream.getApp(), stream.getStream(), stream.getGbDeviceId()); + StreamPush streamPushInDb = streamPushMapper.selectByAppAndStream(stream.getApp(), stream.getStream()); + if (streamPushInDb != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "应用名+流ID已存在"); + } + stream.setUpdateTime(DateUtil.getNow()); + stream.setCreateTime(DateUtil.getNow()); + int addResult = streamPushMapper.add(stream); + if (addResult <= 0) { + return false; + } + if (ObjectUtils.isEmpty(stream.getGbDeviceId())) { + return true; + } + CommonGBChannel channel = gbChannelService.queryByDeviceId(stream.getGbDeviceId()); + if (channel != null) { + log.info("[添加推流]失败,国标编号已存在: {} app: {}, stream: {}, ", stream.getGbDeviceId(), stream.getApp(), stream.getStream()); + } + int addChannelResult = gbChannelService.add(stream.buildCommonGBChannel()); + return addChannelResult > 0; + } + + @Override + @Transactional + public void deleteByAppAndStream(String app, String stream) { + log.info("[删除推流] app: {}, stream: {}, ", app, stream); + StreamPush streamPush = streamPushMapper.selectByAppAndStream(app, stream); + if (streamPush == null) { + log.info("[删除推流]失败, 不存在 app: {}, stream: {}, ", app, stream); + return; + } + if (streamPush.isPushing()) { + stop(streamPush); + } + if (streamPush.getGbId() > 0) { + gbChannelService.delete(streamPush.getGbId()); + } + streamPushMapper.del(streamPush.getId()); + } + @Override + @Transactional + public boolean update(StreamPush streamPush) { + Assert.notNull(streamPush, "推流信息不可为NULL"); + Assert.isTrue(streamPush.getId() > 0, "推流信息ID必须存在"); + log.info("[更新推流]:id: {}, app: {}, stream: {}, ", streamPush.getId(), streamPush.getApp(), streamPush.getStream()); + StreamPush streamPushInDb = streamPushMapper.queryOne(streamPush.getId()); + if (!streamPushInDb.getApp().equals(streamPush.getApp()) || !streamPushInDb.getStream().equals(streamPush.getStream())) { + // app或者stream变化 + StreamPush streamPushInDbForAppAndStream = streamPushMapper.selectByAppAndStream(streamPush.getApp(), streamPush.getStream()); + if (streamPushInDbForAppAndStream != null && !streamPushInDbForAppAndStream.getId().equals(streamPush.getId())) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "应用名+流ID已存在"); + } + } + streamPush.setUpdateTime(DateUtil.getNow()); + streamPushMapper.update(streamPush); + if (streamPush.getGbId() > 0) { + gbChannelService.update(streamPush.buildCommonGBChannel()); + } + return true; + } + + + @Override + @Transactional + public boolean stop(StreamPush streamPush) { + log.info("[主动停止推流] id: {}, app: {}, stream: {}, ", streamPush.getId(), streamPush.getApp(), streamPush.getStream()); + MediaServer mediaServer = null; + if (streamPush.getMediaServerId() == null) { + log.info("[主动停止推流]未找到使用MediaServer,开始自动检索 id: {}, app: {}, stream: {}, ", streamPush.getId(), streamPush.getApp(), streamPush.getStream()); + mediaServer = mediaServerService.getMediaServerByAppAndStream(streamPush.getApp(), streamPush.getStream()); + if (mediaServer != null) { + log.info("[主动停止推流] 检索到MediaServer为{}, id: {}, app: {}, stream: {}, ", mediaServer.getId(), streamPush.getId(), streamPush.getApp(), streamPush.getStream()); + }else { + log.info("[主动停止推流]未找到使用MediaServer id: {}, app: {}, stream: {}, ", streamPush.getId(), streamPush.getApp(), streamPush.getStream()); + } + }else { + mediaServer = mediaServerService.getOne(streamPush.getMediaServerId()); + if (mediaServer == null) { + log.info("[主动停止推流]未找到使用的MediaServer: {},开始自动检索 id: {}, app: {}, stream: {}, ",streamPush.getMediaServerId(), streamPush.getId(), streamPush.getApp(), streamPush.getStream()); + mediaServer = mediaServerService.getMediaServerByAppAndStream(streamPush.getApp(), streamPush.getStream()); + if (mediaServer != null) { + log.info("[主动停止推流] 检索到MediaServer为{}, id: {}, app: {}, stream: {}, ", mediaServer.getId(), streamPush.getId(), streamPush.getApp(), streamPush.getStream()); + }else { + log.info("[主动停止推流]未找到使用MediaServer id: {}, app: {}, stream: {}, ", streamPush.getId(), streamPush.getApp(), streamPush.getStream()); + } + } + } + if (mediaServer != null) { + mediaServerService.closeStreams(mediaServer, streamPush.getApp(), streamPush.getStream()); + } + streamPush.setPushing(false); + if (userSetting.getUsePushingAsStatus()) { + CommonGBChannel commonGBChannel = streamPush.buildCommonGBChannel(); + if (commonGBChannel != null) { + gbChannelService.offline(commonGBChannel); + } + } + sendRtpServerService.deleteByStream(streamPush.getStream()); + mediaServerService.stopSendRtp(mediaServer, streamPush.getApp(), streamPush.getStream(), null); + streamPush.setUpdateTime(DateUtil.getNow()); + streamPushMapper.update(streamPush); + return true; + } + + @Override + @Transactional + public boolean stopByAppAndStream(String app, String stream) { + log.info("[主动停止推流] : app: {}, stream: {}, ", app, stream); + StreamPush streamPushItem = streamPushMapper.selectByAppAndStream(app, stream); + if (streamPushItem != null) { + stop(streamPushItem); + } + return true; + } + + @Override + @Transactional + public void zlmServerOnline(MediaServer mediaServer) { + // 同步zlm推流信息 + if (mediaServer == null) { + return; + } + // 数据库记录 + List pushList = getPushList(mediaServer.getId()); + Map pushItemMap = new HashMap<>(); + // redis记录 + List mediaInfoList = redisCatchStorage.getStreams(mediaServer.getId(), "PUSH"); + Map streamInfoPushItemMap = new HashMap<>(); + if (!pushList.isEmpty()) { + for (StreamPush streamPushItem : pushList) { + if (ObjectUtils.isEmpty(streamPushItem.getGbId())) { + pushItemMap.put(streamPushItem.getApp() + streamPushItem.getStream(), streamPushItem); + } + } + } + if (!mediaInfoList.isEmpty()) { + for (MediaInfo mediaInfo : mediaInfoList) { + if (mediaInfo == null) { + continue; + } + streamInfoPushItemMap.put(mediaInfo.getApp() + mediaInfo.getStream(), mediaInfo); + } + } + // 获取所有推流鉴权信息,清理过期的 + List allStreamAuthorityInfo = redisCatchStorage.getAllStreamAuthorityInfo(); + Map streamAuthorityInfoInfoMap = new HashMap<>(); + for (StreamAuthorityInfo streamAuthorityInfo : allStreamAuthorityInfo) { + streamAuthorityInfoInfoMap.put(streamAuthorityInfo.getApp() + streamAuthorityInfo.getStream(), streamAuthorityInfo); + } + List mediaList = mediaServerService.getMediaList(mediaServer, null, null, null); + if (mediaList == null) { + return; + } + List streamPushItems = handleJSON(mediaList); + if (streamPushItems != null) { + for (StreamPush streamPushItem : streamPushItems) { + pushItemMap.remove(streamPushItem.getApp() + streamPushItem.getStream()); + streamInfoPushItemMap.remove(streamPushItem.getApp() + streamPushItem.getStream()); + streamAuthorityInfoInfoMap.remove(streamPushItem.getApp() + streamPushItem.getStream()); + } + } + List changedStreamPushList = new ArrayList<>(pushItemMap.values()); + if (!changedStreamPushList.isEmpty()) { + for (StreamPush streamPush : changedStreamPushList) { + stop(streamPush); + } + } + + Collection mediaInfos = streamInfoPushItemMap.values(); + if (!mediaInfos.isEmpty()) { + String type = "PUSH"; + for (MediaInfo mediaInfo : mediaInfos) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("serverId", userSetting.getServerId()); + jsonObject.put("app", mediaInfo.getApp()); + jsonObject.put("stream", mediaInfo.getStream()); + jsonObject.put("register", false); + jsonObject.put("mediaServerId", mediaServer.getId()); + redisCatchStorage.sendStreamChangeMsg(type, jsonObject); + // 移除redis内流的信息 + redisCatchStorage.removeStream(mediaServer.getId(), "PUSH", mediaInfo.getApp(), mediaInfo.getStream()); + // 冗余数据,自己系统中自用 + redisCatchStorage.removePushListItem(mediaInfo.getApp(), mediaInfo.getStream(), mediaServer.getId()); + } + } + + Collection streamAuthorityInfos = streamAuthorityInfoInfoMap.values(); + if (!streamAuthorityInfos.isEmpty()) { + for (StreamAuthorityInfo streamAuthorityInfo : streamAuthorityInfos) { + // 移除redis内流的信息 + redisCatchStorage.removeStreamAuthorityInfo(streamAuthorityInfo.getApp(), streamAuthorityInfo.getStream()); + } + } + } + + @Override + @Transactional + public void zlmServerOffline(MediaServer mediaServer) { + List streamPushItems = streamPushMapper.selectAllByMediaServerId(mediaServer.getId()); + if (!streamPushItems.isEmpty()) { + for (StreamPush streamPushItem : streamPushItems) { + stop(streamPushItem); + } + } +// // 移除没有GBId的推流 +// streamPushMapper.deleteWithoutGBId(mediaServerId); +// // 其他的流设置未启用 +// streamPushMapper.updateStatusByMediaServerId(mediaServerId, false); +// streamProxyMapper.updateStatusByMediaServerId(mediaServerId, false); + // 发送流停止消息 + String type = "PUSH"; + // 发送redis消息 + List mediaInfoList = redisCatchStorage.getStreams(mediaServer.getId(), type); + if (!mediaInfoList.isEmpty()) { + for (MediaInfo mediaInfo : mediaInfoList) { + // 移除redis内流的信息 + redisCatchStorage.removeStream(mediaServer.getId(), type, mediaInfo.getApp(), mediaInfo.getStream()); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("serverId", userSetting.getServerId()); + jsonObject.put("app", mediaInfo.getApp()); + jsonObject.put("stream", mediaInfo.getStream()); + jsonObject.put("register", false); + jsonObject.put("mediaServerId", mediaServer.getId()); + redisCatchStorage.sendStreamChangeMsg(type, jsonObject); + + // 冗余数据,自己系统中自用 + redisCatchStorage.removePushListItem(mediaInfo.getApp(), mediaInfo.getStream(), mediaServer.getId()); + } + } + } + + @Override + @Transactional + public void batchAdd(List streamPushItems) { + streamPushMapper.addAll(streamPushItems); + List commonGBChannels = new ArrayList<>(); + for (StreamPush streamPush : streamPushItems) { + if (!ObjectUtils.isEmpty(streamPush.getGbDeviceId())) { + commonGBChannels.add(streamPush.buildCommonGBChannel()); + } + } + gbChannelService.batchAdd(commonGBChannels); + } + + @Override + public void allOffline() { + List streamPushList = streamPushMapper.selectAll(null, null, null); + if (streamPushList.isEmpty()) { + return; + } + List commonGBChannelList = new ArrayList<>(); + for (StreamPush streamPush : streamPushList) { + CommonGBChannel commonGBChannel = streamPush.buildCommonGBChannel(); + if (commonGBChannel != null) { + commonGBChannelList.add(streamPush.buildCommonGBChannel()); + } + } + gbChannelService.offline(commonGBChannelList); + } + + @Override + public void offline(List offlineStreams) { + // 更新部分设备离线 + List streamPushList = streamPushMapper.getListInList(offlineStreams); + if (streamPushList.isEmpty()) { + log.info("[推流设备] 设备离线操作未发现可操作数据。"); + return; + } + List commonGBChannelList = gbChannelService.queryListByStreamPushList(streamPushList); + gbChannelService.offline(commonGBChannelList); + } + + @Override + public void online(List onlineStreams) { + if (onlineStreams.isEmpty()) { + log.info("[设备上线] 推流设备列表为空"); + return; + } + // 更新部分设备上线streamPushService + List streamPushList = streamPushMapper.getListInList(onlineStreams); + if (streamPushList.isEmpty()) { + for (StreamPushItemFromRedis onlineStream : onlineStreams) { + log.info("[设备上线] 未查询到这些通道: {}/{}", onlineStream.getApp(), onlineStream.getStream()); + } + return; + } + List commonGBChannelList = gbChannelService.queryListByStreamPushList(streamPushList); + gbChannelService.online(commonGBChannelList); + } + + @Override + public List getAllAppAndStream() { + return streamPushMapper.getAllAppAndStream(); + } + + @Override + public ResourceBaseInfo getOverview() { + int total = streamPushMapper.getAllCount(); + int online = streamPushMapper.getAllPushing(userSetting.getUsePushingAsStatus()); + + return new ResourceBaseInfo(total, online); + } + + @Override + public Map getAllAppAndStreamMap() { + return streamPushMapper.getAllAppAndStreamMap(); + } + + @Override + public Map getAllGBId() { + return streamPushMapper.getAllGBId(); + } + + @Override + @Transactional + public void updatePushStatus(StreamPush streamPush) { + if (userSetting.getUsePushingAsStatus()) { + streamPush.setGbStatus(streamPush.isPushing()?"ON":"OFF"); + } + streamPushMapper.updatePushStatus(streamPush); + if (ObjectUtils.isEmpty(streamPush.getGbDeviceId())) { + return; + } + if (userSetting.getUsePushingAsStatus()) { + if ("ON".equalsIgnoreCase(streamPush.getGbStatus()) ) { + gbChannelService.online(streamPush.buildCommonGBChannel()); + }else { + gbChannelService.offline(streamPush.buildCommonGBChannel()); + } + } + } + + private List handleJSON(List streamInfoList) { + if (streamInfoList == null || streamInfoList.isEmpty()) { + return null; + } + Map result = new HashMap<>(); + for (StreamInfo streamInfo : streamInfoList) { + // 不保存国标推理以及拉流代理的流 + if (streamInfo.getOriginType() == OriginType.RTSP_PUSH.ordinal() + || streamInfo.getOriginType() == OriginType.RTMP_PUSH.ordinal() + || streamInfo.getOriginType() == OriginType.RTC_PUSH.ordinal() ) { + String key = streamInfo.getApp() + "_" + streamInfo.getStream(); + StreamPush streamPushItem = result.get(key); + if (streamPushItem == null) { + streamPushItem = StreamPush.getInstance(streamInfo); + result.put(key, streamPushItem); + } + } + } + return new ArrayList<>(result.values()); + } + + @Override + @Transactional + public void batchUpdate(List streamPushItemForUpdate) { + streamPushMapper.batchUpdate(streamPushItemForUpdate); + List commonGBChannels = new ArrayList<>(); + for (StreamPush streamPush : streamPushItemForUpdate) { + if (!ObjectUtils.isEmpty(streamPush.getGbDeviceId())) { + commonGBChannels.add(streamPush.buildCommonGBChannel()); + } + } + gbChannelService.batchUpdate(commonGBChannels); + } + + @Override + @Transactional + public int delete(int id) { + StreamPush streamPush = streamPushMapper.queryOne(id); + if (streamPush == null) { + return 0; + } + if(streamPush.isPushing()) { + MediaServer mediaServer = mediaServerService.getOne(streamPush.getMediaServerId()); + mediaServerService.closeStreams(mediaServer, streamPush.getApp(), streamPush.getStream()); + } + if (streamPush.getGbDeviceId() != null) { + gbChannelService.delete(streamPush.getGbId()); + } + return streamPushMapper.del(id); + } + + @Override + @Transactional + public void batchRemove(Set ids) { + List streamPushList = streamPushMapper.selectInSet(ids); + if (streamPushList.isEmpty()) { + return; + } + Set channelIds = new HashSet<>(); + streamPushList.stream().forEach(streamPush -> { + if (streamPush.getGbDeviceId() != null) { + channelIds.add(streamPush.getGbId()); + } + }); + streamPushMapper.batchDel(streamPushList); + gbChannelService.delete(channelIds); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/CivilCodeUtil.java b/src/main/java/com/genersoft/iot/vmp/utils/CivilCodeUtil.java new file mode 100644 index 0000000..33c3e47 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/CivilCodeUtil.java @@ -0,0 +1,103 @@ +package com.genersoft.iot.vmp.utils; + +import com.genersoft.iot.vmp.common.CivilCodePo; +import com.genersoft.iot.vmp.gb28181.bean.Region; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.ObjectUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +public enum CivilCodeUtil { + + INSTANCE; + // 用与消息的缓存 + private final Map civilCodeMap = new ConcurrentHashMap<>(); + + CivilCodeUtil() { + } + + public void add(List civilCodePoList) { + if (!civilCodePoList.isEmpty()) { + for (CivilCodePo civilCodePo : civilCodePoList) { + civilCodeMap.put(civilCodePo.getCode(), civilCodePo); + } + } + } + + public void add(CivilCodePo civilCodePo) { + civilCodeMap.put(civilCodePo.getCode(), civilCodePo); + } + + public CivilCodePo get(String code) { + return civilCodeMap.get(code); + } + + public CivilCodePo getParentCode(String code) { + if (code.length() > 8) { + return null; + } + if (code.length() == 8) { + String parentCode = code.substring(0, 6); + return civilCodeMap.get(parentCode); + }else { + CivilCodePo civilCodePo = civilCodeMap.get(code); + if (civilCodePo == null){ + return null; + } + String parentCode = civilCodePo.getParentCode(); + if (parentCode == null) { + return null; + } + return civilCodeMap.get(parentCode); + } + } + + public CivilCodePo getCivilCodePo(String code) { + if (code.length() > 8) { + return null; + }else { + return civilCodeMap.get(code); + } + } + + public List getAllParentCode(String civilCode) { + List civilCodePoList = new ArrayList<>(); + CivilCodePo parentCode = getParentCode(civilCode); + if (parentCode != null) { + civilCodePoList.add(parentCode); + List allParentCode = getAllParentCode(parentCode.getCode()); + if (!allParentCode.isEmpty()) { + civilCodePoList.addAll(allParentCode); + }else { + return civilCodePoList; + } + } + return civilCodePoList; + } + + public boolean isEmpty() { + return civilCodeMap.isEmpty(); + } + + public int size() { + return civilCodeMap.size(); + } + + public List getAllChild(String parent) { + List result = new ArrayList<>(); + for (String key : civilCodeMap.keySet()) { + if (parent == null) { + if (ObjectUtils.isEmpty(civilCodeMap.get(key).getParentCode().trim())) { + result.add(Region.getInstance(key, civilCodeMap.get(key).getName(), civilCodeMap.get(key).getParentCode())); + } + }else if (civilCodeMap.get(key).getParentCode().equals(parent)) { + result.add(Region.getInstance(key, civilCodeMap.get(key).getName(), civilCodeMap.get(key).getParentCode())); + } + } + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/Coordtransform.java b/src/main/java/com/genersoft/iot/vmp/utils/Coordtransform.java new file mode 100755 index 0000000..c10357c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/Coordtransform.java @@ -0,0 +1,126 @@ +package com.genersoft.iot.vmp.utils; + +/** + * 坐标转换 + * 一个提供了百度坐标(BD09)、国测局坐标(火星坐标,GCJ02)、和WGS84坐标系之间的转换的工具类 + * 参考https://github.com/wandergis/coordtransform 写的Java版本 + * @author Xinconan + * @date 2016-03-18 + * @url https://github.com/xinconan/coordtransform + */ +public class Coordtransform { + + private static double x_PI = 3.14159265358979324 * 3000.0 / 180.0; + private static double PI = 3.1415926535897932384626; + private static double a = 6378245.0; + private static double ee = 0.00669342162296594323; + + /** + * 百度坐标系 (BD-09) 与 火星坐标系 (GCJ-02)的转换 + * 即 百度 转 谷歌、高德 + * @param bd_lon + * @param bd_lat + * @return Double[lon,lat] + */ + public static Double[] BD09ToGCJ02(Double bd_lon,Double bd_lat){ + double x = bd_lon - 0.0065; + double y = bd_lat - 0.006; + double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_PI); + double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_PI); + Double[] arr = new Double[2]; + arr[0] = z * Math.cos(theta); + arr[1] = z * Math.sin(theta); + return arr; + } + + /** + * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换 + * 即谷歌、高德 转 百度 + * @param gcj_lon + * @param gcj_lat + * @return Double[lon,lat] + */ + public static Double[] GCJ02ToBD09(Double gcj_lon,Double gcj_lat){ + double z = Math.sqrt(gcj_lon * gcj_lon + gcj_lat * gcj_lat) + 0.00002 * Math.sin(gcj_lat * x_PI); + double theta = Math.atan2(gcj_lat, gcj_lon) + 0.000003 * Math.cos(gcj_lon * x_PI); + Double[] arr = new Double[2]; + arr[0] = z * Math.cos(theta) + 0.0065; + arr[1] = z * Math.sin(theta) + 0.006; + return arr; + } + + /** + * WGS84转GCJ02 + * @param wgs_lon + * @param wgs_lat + * @return Double[lon,lat] + */ + public static Double[] WGS84ToGCJ02(Double wgs_lon,Double wgs_lat){ + if(outOfChina(wgs_lon, wgs_lat)){ + return new Double[]{wgs_lon,wgs_lat}; + } + double dlat = transformlat(wgs_lon - 105.0, wgs_lat - 35.0); + double dlng = transformlng(wgs_lon - 105.0, wgs_lat - 35.0); + double radlat = wgs_lat / 180.0 * PI; + double magic = Math.sin(radlat); + magic = 1 - ee * magic * magic; + double sqrtmagic = Math.sqrt(magic); + dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI); + dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI); + Double[] arr = new Double[2]; + arr[0] = wgs_lon + dlng; + arr[1] = wgs_lat + dlat; + return arr; + } + + /** + * GCJ02转WGS84 + * @param gcj_lon + * @param gcj_lat + * @return Double[lon,lat] + */ + public static Double[] GCJ02ToWGS84(Double gcj_lon,Double gcj_lat){ + if(outOfChina(gcj_lon, gcj_lat)){ + return new Double[]{gcj_lon,gcj_lat}; + } + double dlat = transformlat(gcj_lon - 105.0, gcj_lat - 35.0); + double dlng = transformlng(gcj_lon - 105.0, gcj_lat - 35.0); + double radlat = gcj_lat / 180.0 * PI; + double magic = Math.sin(radlat); + magic = 1 - ee * magic * magic; + double sqrtmagic = Math.sqrt(magic); + dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI); + dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI); + double mglat = gcj_lat + dlat; + double mglng = gcj_lon + dlng; + return new Double[]{gcj_lon * 2 - mglng, gcj_lat * 2 - mglat}; + } + + private static Double transformlat(double lng, double lat) { + double ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng)); + ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0; + ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0; + return ret; + } + + private static Double transformlng(double lng,double lat) { + double ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng)); + ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0; + ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0; + return ret; + } + + /** + * outOfChina + * @描述: 判断是否在国内,不在国内则不做偏移 + * @param lng + * @param lat + * @return {boolean} + */ + private static boolean outOfChina(Double lng,Double lat) { + return (lng < 72.004 || lng > 137.8347) || ((lat < 0.8293 || lat > 55.8271) || false); + }; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/DateUtil.java b/src/main/java/com/genersoft/iot/vmp/utils/DateUtil.java new file mode 100755 index 0000000..41a0310 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/DateUtil.java @@ -0,0 +1,229 @@ +package com.genersoft.iot.vmp.utils; + + +import jakarta.validation.constraints.NotNull; +import org.apache.commons.lang3.ObjectUtils; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAccessor; +import java.util.Locale; + +/** + * 全局时间工具类 + * @author lin + */ +public class DateUtil { + + /** + * 兼容不规范的iso8601时间格式 + */ + private static final String ISO8601_COMPATIBLE_PATTERN = "yyyy-M-d'T'H:m:s"; + + /** + * 用以输出标准的iso8601时间格式 + */ + private static final String ISO8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss"; + + /** + * iso8601时间格式带时区,例如:2024-02-21T11:10:36+08:00 + */ + private static final String ISO8601_ZONE_PATTERN = "yyyy-MM-dd'T'HH:mm:ssXXX"; + + /** + * 兼容的时间格式 iso8601时间格式带毫秒 + */ + private static final String ISO8601_MILLISECOND_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS"; + + /** + * wvp内部统一时间格式 + */ + public static final String PATTERN = "yyyy-MM-dd HH:mm:ss"; + + /** + * wvp内部统一时间格式 + */ + public static final String URL_PATTERN = "yyyyMMddHHmmss"; + public static final String PATTERN1078 = "yyMMddHHmmss"; + public static final String PATTERN1078Date = "yyyyMMdd"; + + /** + * 日期格式 + */ + public static final String date_PATTERN = "yyyy-MM-dd"; + + public static final String zoneStr = "Asia/Shanghai"; + + public static final DateTimeFormatter formatterCompatibleISO8601 = DateTimeFormatter.ofPattern(ISO8601_COMPATIBLE_PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + public static final DateTimeFormatter formatterISO8601 = DateTimeFormatter.ofPattern(ISO8601_PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + public static final DateTimeFormatter formatterZoneISO8601 = DateTimeFormatter.ofPattern(ISO8601_ZONE_PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + public static final DateTimeFormatter formatterMillisecondISO8601 = DateTimeFormatter.ofPattern(ISO8601_MILLISECOND_PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + + public static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + public static final DateTimeFormatter DateFormatter = DateTimeFormatter.ofPattern(date_PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + public static final DateTimeFormatter urlFormatter = DateTimeFormatter.ofPattern(URL_PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + public static final DateTimeFormatter formatter1078 = DateTimeFormatter.ofPattern(PATTERN1078, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + public static final DateTimeFormatter formatter1078date = DateTimeFormatter.ofPattern(PATTERN1078Date, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + + public static String yyyy_MM_dd_HH_mm_ssToISO8601(@NotNull String formatTime) { + return formatterISO8601.format(formatter.parse(formatTime)); + } + + public static String yyyy_MM_dd_HH_mm_ssToUrl(@NotNull String formatTime) { + return urlFormatter.format(formatter.parse(formatTime)); + } + + public static String ISO8601Toyyyy_MM_dd_HH_mm_ss(String formatTime) { + // 三种日期格式都尝试,为了兼容不同厂家的日期格式 + if (verification(formatTime, formatterCompatibleISO8601)) { + return formatter.format(formatterCompatibleISO8601.parse(formatTime)); + } else if (verification(formatTime, formatterZoneISO8601)) { + return formatter.format(formatterZoneISO8601.parse(formatTime)); + } else if (verification(formatTime, formatterMillisecondISO8601)) { + return formatter.format(formatterMillisecondISO8601.parse(formatTime)); + } + return formatter.format(formatterISO8601.parse(formatTime)); + } + + public static String urlToyyyy_MM_dd_HH_mm_ss(String formatTime) { + return formatter.format(urlFormatter.parse(formatTime)); + } + public static String yyyy_MM_dd_HH_mm_ssTo1078(String formatTime) { + return formatter1078.format(formatter.parse(formatTime)); + } + public static String jt1078Toyyyy_MM_dd_HH_mm_ss(String formatTime) { + return formatter.format(formatter1078.parse(formatTime)); + } + public static String jt1078dateToyyyy_MM_dd(String formatTime) { + return DateFormatter.format(formatter1078date.parse(formatTime)); + } + + /** + * yyyy_MM_dd_HH_mm_ss 转时间戳 + * @param formatTime + * @return + */ + public static long yyyy_MM_dd_HH_mm_ssToTimestamp(String formatTime) { + TemporalAccessor temporalAccessor = formatter.parse(formatTime); + Instant instant = Instant.from(temporalAccessor); + return instant.getEpochSecond(); + } + + /** + * 时间戳 转 yyyy_MM_dd_HH_mm_ss + */ + public static String timestampTo_yyyy_MM_dd_HH_mm_ss(long timestamp) { + Instant instant = Instant.ofEpochSecond(timestamp); + return formatter.format(LocalDateTime.ofInstant(instant, ZoneId.of(zoneStr))); + } + + /** + * 时间戳 转 yyyy_MM_dd_HH_mm_ss + */ + public static String timestampMsToUrlToyyyy_MM_dd_HH_mm_ss(long timestamp) { + Instant instant = Instant.ofEpochMilli(timestamp); + return urlFormatter.format(LocalDateTime.ofInstant(instant, ZoneId.of(zoneStr))); + } + + /** + * yyyy_MM_dd_HH_mm_ss 转时间戳(毫秒) + * + * @param formatTime + * @return + */ + public static long yyyy_MM_dd_HH_mm_ssToTimestampMs(String formatTime) { + TemporalAccessor temporalAccessor = formatter.parse(formatTime); + Instant instant = Instant.from(temporalAccessor); + return instant.toEpochMilli(); + } + + /** + * 时间戳(毫秒) 转 yyyy_MM_dd_HH_mm_ss + */ + public static String timestampMsTo_yyyy_MM_dd_HH_mm_ss(long timestamp) { + Instant instant = Instant.ofEpochMilli(timestamp); + return formatter.format(LocalDateTime.ofInstant(instant, ZoneId.of(zoneStr))); + } + + /** + * yyyy_MM_dd_HH_mm_ss 转时间戳(毫秒) + * + * @param formatTime + * @return + */ + public static long urlToTimestampMs(String formatTime) { + TemporalAccessor temporalAccessor = urlFormatter.parse(formatTime); + Instant instant = Instant.from(temporalAccessor); + return instant.toEpochMilli(); + } + + /** + * 时间戳 转 yyyy_MM_dd + */ + public static String timestampTo_yyyy_MM_dd(long timestamp) { + Instant instant = Instant.ofEpochMilli(timestamp); + return DateFormatter.format(LocalDateTime.ofInstant(instant, ZoneId.of(zoneStr))); + } + + /** + * 获取当前时间 + * @return + */ + public static String getNow() { + LocalDateTime nowDateTime = LocalDateTime.now(); + return formatter.format(nowDateTime); + } + + /** + * 获取当前时间 + * @return + */ + public static String getNowForUrl() { + LocalDateTime nowDateTime = LocalDateTime.now(); + return urlFormatter.format(nowDateTime); + } + + + /** + * 格式校验 + * @param timeStr 时间字符串 + * @param dateTimeFormatter 待校验的格式 + * @return + */ + public static boolean verification(String timeStr, DateTimeFormatter dateTimeFormatter) { + try { + LocalDate.parse(timeStr, dateTimeFormatter); + return true; + }catch (DateTimeParseException exception) { + return false; + } + } + + public static String getNowForISO8601() { + LocalDateTime nowDateTime = LocalDateTime.now(); + return formatterISO8601.format(nowDateTime); + } + + public static long getDifferenceForNow(String keepaliveTime) { + if (ObjectUtils.isEmpty(keepaliveTime)) { + return 0; + } + Instant beforeInstant = Instant.from(formatter.parse(keepaliveTime)); + return ChronoUnit.MILLIS.between(beforeInstant, Instant.now()); + } + + public static long getDifference(String startTime, String endTime) { + if (ObjectUtils.isEmpty(startTime) || ObjectUtils.isEmpty(endTime)) { + return 0; + } + Instant startInstant = Instant.from(formatter.parse(startTime)); + Instant endInstant = Instant.from(formatter.parse(endTime)); + return ChronoUnit.MILLIS.between(startInstant, endInstant); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/GitUtil.java b/src/main/java/com/genersoft/iot/vmp/utils/GitUtil.java new file mode 100755 index 0000000..ca637dd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/GitUtil.java @@ -0,0 +1,59 @@ +package com.genersoft.iot.vmp.utils; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +/** + * 一个优秀的颓废程序猿(CSDN) + */ +@Component +@PropertySource(value = {"classpath:git.properties" }, ignoreResourceNotFound = true) +public class GitUtil { + + @Value("${git.branch:}") + private String branch; + @Value("${git.commit.id:}") + private String gitCommitId; + @Value("${git.remote.origin.url:}") + private String gitUrl; + @Value("${git.build.time:}") + private String buildDate; + + @Value("${git.build.version:}") + private String buildVersion; + + @Value("${git.commit.id.abbrev:}") + private String commitIdShort; + + @Value("${git.commit.time:}") + private String commitTime; + + public String getGitCommitId() { + return gitCommitId; + } + + public String getBranch() { + return branch; + } + + public String getGitUrl() { + return gitUrl; + } + + public String getBuildDate() { + return buildDate; + } + + public String getCommitIdShort() { + return commitIdShort; + } + + public String getBuildVersion() { + return buildVersion; + } + + public String getCommitTime() { + return commitTime; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/GpsUtil.java b/src/main/java/com/genersoft/iot/vmp/utils/GpsUtil.java new file mode 100755 index 0000000..1af61c7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/GpsUtil.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.utils; + +import com.genersoft.iot.vmp.gb28181.bean.BaiduPoint; +import lombok.extern.slf4j.Slf4j; + +import java.util.Base64; + +@Slf4j +public class GpsUtil { + + + public static BaiduPoint Wgs84ToBd09(String xx, String yy) { + double lng = Double.parseDouble(xx); + double lat = Double.parseDouble(yy); + Double[] gcj02 = Coordtransform.WGS84ToGCJ02(lng, lat); + Double[] doubles = Coordtransform.GCJ02ToBD09(gcj02[0], gcj02[1]); + BaiduPoint bdPoint= new BaiduPoint(); + bdPoint.setBdLng(doubles[0] + ""); + bdPoint.setBdLat(doubles[1] + ""); + return bdPoint; + } + + /** + * BASE64解码 + * @param str + * @return string + */ + public static byte[] decode(String str) { + byte[] bt = null; + final Base64.Decoder decoder = Base64.getDecoder(); + bt = decoder.decode(str); // .decodeBuffer(str); + return bt; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/HttpUtils.java b/src/main/java/com/genersoft/iot/vmp/utils/HttpUtils.java new file mode 100644 index 0000000..407a813 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/HttpUtils.java @@ -0,0 +1,51 @@ +package com.genersoft.iot.vmp.utils; + +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipOutputStream; + +@Slf4j +public class HttpUtils { + + public static boolean downLoadFile(String url, ZipOutputStream zos) { + OkHttpClient client = new OkHttpClient(); + Request request = new Request.Builder() + .url(url) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + log.error("下载失败,HTTP 状态码: {}, URL: {}", response.code(), url); + return false; + } + + // 获取响应体的输入流 + InputStream inputStream = null; + if (response.body() != null) { + inputStream = response.body().byteStream(); + } + if (inputStream == null) { + log.error("响应体为空,无法下载文件: {}", url); + return false; + } + + // 将输入流写入zip文件 + byte[] buffer = new byte[8192]; // 8KB 缓冲区,提高性能 + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + zos.write(buffer, 0, bytesRead); + } + + log.debug("成功下载文件: {}, 大小: {} bytes", url, response.body().contentLength()); + return true; + } catch (IOException e) { + log.error("下载过程中出错: {}, URL: {}", e.getMessage(), url); + return false; + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/IpPortUtil.java b/src/main/java/com/genersoft/iot/vmp/utils/IpPortUtil.java new file mode 100644 index 0000000..2ce0a55 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/IpPortUtil.java @@ -0,0 +1,90 @@ +package com.genersoft.iot.vmp.utils; + +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class IpPortUtil { + + /** + * 拼接IP和端口 + * @param ip IP地址字符串 + * @param port 端口号字符串 + * @return 拼接后的字符串 + * @throws IllegalArgumentException 如果IP地址无效或端口无效 + */ + public static String concatenateIpAndPort(String ip, String port) { + if (port == null || port.isEmpty()) { + throw new IllegalArgumentException("端口号不能为空"); + } + + // 验证端口是否为有效数字 + try { + int portNum = Integer.parseInt(port); + if (portNum < 0 || portNum > 65535) { + throw new IllegalArgumentException("端口号必须在0-65535范围内"); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("端口号必须是有效数字", e); + } + + try { + InetAddress inetAddress = InetAddress.getByName(ip); + + if (inetAddress instanceof Inet6Address) { + // IPv6地址需要加上方括号 + return "[" + ip + "]:" + port; + } else { + // IPv4地址直接拼接 + return ip + ":" + port; + } + } catch (UnknownHostException e) { + throw new IllegalArgumentException("无效的IP地址: " + ip, e); + } + } + + // 测试用例 + public static void main(String[] args) { + // IPv4测试 + String ipv4 = "192.168.1.1"; + String port1 = "8080"; + System.out.println(concatenateIpAndPort(ipv4, port1)); // 输出: 192.168.1.1:8080 + + // IPv6测试 + String ipv6 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; + String port2 = "80"; + System.out.println(concatenateIpAndPort(ipv6, port2)); // 输出: [2001:0db8:85a3:0000:0000:8a2e:0370:7334]:80 + + // 压缩格式IPv6测试 + String ipv6Compressed = "2001:db8::1"; + System.out.println(concatenateIpAndPort(ipv6Compressed, port2)); // 输出: [2001:db8::1]:80 + + // 无效IP测试 + try { + System.out.println(concatenateIpAndPort("invalid.ip", "1234")); + } catch (IllegalArgumentException e) { + System.out.println("捕获到预期异常: " + e.getMessage()); + } + + // 无效端口测试 - 非数字 + try { + System.out.println(concatenateIpAndPort(ipv4, "abc")); + } catch (IllegalArgumentException e) { + System.out.println("捕获到预期异常: " + e.getMessage()); + } + + // 无效端口测试 - 超出范围 + try { + System.out.println(concatenateIpAndPort(ipv4, "70000")); + } catch (IllegalArgumentException e) { + System.out.println("捕获到预期异常: " + e.getMessage()); + } + + // 空端口测试 + try { + System.out.println(concatenateIpAndPort(ipv4, "")); + } catch (IllegalArgumentException e) { + System.out.println("捕获到预期异常: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/genersoft/iot/vmp/utils/JsonUtil.java b/src/main/java/com/genersoft/iot/vmp/utils/JsonUtil.java new file mode 100755 index 0000000..b44af10 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/JsonUtil.java @@ -0,0 +1,45 @@ +package com.genersoft.iot.vmp.utils; + +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.Objects; + +/** + * JsonUtil + * + * @author KunLong-Luo + * @version 1.0.0 + * @since 2023/2/2 15:24 + */ +public final class JsonUtil { + + private JsonUtil() { + } + + /** + * safe json type conversion + * + * @param key redis key + * @param clazz cast type + * @param + * @return result type + */ + public static T redisJsonToObject(RedisTemplate redisTemplate, String key, Class clazz) { + Object jsonObject = redisTemplate.opsForValue().get(key); + if (Objects.isNull(jsonObject)) { + return null; + } + return clazz.cast(jsonObject); + } + + public static T redisHashJsonToObject(RedisTemplate redisTemplate, String key, String objKey, Class clazz) { +// if (key == null || objKey == null) { +// return null; +// } + Object jsonObject = redisTemplate.opsForHash().get(key, objKey); + if (Objects.isNull(jsonObject)) { + return null; + } + return clazz.cast(jsonObject); + } +} \ No newline at end of file diff --git a/src/main/java/com/genersoft/iot/vmp/utils/MediaServerUtils.java b/src/main/java/com/genersoft/iot/vmp/utils/MediaServerUtils.java new file mode 100644 index 0000000..bb57547 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/MediaServerUtils.java @@ -0,0 +1,26 @@ +package com.genersoft.iot.vmp.utils; + +import org.springframework.util.ObjectUtils; + +import java.util.HashMap; +import java.util.Map; + +public class MediaServerUtils { + public static Map urlParamToMap(String params) { + HashMap map = new HashMap<>(); + if (ObjectUtils.isEmpty(params)) { + return map; + } + String[] paramsArray = params.split("&"); + if (paramsArray.length == 0) { + return map; + } + for (String param : paramsArray) { + String[] paramArray = param.split("="); + if (paramArray.length == 2) { + map.put(paramArray[0], paramArray[1]); + } + } + return map; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/SSLSocketClientUtil.java b/src/main/java/com/genersoft/iot/vmp/utils/SSLSocketClientUtil.java new file mode 100644 index 0000000..c85b163 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/SSLSocketClientUtil.java @@ -0,0 +1,53 @@ +package com.genersoft.iot.vmp.utils; + +import javax.net.ssl.*; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +public class SSLSocketClientUtil { + public static SSLSocketFactory getSocketFactory(TrustManager manager) { + SSLSocketFactory socketFactory = null; + try { + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, new TrustManager[]{manager}, new SecureRandom()); + socketFactory = sslContext.getSocketFactory(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (KeyManagementException e) { + e.printStackTrace(); + } + return socketFactory; + } + + public static X509TrustManager getX509TrustManager() { + return new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + } + + public static HostnameVerifier getHostnameVerifier() { + HostnameVerifier hostnameVerifier = new HostnameVerifier() { + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } + }; + return hostnameVerifier; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/SpringBeanFactory.java b/src/main/java/com/genersoft/iot/vmp/utils/SpringBeanFactory.java new file mode 100755 index 0000000..1806524 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/SpringBeanFactory.java @@ -0,0 +1,50 @@ +package com.genersoft.iot.vmp.utils; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * @description:spring bean获取工厂,获取spring中的已初始化的bean + * @author: swwheihei + * @date: 2019年6月25日 下午4:51:52 + * + */ +@Component +public class SpringBeanFactory implements ApplicationContextAware { + + // Spring应用上下文环境 + private static ApplicationContext applicationContext; + + /** + * 实现ApplicationContextAware接口的回调方法,设置上下文环境 + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + SpringBeanFactory.applicationContext = applicationContext; + } + + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + + /** + * 获取对象 这里重写了bean方法,起主要作用 + */ + public static T getBean(String beanId) throws BeansException { + if (applicationContext == null) { + return null; + } + return (T) applicationContext.getBean(beanId); + } + + /** + * 获取当前环境 + */ + public static String getActiveProfile() { + return applicationContext.getEnvironment().getActiveProfiles()[0]; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/SystemInfoUtils.java b/src/main/java/com/genersoft/iot/vmp/utils/SystemInfoUtils.java new file mode 100755 index 0000000..b389d69 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/SystemInfoUtils.java @@ -0,0 +1,162 @@ +package com.genersoft.iot.vmp.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.DigestUtils; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.hardware.NetworkIF; +import oshi.software.os.OperatingSystem; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * 实现参考自xiaozhangnomoney原创文章, + * 版权声明:本文为xiaozhangnomoney原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明 + * 原文出处链接:https://blog.csdn.net/xiaozhangnomoney/article/details/107769147 + */ +@Slf4j +public class SystemInfoUtils { + + /** + * 获取cpu信息 + * @return + * @throws InterruptedException + */ + public static double getCpuInfo() throws InterruptedException { + SystemInfo systemInfo = new SystemInfo(); + CentralProcessor processor = systemInfo.getHardware().getProcessor(); + long[] prevTicks = processor.getSystemCpuLoadTicks(); + // 睡眠1s + TimeUnit.SECONDS.sleep(1); + long[] ticks = processor.getSystemCpuLoadTicks(); + long nice = ticks[CentralProcessor.TickType.NICE.getIndex()] - prevTicks[CentralProcessor.TickType.NICE.getIndex()]; + long irq = ticks[CentralProcessor.TickType.IRQ.getIndex()] - prevTicks[CentralProcessor.TickType.IRQ.getIndex()]; + long softirq = ticks[CentralProcessor.TickType.SOFTIRQ.getIndex()] - prevTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()]; + long steal = ticks[CentralProcessor.TickType.STEAL.getIndex()] - prevTicks[CentralProcessor.TickType.STEAL.getIndex()]; + long cSys = ticks[CentralProcessor.TickType.SYSTEM.getIndex()] - prevTicks[CentralProcessor.TickType.SYSTEM.getIndex()]; + long user = ticks[CentralProcessor.TickType.USER.getIndex()] - prevTicks[CentralProcessor.TickType.USER.getIndex()]; + long iowait = ticks[CentralProcessor.TickType.IOWAIT.getIndex()] - prevTicks[CentralProcessor.TickType.IOWAIT.getIndex()]; + long idle = ticks[CentralProcessor.TickType.IDLE.getIndex()] - prevTicks[CentralProcessor.TickType.IDLE.getIndex()]; + long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal; + return 1.0-(idle * 1.0 / totalCpu); + } + + /** + * 获取内存使用率 + * @return + */ + public static double getMemInfo(){ + SystemInfo systemInfo = new SystemInfo(); + GlobalMemory memory = systemInfo.getHardware().getMemory(); + //总内存 + long totalByte = memory.getTotal(); + //剩余 + long acaliableByte = memory.getAvailable(); + return (totalByte-acaliableByte)*1.0/totalByte; + } + + /** + * 获取网络上传和下载 + * @return + */ + public static Map getNetworkInterfaces() { + SystemInfo si = new SystemInfo(); + HardwareAbstractionLayer hal = si.getHardware(); + List beforeRecvNetworkIFs = hal.getNetworkIFs(); + NetworkIF beforeBet= beforeRecvNetworkIFs.get(beforeRecvNetworkIFs.size() - 1); + long beforeRecv = beforeBet.getBytesRecv(); + long beforeSend = beforeBet.getBytesSent(); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + log.error("[线程休眠失败] : {}", e.getMessage()); + } + List afterNetworkIFs = hal.getNetworkIFs(); + NetworkIF afterNet = afterNetworkIFs.get(afterNetworkIFs.size() - 1); + + HashMap map = new HashMap<>(); + // 速度单位: Mbps + map.put("in",formatUnits(afterNet.getBytesRecv()-beforeRecv, 1048576L)); + map.put("out",formatUnits(afterNet.getBytesSent()-beforeSend, 1048576L)); + return map; + } + + /** + * 获取带宽总值 + * @return + */ + public static long getNetworkTotal() { + SystemInfo si = new SystemInfo(); + HardwareAbstractionLayer hal = si.getHardware(); + List recvNetworkIFs = hal.getNetworkIFs(); + NetworkIF networkIF= recvNetworkIFs.get(recvNetworkIFs.size() - 1); + + return networkIF.getSpeed()/1048576L/8L; + } + + public static double formatUnits(long value, long prefix) { + return (double)value / (double)prefix; + } + + /** + * 获取进程数 + * @return + */ + public static int getProcessesCount(){ + SystemInfo si = new SystemInfo(); + OperatingSystem os = si.getOperatingSystem(); + + int processCount = os.getProcessCount(); + return processCount; + } + + public static List> getDiskInfo() { + List> result = new ArrayList<>(); + + String osName = System.getProperty("os.name"); + List pathArray = new ArrayList<>(); + if (osName.startsWith("Mac OS")) { + // 苹果 + pathArray.add("/"); + } else if (osName.startsWith("Windows")) { + // windows + pathArray.add("C:"); + } else { + pathArray.add("/"); + pathArray.add("/home"); + } + for (String path : pathArray) { + Map infoMap = new HashMap<>(); + infoMap.put("path", path); + File partitionFile = new File(path); + // 单位: GB + infoMap.put("use", (partitionFile.getTotalSpace() - partitionFile.getFreeSpace())/1024/1024/1024D); + infoMap.put("free", partitionFile.getFreeSpace()/1024/1024/1024D); + result.add(infoMap); + } + return result; + } + + public static String getHardwareId(){ + SystemInfo systemInfo = new SystemInfo(); + HardwareAbstractionLayer hardware = systemInfo.getHardware(); + // CPU ID + String cpuId = hardware.getProcessor().getProcessorIdentifier().getProcessorID(); + // 主板序号 + String serialNumber = hardware.getComputerSystem().getSerialNumber(); + + return DigestUtils.md5DigestAsHex( + ( + DigestUtils.md5DigestAsHex(cpuId.getBytes(StandardCharsets.UTF_8)) + + DigestUtils.md5DigestAsHex(serialNumber.getBytes(StandardCharsets.UTF_8)) + ).getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/TileUtils.java b/src/main/java/com/genersoft/iot/vmp/utils/TileUtils.java new file mode 100644 index 0000000..a989ab5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/TileUtils.java @@ -0,0 +1,160 @@ +package com.genersoft.iot.vmp.utils; + +import com.genersoft.iot.vmp.gb28181.controller.bean.Extent; + +import java.util.ArrayList; +import java.util.List; + +public class TileUtils { + private static final double MAX_LATITUDE = 85.05112878; + + /** + * 根据坐标获取指定层级的x y 值 + * + * @param lon 经度 + * @param lat 纬度 + * @param z 层级 + * @return double[2] = {xTileFloat, yTileFloat} + */ + public static double[] lonLatToTileXY(double lon, double lat, int z) { + double n = Math.pow(2.0, z); + double x = (lon + 180.0) / 360.0 * n; + + // clamp latitude to WebMercator bounds + double latClamped = Math.max(Math.min(lat, MAX_LATITUDE), -MAX_LATITUDE); + double latRad = Math.toRadians(latClamped); + double y = (1.0 - (Math.log(Math.tan(latRad) + 1.0 / Math.cos(latRad)) / Math.PI)) / 2.0 * n; + + return new double[]{x, y}; + } + + /** + * 根据坐标范围获取指定层级的x y 范围 + * + * @param bbox array length 4: {minLon, minLat, maxLon, maxLat} + * @param z zoom level + * @return TileRange object with xMin,xMax,yMin,yMax,z + */ + public static TileRange bboxToTileRange(double[] bbox, int z) { + double minLon = bbox[0], minLat = bbox[1], maxLon = bbox[2], maxLat = bbox[3]; + + // If bbox crosses antimeridian (minLon > maxLon), caller should split into two bboxes. + if (minLon > maxLon) { + throw new IllegalArgumentException("bbox crosses antimeridian; split it before calling bboxToTileRange."); + } + + double[] tMin = lonLatToTileXY(minLon, maxLat, z); // top-left (use maxLat) + double[] tMax = lonLatToTileXY(maxLon, minLat, z); // bottom-right (use minLat) + + int xMin = (int) Math.floor(Math.min(tMin[0], tMax[0])); + int xMax = (int) Math.floor(Math.max(tMin[0], tMax[0])); + int yMin = (int) Math.floor(Math.min(tMin[1], tMax[1])); + int yMax = (int) Math.floor(Math.max(tMin[1], tMax[1])); + + int maxIndex = ((int) Math.pow(2, z)) - 1; + xMin = clamp(xMin, 0, maxIndex); + xMax = clamp(xMax, 0, maxIndex); + yMin = clamp(yMin, 0, maxIndex); + yMax = clamp(yMax, 0, maxIndex); + + return new TileRange(xMin, xMax, yMin, yMax, z); + } + + /** + * If bbox crosses antimeridian (minLon > maxLon), split into two bboxes: + * [minLon, minLat, 180, maxLat] and [-180, minLat, maxLon, maxLat] + * + * @param bbox input bbox array length 4 + * @return list of 1 or 2 bboxes (each double[4]) + */ + public static List splitAntimeridian(double[] bbox) { + double minLon = bbox[0], minLat = bbox[1], maxLon = bbox[2], maxLat = bbox[3]; + List parts = new ArrayList<>(); + if (minLon <= maxLon) { + parts.add(new double[]{minLon, minLat, maxLon, maxLat}); + } else { + parts.add(new double[]{minLon, minLat, 180.0, maxLat}); + parts.add(new double[]{-180.0, minLat, maxLon, maxLat}); + } + return parts; + } + + private static int clamp(int v, int a, int b) { + return Math.max(a, Math.min(b, v)); + } + + /** + * Return list of tile coordinates (x,y,z) covering bbox at zoom z. + * Be careful: the number of tiles can be large for high zooms & big bbox. + * + */ + public static List tilesForBoxAtZoom(Extent extent, int z) { + List tiles = new ArrayList<>(); + List parts = splitAntimeridian(new double[]{extent.getMinLng(), extent.getMinLat(), + extent.getMaxLng(), extent.getMaxLat()}); + for (double[] part : parts) { + TileRange range = bboxToTileRange(part, z); + for (int x = range.xMin; x <= range.xMax; x++) { + for (int y = range.yMin; y <= range.yMax; y++) { + tiles.add(new TileCoord(x, y, z)); + } + } + } + return tiles; + } + + // Simple helper classes + public static class TileRange { + public final int xMin, xMax, yMin, yMax, z; + public TileRange(int xMin, int xMax, int yMin, int yMax, int z) { + this.xMin = xMin; this.xMax = xMax; this.yMin = yMin; this.yMax = yMax; this.z = z; + } + } + + public static class TileCoord { + public final int x, y, z; + public TileCoord(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + + } + @Override + public String toString() { + return "{" + "z=" + z + ", x=" + x + ", y=" + y + '}'; + } + } + + /** + * tile X/Z -> longitude (deg) + */ + public static double tile2lon(int x, int z) { + double n = Math.pow(2.0, z); + return x / n * 360.0 - 180.0; + } + + /** + * tile Y/Z -> latitude (deg) + */ + public static double tile2lat(int y, int z) { + double n = Math.pow(2.0, z); + double latRad = Math.atan(Math.sinh(Math.PI * (1 - 2.0 * y / n))); + return Math.toDegrees(latRad); + } + + /** + * lon/lat -> pixel in tile (0..256) + */ + public static double[] lonLatToTilePixel(double lon, double lat, int z, int tileX, int tileY) { + double n = Math.pow(2.0, z); + double xtile = (lon + 180.0) / 360.0 * n; + + double latRad = Math.toRadians(lat); + double ytile = (1.0 - Math.log(Math.tan(latRad) + 1.0 / Math.cos(latRad)) / Math.PI) / 2.0 * n; + + double pixelX = (xtile - tileX) * 256.0; + double pixelY = (ytile - tileY) * 256.0; + + return new double[] { pixelX, pixelY }; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/UJson.java b/src/main/java/com/genersoft/iot/vmp/utils/UJson.java new file mode 100755 index 0000000..e39147c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/UJson.java @@ -0,0 +1,149 @@ +package com.genersoft.iot.vmp.utils; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; + +/** + * @author gaofuwang + * @version 1.0 + * @date 2022/3/11 10:17 + */ +@Slf4j +public class UJson { + + public static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + + static { + JSON_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false); + } + + private ObjectNode node; + + public UJson(){ + this.node = JSON_MAPPER.createObjectNode(); + } + + public UJson(String json){ + if(StringUtils.isBlank(json)){ + this.node = JSON_MAPPER.createObjectNode(); + }else{ + try { + this.node = JSON_MAPPER.readValue(json, ObjectNode.class); + }catch (Exception e){ + log.error(e.getMessage(), e); + this.node = JSON_MAPPER.createObjectNode(); + } + } + } + + public UJson(ObjectNode node){ + this.node = node; + } + + public String asText(String key){ + JsonNode jsonNode = node.get(key); + if(Objects.isNull(jsonNode)){ + return ""; + } + return jsonNode.asText(); + } + + public String asText(String key, String defaultVal){ + JsonNode jsonNode = node.get(key); + if(Objects.isNull(jsonNode)){ + return ""; + } + return jsonNode.asText(defaultVal); + } + + public UJson put(String key, String value){ + this.node.put(key, value); + return this; + } + + public UJson put(String key, Integer value){ + this.node.put(key, value); + return this; + } + + public static UJson json(){ + return new UJson(); + } + + public static UJson json(String json){ + return new UJson(json); + } + + public static T readJson(String json, Class clazz){ + if(StringUtils.isBlank(json)){ + return null; + } + try { + return JSON_MAPPER.readValue(json, clazz); + }catch (Exception e){ + log.error(e.getMessage(), e); + return null; + } + } + + public static String writeJson(Object object) { + try{ + return JSON_MAPPER.writeValueAsString(object); + }catch (Exception e){ + log.error(e.getMessage(), e); + return ""; + } + } + + @Override + public String toString() { + return node.toString(); + } + + public int asInt(String key, int defValue) { + JsonNode jsonNode = this.node.get(key); + if(Objects.isNull(jsonNode)){ + return defValue; + } + return jsonNode.asInt(defValue); + } + + public UJson getSon(String key) { + JsonNode sonNode = this.node.get(key); + if(Objects.isNull(sonNode)){ + return new UJson(); + } + return new UJson((ObjectNode) sonNode); + } + + public UJson set(String key, ObjectNode sonNode) { + this.node.set(key, sonNode); + return this; + } + + public UJson set(String key, UJson sonNode) { + this.node.set(key, sonNode.node); + return this; + } + + public Iterator> fields() { + return this.node.fields(); + } + + public ObjectNode getNode() { + return this.node; + } + + public UJson setAll(UJson json) { + this.node.setAll(json.node); + return this; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/redis/FastJsonRedisSerializer.java b/src/main/java/com/genersoft/iot/vmp/utils/redis/FastJsonRedisSerializer.java new file mode 100755 index 0000000..86b7dce --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/redis/FastJsonRedisSerializer.java @@ -0,0 +1,45 @@ +package com.genersoft.iot.vmp.utils.redis; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; + +import java.nio.charset.Charset; + +/** + * @description:使用fastjson实现redis的序列化 + * @author: swwheihei + * @date: 2020年5月6日 下午8:40:11 + */ +public class FastJsonRedisSerializer implements RedisSerializer { + + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + private Class clazz; + + public FastJsonRedisSerializer(Class clazz) { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) throws SerializationException { + if (t == null) { + return new byte[0]; + } + return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName, JSONWriter.Feature.WritePairAsJavaBean).getBytes(DEFAULT_CHARSET); + } + + @Override + public T deserialize(byte[] bytes) throws SerializationException { + if (bytes == null || bytes.length <= 0) { + return null; + } + String str = new String(bytes, DEFAULT_CHARSET); + return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType); + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil.java b/src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil.java new file mode 100755 index 0000000..101a3b3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil.java @@ -0,0 +1,47 @@ +package com.genersoft.iot.vmp.utils.redis; + +import com.google.common.collect.Lists; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Redis工具类 + * + * @author swwheihei + * @date 2020年5月6日 下午8:27:29 + */ +@SuppressWarnings(value = {"rawtypes", "unchecked"}) +public class RedisUtil { + + /** + * 模糊查询 + * + * @param query 查询参数 + * @return + */ + public static List scan(RedisTemplate redisTemplate, String query) { + + Set resultKeys = (Set) redisTemplate.execute((RedisCallback>) connection -> { + ScanOptions scanOptions = ScanOptions.scanOptions().match("*" + query + "*").count(1000).build(); + Cursor scan = connection.scan(scanOptions); + Set keys = new HashSet<>(); + while (scan.hasNext()) { + byte[] next = scan.next(); + keys.add(new String(next)); + } + return keys; + }); + + return Lists.newArrayList(resultKeys); + } +} + + + diff --git a/src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil2.java b/src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil2.java new file mode 100755 index 0000000..18c1fd5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil2.java @@ -0,0 +1,899 @@ +//package com.genersoft.iot.vmp.utils.redis; +// +//import com.alibaba.fastjson2.JSONObject; +//import com.genersoft.iot.vmp.utils.SpringBeanFactory; +//import org.springframework.data.redis.core.*; +//import org.springframework.util.CollectionUtils; +// +//import java.util.*; +//import java.util.concurrent.TimeUnit; +// +///** +// * Redis工具类 +// * @author swwheihei +// * @date 2020年5月6日 下午8:27:29 +// */ +//@SuppressWarnings(value = {"rawtypes", "unchecked"}) +//public class RedisUtil2 { +// +// private static RedisTemplate redisTemplate; +// +// static { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// +// /** +// * 指定缓存失效时间 +// * @param key 键 +// * @param time 时间(秒) +// * @return true / false +// */ +// public static boolean expire(String key, long time) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// if (time > 0) { +// redisTemplate.expire(key, time, TimeUnit.SECONDS); +// } +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 根据 key 获取过期时间 +// * @param key 键 +// */ +// public static long getExpire(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.getExpire(key, TimeUnit.SECONDS); +// } +// +// /** +// * 判断 key 是否存在 +// * @param key 键 +// * @return true / false +// */ +// public static boolean hasKey(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.hasKey(key); +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 删除缓存 +// * @SuppressWarnings("unchecked") 忽略类型转换警告 +// * @param key 键(一个或者多个) +// */ +// public static boolean del(String... key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// if (key != null && key.length > 0) { +// if (key.length == 1) { +// redisTemplate.delete(key[0]); +// } else { +//// 传入一个 Collection 集合 +// redisTemplate.delete(CollectionUtils.arrayToList(key)); +// } +// } +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +//// ============================== String ============================== +// +// /** +// * 普通缓存获取 +// * @param key 键 +// * @return 值 +// */ +// public static Object get(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return key == null ? null : redisTemplate.opsForValue().get(key); +// } +// +// /** +// * 普通缓存放入 +// * @param key 键 +// * @param value 值 +// * @return true / false +// */ +// public static boolean set(String key, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForValue().set(key, value); +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 普通缓存放入并设置时间 +// * @param key 键 +// * @param value 值 +// * @param time 时间(秒),如果 time < 0 则设置无限时间 +// * @return true / false +// */ +// public static boolean set(String key, Object value, long time) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// if (time > 0) { +// redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); +// } else { +// set(key, value); +// } +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 递增 +// * @param key 键 +// * @param delta 递增大小 +// * @return +// */ +// public static long incr(String key, long delta) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// if (delta < 0) { +// throw new RuntimeException("递增因子必须大于 0"); +// } +// return redisTemplate.opsForValue().increment(key, delta); +// } +// +// /** +// * 递减 +// * @param key 键 +// * @param delta 递减大小 +// * @return +// */ +// public static long decr(String key, long delta) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// if (delta < 0) { +// throw new RuntimeException("递减因子必须大于 0"); +// } +// return redisTemplate.opsForValue().increment(key, delta); +// } +// +//// ============================== Map ============================== +// +// /** +// * HashGet +// * @param key 键(no null) +// * @param item 项(no null) +// * @return 值 +// */ +// public static Object hget(String key, String item) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForHash().get(key, item); +// } +// +// /** +// * 获取 key 对应的 map +// * @param key 键(no null) +// * @return 对应的多个键值 +// */ +// public static Map hmget(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForHash().entries(key); +// } +// +// /** +// * HashSet +// * @param key 键 +// * @param map 值 +// * @return true / false +// */ +// public static boolean hmset(String key, Map map) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForHash().putAll(key, map); +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * HashSet 并设置时间 +// * @param key 键 +// * @param map 值 +// * @param time 时间 +// * @return true / false +// */ +// public static boolean hmset(String key, Map map, long time) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForHash().putAll(key, map); +// if (time > 0) { +// expire(key, time); +// } +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 向一张 Hash表 中放入数据,如不存在则创建 +// * @param key 键 +// * @param item 项 +// * @param value 值 +// * @return true / false +// */ +// public static boolean hset(String key, String item, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForHash().put(key, item, value); +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 向一张 Hash表 中放入数据,并设置时间,如不存在则创建 +// * @param key 键 +// * @param item 项 +// * @param value 值 +// * @param time 时间(如果原来的 Hash表 设置了时间,这里会覆盖) +// * @return true / false +// */ +// public static boolean hset(String key, String item, Object value, long time) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForHash().put(key, item, value); +// if (time > 0) { +// expire(key, time); +// } +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 删除 Hash表 中的值 +// * @param key 键 +// * @param item 项(可以多个,no null) +// */ +// public static void hdel(String key, Object... item) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// redisTemplate.opsForHash().delete(key, item); +// } +// +// /** +// * 判断 Hash表 中是否有该键的值 +// * @param key 键(no null) +// * @param item 值(no null) +// * @return true / false +// */ +// public static boolean hHasKey(String key, String item) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForHash().hasKey(key, item); +// } +// +// /** +// * Hash递增,如果不存在则创建一个,并把新增的值返回 +// * @param key 键 +// * @param item 项 +// * @param by 递增大小 > 0 +// * @return +// */ +// public static Double hincr(String key, String item, Double by) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForHash().increment(key, item, by); +// } +// +// /** +// * Hash递减 +// * @param key 键 +// * @param item 项 +// * @param by 递减大小 +// * @return +// */ +// public static Double hdecr(String key, String item, Double by) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForHash().increment(key, item, -by); +// } +// +//// ============================== Set ============================== +// +// /** +// * 根据 key 获取 set 中的所有值 +// * @param key 键 +// * @return 值 +// */ +// public static Set sGet(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForSet().members(key); +// } catch (Exception e) { +// e.printStackTrace(); +// return null; +// } +// } +// +// /** +// * 从键为 key 的 set 中,根据 value 查询是否存在 +// * @param key 键 +// * @param value 值 +// * @return true / false +// */ +// public static boolean sHasKey(String key, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForSet().isMember(key, value); +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 将数据放入 set缓存 +// * @param key 键值 +// * @param values 值(可以多个) +// * @return 成功个数 +// */ +// public static long sSet(String key, Object... values) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForSet().add(key, values); +// } catch (Exception e) { +// e.printStackTrace(); +// return 0; +// } +// } +// +// /** +// * 将数据放入 set缓存,并设置时间 +// * @param key 键 +// * @param time 时间 +// * @param values 值(可以多个) +// * @return 成功放入个数 +// */ +// public static long sSet(String key, long time, Object... values) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// long count = redisTemplate.opsForSet().add(key, values); +// if (time > 0) { +// expire(key, time); +// } +// return count; +// } catch (Exception e) { +// e.printStackTrace(); +// return 0; +// } +// } +// +// /** +// * 获取 set缓存的长度 +// * @param key 键 +// * @return 长度 +// */ +// public static long sGetSetSize(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForSet().size(key); +// } catch (Exception e) { +// e.printStackTrace(); +// return 0; +// } +// } +// +// /** +// * 移除 set缓存中,值为 value 的 +// * @param key 键 +// * @param values 值 +// * @return 成功移除个数 +// */ +// public static long setRemove(String key, Object... values) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForSet().remove(key, values); +// } catch (Exception e) { +// e.printStackTrace(); +// return 0; +// } +// } +//// ============================== ZSet ============================== +// +// /** +// * 添加一个元素, zset与set最大的区别就是每个元素都有一个score,因此有个排序的辅助功能; zadd +// * +// * @param key +// * @param value +// * @param score +// */ +// public static void zAdd(Object key, Object value, double score) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// redisTemplate.opsForZSet().add(key, value, score); +// } +// +// /** +// * 删除元素 zrem +// * +// * @param key +// * @param value +// */ +// public static void zRemove(Object key, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// redisTemplate.opsForZSet().remove(key, value); +// } +// +// /** +// * score的增加or减少 zincrby +// * +// * @param key +// * @param value +// * @param delta -1 表示减 1 表示加1 +// */ +// public static Double zIncrScore(Object key, Object value, double delta) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForZSet().incrementScore(key, value, delta); +// } +// +// /** +// * 查询value对应的score zscore +// * +// * @param key +// * @param value +// * @return +// */ +// public static Double zScore(Object key, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForZSet().score(key, value); +// } +// +// /** +// * 判断value在zset中的排名 zrank +// * +// * @param key +// * @param value +// * @return +// */ +// public static Long zRank(Object key, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForZSet().rank(key, value); +// } +// +// /** +// * 返回集合的长度 +// * +// * @param key +// * @return +// */ +// public static Long zSize(Object key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForZSet().zCard(key); +// } +// +// /** +// * 查询集合中指定顺序的值, 0 -1 表示获取全部的集合内容 zrange +// * +// * 返回有序的集合,score小的在前面 +// * +// * @param key +// * @param start +// * @param end +// * @return +// */ +// public static Set zRange(Object key, int start, int end) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForZSet().range(key, start, end); +// } +// /** +// * 查询集合中指定顺序的值和score,0, -1 表示获取全部的集合内容 +// * +// * @param key +// * @param start +// * @param end +// * @return +// */ +// public static Set> zRangeWithScore(Object key, int start, int end) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForZSet().rangeWithScores(key, start, end); +// } +// /** +// * 查询集合中指定顺序的值 zrevrange +// * +// * 返回有序的集合中,score大的在前面 +// * +// * @param key +// * @param start +// * @param end +// * @return +// */ +// public static Set zRevRange(Object key, int start, int end) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForZSet().reverseRange(key, start, end); +// } +// /** +// * 根据score的值,来获取满足条件的集合 zrangebyscore +// * +// * @param key +// * @param min +// * @param max +// * @return +// */ +// public static Set zSortRange(Object key, int min, int max) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForZSet().rangeByScore(key, min, max); +// } +// +// +//// ============================== List ============================== +// +// /** +// * 获取 list缓存的内容 +// * @param key 键 +// * @param start 开始 +// * @param end 结束(0 到 -1 代表所有值) +// * @return +// */ +// public static List lGet(String key, long start, long end) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForList().range(key, start, end); +// } catch (Exception e) { +// e.printStackTrace(); +// return null; +// } +// } +// +// /** +// * 获取 list缓存的长度 +// * @param key 键 +// * @return 长度 +// */ +// public static long lGetListSize(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForList().size(key); +// } catch (Exception e) { +// e.printStackTrace(); +// return 0; +// } +// } +// +// /** +// * 根据索引 index 获取键为 key 的 list 中的元素 +// * @param key 键 +// * @param index 索引 +// * 当 index >= 0 时 {0:表头, 1:第二个元素} +// * 当 index < 0 时 {-1:表尾, -2:倒数第二个元素} +// * @return 值 +// */ +// public static Object lGetIndex(String key, long index) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForList().index(key, index); +// } catch (Exception e) { +// e.printStackTrace(); +// return null; +// } +// } +// +// /** +// * 将值 value 插入键为 key 的 list 中,如果 list 不存在则创建空 list +// * @param key 键 +// * @param value 值 +// * @return true / false +// */ +// public static boolean lSet(String key, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForList().rightPush(key, value); +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 将值 value 插入键为 key 的 list 中,并设置时间 +// * @param key 键 +// * @param value 值 +// * @param time 时间 +// * @return true / false +// */ +// public static boolean lSet(String key, Object value, long time) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForList().rightPush(key, value); +// if (time > 0) { +// expire(key, time); +// } +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 将 values 插入键为 key 的 list 中 +// * @param key 键 +// * @param values 值 +// * @return true / false +// */ +// public static boolean lSetList(String key, List values) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForList().rightPushAll(key, values); +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 将 values 插入键为 key 的 list 中,并设置时间 +// * @param key 键 +// * @param values 值 +// * @param time 时间 +// * @return true / false +// */ +// public static boolean lSetList(String key, List values, long time) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForList().rightPushAll(key, values); +// if (time > 0) { +// expire(key, time); +// } +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 根据索引 index 修改键为 key 的值 +// * @param key 键 +// * @param index 索引 +// * @param value 值 +// * @return true / false +// */ +// public static boolean lUpdateIndex(String key, long index, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForList().set(key, index, value); +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 在键为 key 的 list 中删除值为 value 的元素 +// * @param key 键 +// * @param count 如果 count == 0 则删除 list 中所有值为 value 的元素 +// * 如果 count > 0 则删除 list 中最左边那个值为 value 的元素 +// * 如果 count < 0 则删除 list 中最右边那个值为 value 的元素 +// * @param value +// * @return +// */ +// public static long lRemove(String key, long count, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForList().remove(key, count, value); +// } catch (Exception e) { +// e.printStackTrace(); +// return 0; +// } +// } +// +// /** +// * 在键为 key 的 list中移除第一个元素 +// * @param key 键 +// * @return +// */ +// public static Object lLeftPop(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForList().leftPop(key); +// } +// +// /** +// * 在键为 key 的 list中移除、最后一个元素 +// * @param key 键 +// * @return +// */ +// public static Object lrightPop(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForList().rightPop(key); +// } +// +// /** +// * 模糊查询 +// * @param key 键 +// * @return true / false +// */ +// public static List keys(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// Set set = redisTemplate.keys(key); +// return new ArrayList<>(set); +// } catch (Exception e) { +// e.printStackTrace(); +// return null; +// } +// } +// +// +// /** +// * 模糊查询 +// * @param query 查询参数 +// * @return +// */ +//// public static List scan(String query) { +//// List result = new ArrayList<>(); +//// try { +//// Cursor> cursor = redisTemplate.opsForHash().scan("field", +//// ScanOptions.scanOptions().match(query).count(1000).build()); +//// while (cursor.hasNext()) { +//// Map.Entry entry = cursor.next(); +//// result.add(entry.getKey()); +//// Object key = entry.getKey(); +//// Object valueSet = entry.getValue(); +//// } +//// //关闭cursor +//// cursor.close(); +//// } catch (Exception e) { +//// e.printStackTrace(); +//// } +//// return result; +//// } +// +// /** +// * 模糊查询 +// * @param query 查询参数 +// * @return +// */ +// public static List scan(String query) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// Set resultKeys = (Set) redisTemplate.execute((RedisCallback>) connection -> { +// ScanOptions scanOptions = ScanOptions.scanOptions().match("*" + query + "*").count(1000).build(); +// Cursor scan = connection.scan(scanOptions); +// Set keys = new HashSet<>(); +// while (scan.hasNext()) { +// byte[] next = scan.next(); +// keys.add(new String(next)); +// } +// return keys; +// }); +// +// return new ArrayList<>(resultKeys); +// } +// public static List scan2(String query) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// Set keys = redisTemplate.keys(query); +// return new ArrayList<>(keys); +// } +// // ============================== 消息发送与订阅 ============================== +// public static void convertAndSend(String channel, JSONObject msg) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// redisTemplate.convertAndSend(channel, msg); +// } +// +//} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/TestController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/TestController.java new file mode 100644 index 0000000..6df13ba --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/TestController.java @@ -0,0 +1,74 @@ +package com.genersoft.iot.vmp.vmanager; + +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/test") +public class TestController { + + @Autowired + private HookSubscribe subscribe; + + @Autowired + private RedisTemplate redisTemplate; + + @GetMapping("/hook/list") + public List all(){ + return subscribe.getAll(); + } + + + @GetMapping("/redis") + public List redis(){ + InviteSessionType type = InviteSessionType.PLAY; + String channelId = null; + String stream = null; + + String key = VideoManagerConstants.INVITE_PREFIX; + String keyPattern = (type != null ? type : "*") + + ":" + (channelId != null ? channelId : "*") + + ":" + (stream != null ? stream : "*") + + ":*"; + ScanOptions options = ScanOptions.scanOptions().match(keyPattern).count(20).build(); + Cursor> cursor = redisTemplate.opsForHash().scan(key, options); + List result = new ArrayList<>(); + try { + while (cursor.hasNext()) { + System.out.println(cursor.next().getKey()); + result.add((InviteInfo) cursor.next().getValue()); + } + }catch (Exception e) { + + }finally { + cursor.close(); + } + return result; + } + +// @Bean +// public ServletRegistrationBean druidStatViewServlet() { +// ServletRegistrationBean registrationBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); +// registrationBean.addInitParameter("allow", "127.0.0.1");// IP白名单 (没有配置或者为空,则允许所有访问) +// registrationBean.addInitParameter("deny", "");// IP黑名单 (存在共同时,deny优先于allow) +// registrationBean.addInitParameter("loginUsername", "admin"); +// registrationBean.addInitParameter("loginPassword", "admin"); +// registrationBean.addInitParameter("resetEnable", "false"); +// return registrationBean; +// } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/AudioBroadcastResult.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/AudioBroadcastResult.java new file mode 100644 index 0000000..1126081 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/AudioBroadcastResult.java @@ -0,0 +1,59 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +/** + * @author lin + */ +public class AudioBroadcastResult { + /** + * 推流的各个方式流地址 + */ + private StreamContent streamInfo; + + /** + * 编码格式 + */ + private String codec; + + /** + * 向zlm推流的应用名 + */ + private String app; + + /** + * 向zlm推流的流ID + */ + private String stream; + + + public StreamContent getStreamInfo() { + return streamInfo; + } + + public void setStreamInfo(StreamContent streamInfo) { + this.streamInfo = streamInfo; + } + + public String getCodec() { + return codec; + } + + public void setCodec(String codec) { + this.codec = codec; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/BatchGBStreamParam.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/BatchGBStreamParam.java new file mode 100755 index 0000000..0090488 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/BatchGBStreamParam.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +import com.genersoft.iot.vmp.gb28181.bean.GbStream; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +/** + * @author lin + */ +@Schema(description = "多个推流信息") +public class BatchGBStreamParam { + @Schema(description = "推流信息列表") + private List gbStreams; + + public List getGbStreams() { + return gbStreams; + } + + public void setGbStreams(List gbStreams) { + this.gbStreams = gbStreams; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeferredResultEx.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeferredResultEx.java new file mode 100755 index 0000000..0b9d3d9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeferredResultEx.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +import org.springframework.web.context.request.async.DeferredResult; + +public class DeferredResultEx { + + private DeferredResult deferredResult; + + private DeferredResultFilter filter; + + public DeferredResultEx(DeferredResult result) { + this.deferredResult = result; + } + + + public DeferredResult getDeferredResult() { + return deferredResult; + } + + public void setDeferredResult(DeferredResult deferredResult) { + this.deferredResult = deferredResult; + } + + public DeferredResultFilter getFilter() { + return filter; + } + + public void setFilter(DeferredResultFilter filter) { + this.filter = filter; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeferredResultFilter.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeferredResultFilter.java new file mode 100755 index 0000000..18c2240 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeferredResultFilter.java @@ -0,0 +1,6 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +public interface DeferredResultFilter { + + Object handler(Object o); +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java new file mode 100755 index 0000000..c0a0dc3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +/** + * 全局错误码 + */ +public enum ErrorCode { + SUCCESS(0, "成功"), + ERROR100(100, "失败"), + ERROR400(400, "参数或方法错误"), + ERROR404(404, "资源未找到"), + ERROR403(403, "无权限操作"), + ERROR486(486, "超时或无响应"), + ERROR401(401, "请登录后重新请求"), + ERROR408(408, "请求超时"), + ERROR500(500, "系统异常"); + + private final int code; + private final String msg; + + ErrorCode(int code, String msg) { + this.code = code; + this.msg = msg; + } + + public int getCode() { + return code; + } + + public String getMsg() { + return msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/MapConfig.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/MapConfig.java new file mode 100644 index 0000000..a9d9068 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/MapConfig.java @@ -0,0 +1,16 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +import lombok.Data; + +@Data +public class MapConfig { + + private String name; + private String coordinateSystem; + private String tilesUrl; + private Integer tileSize; + private Integer zoom; + private Double[] center; + private Integer maxZoom; + private Integer minZoom; +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/MapModelIcon.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/MapModelIcon.java new file mode 100644 index 0000000..20838f5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/MapModelIcon.java @@ -0,0 +1,26 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class MapModelIcon { + + @Schema(description = "名称") + private String name; + + @Schema(description = "别名") + private String alias; + + @Schema(description = "路径") + private String path; + + + public static MapModelIcon getInstance(String name, String alias, String path) { + MapModelIcon mapModelIcon = new MapModelIcon(); + mapModelIcon.setAlias(alias); + mapModelIcon.setName(name); + mapModelIcon.setPath(path); + return mapModelIcon; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/OtherPsSendInfo.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/OtherPsSendInfo.java new file mode 100755 index 0000000..ac98409 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/OtherPsSendInfo.java @@ -0,0 +1,137 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +public class OtherPsSendInfo { + + /** + * 发流IP + */ + private String sendLocalIp; + + /** + * 发流端口 + */ + private int sendLocalPort; + + /** + * 收流IP + */ + private String receiveIp; + + /** + * 收流端口 + */ + private int receivePort; + + + /** + * 会话ID + */ + private String callId; + + /** + * 流ID + */ + private String stream; + + /** + * 推流应用名 + */ + private String pushApp; + + /** + * 推流流ID + */ + private String pushStream; + + /** + * 推流SSRC + */ + private String pushSSRC; + + public String getSendLocalIp() { + return sendLocalIp; + } + + public void setSendLocalIp(String sendLocalIp) { + this.sendLocalIp = sendLocalIp; + } + + public int getSendLocalPort() { + return sendLocalPort; + } + + public void setSendLocalPort(int sendLocalPort) { + this.sendLocalPort = sendLocalPort; + } + + public String getReceiveIp() { + return receiveIp; + } + + public void setReceiveIp(String receiveIp) { + this.receiveIp = receiveIp; + } + + public int getReceivePort() { + return receivePort; + } + + public void setReceivePort(int receivePort) { + this.receivePort = receivePort; + } + + public String getCallId() { + return callId; + } + + public void setCallId(String callId) { + this.callId = callId; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getPushApp() { + return pushApp; + } + + public void setPushApp(String pushApp) { + this.pushApp = pushApp; + } + + public String getPushStream() { + return pushStream; + } + + public void setPushStream(String pushStream) { + this.pushStream = pushStream; + } + + public String getPushSSRC() { + return pushSSRC; + } + + public void setPushSSRC(String pushSSRC) { + this.pushSSRC = pushSSRC; + } + + @Override + public String toString() { + return "OtherPsSendInfo{" + + "sendLocalIp='" + sendLocalIp + '\'' + + ", sendLocalPort=" + sendLocalPort + + ", receiveIp='" + receiveIp + '\'' + + ", receivePort=" + receivePort + + ", callId='" + callId + '\'' + + ", stream='" + stream + '\'' + + ", pushApp='" + pushApp + '\'' + + ", pushStream='" + pushStream + '\'' + + ", pushSSRC='" + pushSSRC + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/OtherRtpSendInfo.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/OtherRtpSendInfo.java new file mode 100755 index 0000000..75c05d3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/OtherRtpSendInfo.java @@ -0,0 +1,166 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +public class OtherRtpSendInfo { + + /** + * 发流IP + */ + private String sendLocalIp; + + /** + * 音频发流端口 + */ + private int sendLocalPortForAudio; + + /** + * 视频发流端口 + */ + private int sendLocalPortForVideo; + + /** + * 收流IP + */ + private String receiveIp; + + /** + * 音频收流端口 + */ + private int receivePortForAudio; + + /** + * 视频收流端口 + */ + private int receivePortForVideo; + + /** + * 会话ID + */ + private String callId; + + /** + * 流ID + */ + private String stream; + + /** + * 推流应用名 + */ + private String pushApp; + + /** + * 推流流ID + */ + private String pushStream; + + /** + * 推流SSRC + */ + private String pushSSRC; + + + public String getReceiveIp() { + return receiveIp; + } + + public void setReceiveIp(String receiveIp) { + this.receiveIp = receiveIp; + } + + public int getReceivePortForAudio() { + return receivePortForAudio; + } + + public void setReceivePortForAudio(int receivePortForAudio) { + this.receivePortForAudio = receivePortForAudio; + } + + public int getReceivePortForVideo() { + return receivePortForVideo; + } + + public void setReceivePortForVideo(int receivePortForVideo) { + this.receivePortForVideo = receivePortForVideo; + } + + public String getCallId() { + return callId; + } + + public void setCallId(String callId) { + this.callId = callId; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getPushApp() { + return pushApp; + } + + public void setPushApp(String pushApp) { + this.pushApp = pushApp; + } + + public String getPushStream() { + return pushStream; + } + + public void setPushStream(String pushStream) { + this.pushStream = pushStream; + } + + public String getPushSSRC() { + return pushSSRC; + } + + public void setPushSSRC(String pushSSRC) { + this.pushSSRC = pushSSRC; + } + + + public String getSendLocalIp() { + return sendLocalIp; + } + + public void setSendLocalIp(String sendLocalIp) { + this.sendLocalIp = sendLocalIp; + } + + public int getSendLocalPortForAudio() { + return sendLocalPortForAudio; + } + + public void setSendLocalPortForAudio(int sendLocalPortForAudio) { + this.sendLocalPortForAudio = sendLocalPortForAudio; + } + + public int getSendLocalPortForVideo() { + return sendLocalPortForVideo; + } + + public void setSendLocalPortForVideo(int sendLocalPortForVideo) { + this.sendLocalPortForVideo = sendLocalPortForVideo; + } + + @Override + public String toString() { + return "OtherRtpSendInfo{" + + "sendLocalIp='" + sendLocalIp + '\'' + + ", sendLocalPortForAudio=" + sendLocalPortForAudio + + ", sendLocalPortForVideo=" + sendLocalPortForVideo + + ", receiveIp='" + receiveIp + '\'' + + ", receivePortForAudio=" + receivePortForAudio + + ", receivePortForVideo=" + receivePortForVideo + + ", callId='" + callId + '\'' + + ", stream='" + stream + '\'' + + ", pushApp='" + pushApp + '\'' + + ", pushStream='" + pushStream + '\'' + + ", pushSSRC='" + pushSSRC + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/PlayTypeEnum.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/PlayTypeEnum.java new file mode 100755 index 0000000..d2ee56b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/PlayTypeEnum.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +public enum PlayTypeEnum { + + PLAY("0", "直播"), + PLAY_BACK("1", "回放"); + + private String value; + private String name; + + PlayTypeEnum(String value, String name) { + this.value = value; + this.name = name; + } + + public String getValue() { + return value; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/RecordFile.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/RecordFile.java new file mode 100755 index 0000000..72d6561 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/RecordFile.java @@ -0,0 +1,53 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +public class RecordFile { + private String app; + private String stream; + + private String fileName; + + private String mediaServerId; + + private String date; + + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getMediaServerId() { + return mediaServerId; + } + + public void setMediaServerId(String mediaServerId) { + this.mediaServerId = mediaServerId; + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ResourceBaseInfo.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ResourceBaseInfo.java new file mode 100755 index 0000000..dab9b0a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ResourceBaseInfo.java @@ -0,0 +1,30 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +public class ResourceBaseInfo { + private int total; + private int online; + + public ResourceBaseInfo() { + } + + public ResourceBaseInfo(int total, int online) { + this.total = total; + this.online = online; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + + public int getOnline() { + return online; + } + + public void setOnline(int online) { + this.online = online; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ResourceInfo.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ResourceInfo.java new file mode 100755 index 0000000..3b0ee0d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ResourceInfo.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +public class ResourceInfo { + + private ResourceBaseInfo device; + private ResourceBaseInfo channel; + private ResourceBaseInfo push; + private ResourceBaseInfo proxy; + + public ResourceBaseInfo getDevice() { + return device; + } + + public void setDevice(ResourceBaseInfo device) { + this.device = device; + } + + public ResourceBaseInfo getChannel() { + return channel; + } + + public void setChannel(ResourceBaseInfo channel) { + this.channel = channel; + } + + public ResourceBaseInfo getPush() { + return push; + } + + public void setPush(ResourceBaseInfo push) { + this.push = push; + } + + public ResourceBaseInfo getProxy() { + return proxy; + } + + public void setProxy(ResourceBaseInfo proxy) { + this.proxy = proxy; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/SnapPath.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/SnapPath.java new file mode 100755 index 0000000..ce34d72 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/SnapPath.java @@ -0,0 +1,50 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "截图地址信息") +public class SnapPath { + + @Schema(description = "相对地址") + private String path; + + @Schema(description = "绝对地址") + private String absoluteFilePath; + + @Schema(description = "请求地址") + private String url; + + + public static SnapPath getInstance(String path, String absoluteFilePath, String url) { + SnapPath snapPath = new SnapPath(); + snapPath.setPath(path); + snapPath.setAbsoluteFilePath(absoluteFilePath); + snapPath.setUrl(url); + return snapPath; + } + + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getAbsoluteFilePath() { + return absoluteFilePath; + } + + public void setAbsoluteFilePath(String absoluteFilePath) { + this.absoluteFilePath = absoluteFilePath; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/StreamContent.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/StreamContent.java new file mode 100755 index 0000000..2d3861b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/StreamContent.java @@ -0,0 +1,206 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "流信息") +public class StreamContent { + + @Schema(description = "应用名") + private String app; + + @Schema(description = "流ID") + private String stream; + + @Schema(description = "IP") + private String ip; + + @Schema(description = "HTTP-FLV流地址") + private String flv; + + @Schema(description = "HTTPS-FLV流地址") + private String https_flv; + + @Schema(description = "Websocket-FLV流地址") + private String ws_flv; + + @Schema(description = "Websockets-FLV流地址") + private String wss_flv; + + @Schema(description = "HTTP-FMP4流地址") + private String fmp4; + + @Schema(description = "HTTPS-FMP4流地址") + private String https_fmp4; + + @Schema(description = "Websocket-FMP4流地址") + private String ws_fmp4; + + @Schema(description = "Websockets-FMP4流地址") + private String wss_fmp4; + + @Schema(description = "HLS流地址") + private String hls; + + @Schema(description = "HTTPS-HLS流地址") + private String https_hls; + + @Schema(description = "Websocket-HLS流地址") + private String ws_hls; + + @Schema(description = "Websockets-HLS流地址") + private String wss_hls; + + @Schema(description = "HTTP-TS流地址") + private String ts; + + @Schema(description = "HTTPS-TS流地址") + private String https_ts; + + @Schema(description = "Websocket-TS流地址") + private String ws_ts; + + @Schema(description = "Websockets-TS流地址") + private String wss_ts; + + @Schema(description = "RTMP流地址") + private String rtmp; + + @Schema(description = "RTMPS流地址") + private String rtmps; + + @Schema(description = "RTSP流地址") + private String rtsp; + + @Schema(description = "RTSPS流地址") + private String rtsps; + + @Schema(description = "RTC流地址") + private String rtc; + + @Schema(description = "RTCS流地址") + private String rtcs; + + @Schema(description = "流媒体ID") + private String mediaServerId; + + @Schema(description = "流编码信息") + private MediaInfo mediaInfo; + + @Schema(description = "开始时间") + private String startTime; + + @Schema(description = "结束时间") + private String endTime; + + @Schema(description = "时长(回放时使用)") + private Double duration; + + @Schema(description = "文件下载地址(录像下载使用)") + private DownloadFileInfo downLoadFilePath; + + @Schema(description = "转码后的视频流") + private StreamContent transcodeStream; + + private double progress; + + @Schema(description = "拉流代理返回的KEY") + private String key; + + @Schema(description = "使用的WVP ID") + private String serverId; + + public StreamContent(StreamInfo streamInfo) { + if (streamInfo == null) { + return; + } + this.app = streamInfo.getApp(); + this.stream = streamInfo.getStream(); + if (streamInfo.getFlv() != null) { + this.flv = streamInfo.getFlv().getUrl(); + } + if (streamInfo.getHttps_flv() != null) { + this.https_flv = streamInfo.getHttps_flv().getUrl(); + } + if (streamInfo.getWs_flv() != null) { + this.ws_flv = streamInfo.getWs_flv().getUrl(); + } + if (streamInfo.getWss_flv() != null) { + this.wss_flv = streamInfo.getWss_flv().getUrl(); + } + if (streamInfo.getFmp4() != null) { + this.fmp4 = streamInfo.getFmp4().getUrl(); + } + if (streamInfo.getHttps_fmp4() != null) { + this.https_fmp4 = streamInfo.getHttps_fmp4().getUrl(); + } + if (streamInfo.getWs_fmp4() != null) { + this.ws_fmp4 = streamInfo.getWs_fmp4().getUrl(); + } + if (streamInfo.getWss_fmp4() != null) { + this.wss_fmp4 = streamInfo.getWss_fmp4().getUrl(); + } + if (streamInfo.getHls() != null) { + this.hls = streamInfo.getHls().getUrl(); + } + if (streamInfo.getHttps_hls() != null) { + this.https_hls = streamInfo.getHttps_hls().getUrl(); + } + if (streamInfo.getWs_hls() != null) { + this.ws_hls = streamInfo.getWs_hls().getUrl(); + } + if (streamInfo.getWss_hls() != null) { + this.wss_hls = streamInfo.getWss_hls().getUrl(); + } + if (streamInfo.getTs() != null) { + this.ts = streamInfo.getTs().getUrl(); + } + if (streamInfo.getHttps_ts() != null) { + this.https_ts = streamInfo.getHttps_ts().getUrl(); + } + if (streamInfo.getWs_ts() != null) { + this.ws_ts = streamInfo.getWs_ts().getUrl(); + } + if (streamInfo.getRtmp() != null) { + this.rtmp = streamInfo.getRtmp().getUrl(); + } + if (streamInfo.getRtmps() != null) { + this.rtmps = streamInfo.getRtmps().getUrl(); + } + if (streamInfo.getRtsp() != null) { + this.rtsp = streamInfo.getRtsp().getUrl(); + } + if (streamInfo.getRtsps() != null) { + this.rtsps = streamInfo.getRtsps().getUrl(); + } + if (streamInfo.getRtc() != null) { + this.rtc = streamInfo.getRtc().getUrl(); + } + if (streamInfo.getRtcs() != null) { + this.rtcs = streamInfo.getRtcs().getUrl(); + } + if (streamInfo.getMediaServer() != null) { + this.mediaServerId = streamInfo.getMediaServer().getId(); + } + + this.mediaInfo = streamInfo.getMediaInfo(); + this.startTime = streamInfo.getStartTime(); + this.endTime = streamInfo.getEndTime(); + this.progress = streamInfo.getProgress(); + this.duration = streamInfo.getDuration(); + this.key = streamInfo.getKey(); + this.serverId = streamInfo.getServerId(); + + if (streamInfo.getDownLoadFilePath() != null) { + this.downLoadFilePath = streamInfo.getDownLoadFilePath(); + } + if (streamInfo.getTranscodeStream() != null) { + this.transcodeStream = new StreamContent(streamInfo.getTranscodeStream()); + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/SystemConfigInfo.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/SystemConfigInfo.java new file mode 100755 index 0000000..838e706 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/SystemConfigInfo.java @@ -0,0 +1,19 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +import com.genersoft.iot.vmp.common.VersionPo; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.jt1078.config.JT1078Config; +import lombok.Data; + +@Data +public class SystemConfigInfo { + + private int serverPort; + private SipConfig sip; + private UserSetting addOn; + private VersionPo version; + private JT1078Config jt1078Config; + +} + diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/TablePageInfo.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/TablePageInfo.java new file mode 100755 index 0000000..73d4223 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/TablePageInfo.java @@ -0,0 +1,99 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +import java.util.ArrayList; +import java.util.List; + +public class TablePageInfo { + //当前页 + private int pageNum; + //每页的数量 + private int pageSize; + //当前页的数量 + private int size; + //总页数 + private int pages; + //总数 + private int total; + + private List resultData; + + private List list; + + public TablePageInfo(List resultData) { + this.resultData = resultData; + } + + public TablePageInfo() { + } + + public void startPage(int page, int count) { + if (count <= 0) count = 10; + if (page <= 0) page = 1; + this.pageNum = page; + this.pageSize = count; + this.total = resultData.size(); + + this.pages = total % count == 0 ? total / count : total / count + 1; + int fromIndx = (page - 1) * count; + if (fromIndx > this.total - 1) { + this.list = new ArrayList<>(); + this.size = 0; + return; + } + + int toIndx = page * count; + if (toIndx > this.total) { + toIndx = this.total; + } + this.list = this.resultData.subList(fromIndx, toIndx); + this.size = this.list.size(); + } + + public int getPageNum() { + return pageNum; + } + + public void setPageNum(int pageNum) { + this.pageNum = pageNum; + } + + public int getPageSize() { + return pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public int getPages() { + return pages; + } + + public void setPages(int pages) { + this.pages = pages; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/WVPResult.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/WVPResult.java new file mode 100755 index 0000000..3699d08 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/WVPResult.java @@ -0,0 +1,55 @@ +package com.genersoft.iot.vmp.vmanager.bean; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "统一返回结果") +public class WVPResult implements Cloneable{ + + public WVPResult() { + } + + public WVPResult(int code, String msg, T data) { + this.code = code; + this.msg = msg; + this.data = data; + } + + + @Schema(description = "错误码,0为成功") + private int code; + @Schema(description = "描述,错误时描述错误原因") + private String msg; + @Schema(description = "数据") + private T data; + + + public static WVPResult success(T t, String msg) { + return new WVPResult<>(ErrorCode.SUCCESS.getCode(), msg, t); + } + + public static WVPResult success() { + return new WVPResult<>(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + } + + public static WVPResult success(T t) { + return success(t, ErrorCode.SUCCESS.getMsg()); + } + + public static WVPResult fail(int code, String msg) { + return new WVPResult<>(code, msg, null); + } + + public static WVPResult fail(ErrorCode errorCode) { + return fail(errorCode.getCode(), errorCode.getMsg()); + } + + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/cloudRecord/CloudRecordController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/cloudRecord/CloudRecordController.java new file mode 100755 index 0000000..addd943 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/cloudRecord/CloudRecordController.java @@ -0,0 +1,604 @@ +package com.genersoft.iot.vmp.vmanager.cloudRecord; + +import com.alibaba.fastjson2.JSONArray; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ICloudRecordService; +import com.genersoft.iot.vmp.service.bean.CloudRecordItem; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.streamPush.bean.BatchRemoveParam; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.HttpUtils; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.genersoft.iot.vmp.vmanager.cloudRecord.bean.CloudRecordUrl; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +@SuppressWarnings("rawtypes") +@Tag(name = "云端录像接口") +@Slf4j +@RestController +@RequestMapping("/api/cloud/record") +public class CloudRecordController { + + + @Autowired + private ICloudRecordService cloudRecordService; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private UserSetting userSetting; + + + @ResponseBody + @GetMapping("/date/list") + @Operation(summary = "查询存在云端录像的日期", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @Parameter(name = "year", description = "年,置空则查询当年", required = false) + @Parameter(name = "month", description = "月,置空则查询当月", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID,置空则查询全部", required = false) + public List openRtpServer( + @RequestParam(required = true) String app, + @RequestParam(required = true) String stream, + @RequestParam(required = false) Integer year, + @RequestParam(required = false) Integer month, + @RequestParam(required = false) String mediaServerId + + ) { + log.info("[云端录像] 查询存在云端录像的日期 app->{}, stream->{}, mediaServerId->{}, year->{}, month->{}", app, stream, mediaServerId, year, month); + Calendar calendar = Calendar.getInstance(); + if (ObjectUtils.isEmpty(year)) { + year = calendar.get(Calendar.YEAR); + } + if (ObjectUtils.isEmpty(month)) { + month = calendar.get(Calendar.MONTH) + 1; + } + List mediaServers; + if (!ObjectUtils.isEmpty(mediaServerId)) { + mediaServers = new ArrayList<>(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体: " + mediaServerId); + } + mediaServers.add(mediaServer); + } else { + mediaServers = mediaServerService.getAllOnlineList(); + } + if (mediaServers.isEmpty()) { + return new ArrayList<>(); + } + + return cloudRecordService.getDateList(app, stream, year, month, mediaServers); + } + + @ResponseBody + @GetMapping("/list") + @Operation(summary = "分页查询云端录像", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "query", description = "检索内容", required = false) + @Parameter(name = "app", description = "应用名", required = false) + @Parameter(name = "stream", description = "流ID", required = false) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "startTime", description = "开始时间(yyyy-MM-dd HH:mm:ss)", required = false) + @Parameter(name = "endTime", description = "结束时间(yyyy-MM-dd HH:mm:ss)", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID,置空则查询全部流媒体", required = false) + @Parameter(name = "callId", description = "每次录像的唯一标识,置空则查询全部流媒体", required = false) + @Parameter(name = "ascOrder", description = "是否升序排序, 升序: true, 降序: false", required = false) + public PageInfo openRtpServer(@RequestParam(required = false) String query, + @RequestParam(required = false) String app, + @RequestParam(required = false) String stream, + @RequestParam int page, + @RequestParam int count, + @RequestParam(required = false) String startTime, + @RequestParam(required = false) String endTime, + @RequestParam(required = false) String mediaServerId, + @RequestParam(required = false) String callId, + @RequestParam(required = false) Boolean ascOrder + + ) { + log.info("[云端录像] 查询 app->{}, stream->{}, mediaServerId->{}, page->{}, count->{}, startTime->{}, endTime->{}, callId->{}", app, stream, mediaServerId, page, count, startTime, endTime, callId); + + List mediaServers; + if (!ObjectUtils.isEmpty(mediaServerId)) { + mediaServers = new ArrayList<>(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体: " + mediaServerId); + } + mediaServers.add(mediaServer); + } else { + mediaServers = null; + } + if (query != null && ObjectUtils.isEmpty(query.trim())) { + query = null; + } + if (app != null && ObjectUtils.isEmpty(app.trim())) { + app = null; + } + if (stream != null && ObjectUtils.isEmpty(stream.trim())) { + stream = null; + } + if (startTime != null && ObjectUtils.isEmpty(startTime.trim())) { + startTime = null; + } + if (endTime != null && ObjectUtils.isEmpty(endTime.trim())) { + endTime = null; + } + if (callId != null && ObjectUtils.isEmpty(callId.trim())) { + callId = null; + } + return cloudRecordService.getList(page, count, query, app, stream, startTime, endTime, mediaServers, callId, ascOrder); + } + + @ResponseBody + @GetMapping("/task/add") + @Operation(summary = "添加合并任务") + @Parameter(name = "app", description = "应用名", required = false) + @Parameter(name = "stream", description = "流ID", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID", required = false) + @Parameter(name = "startTime", description = "鉴权ID", required = false) + @Parameter(name = "endTime", description = "鉴权ID", required = false) + @Parameter(name = "callId", description = "鉴权ID", required = false) + @Parameter(name = "remoteHost", description = "返回地址时的远程地址", required = false) + public String addTask(HttpServletRequest request, @RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) String callId, @RequestParam(required = false) String remoteHost) { + MediaServer mediaServer; + if (mediaServerId == null) { + mediaServer = mediaServerService.getDefaultMediaServer(); + } else { + mediaServer = mediaServerService.getOne(mediaServerId); + } + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的流媒体"); + } else { + if (remoteHost == null) { + remoteHost = request.getScheme() + "://" + mediaServer.getIp() + ":" + mediaServer.getRecordAssistPort(); + } + } + return cloudRecordService.addTask(app, stream, mediaServer, startTime, endTime, callId, remoteHost, mediaServerId != null); + } + + @ResponseBody + @GetMapping("/task/list") + @Operation(summary = "查询合并任务") + @Parameter(name = "taskId", description = "任务Id", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID", required = false) + @Parameter(name = "isEnd", description = "是否结束", required = false) + public JSONArray queryTaskList(HttpServletRequest request, @RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam(required = false) String callId, @RequestParam(required = false) String taskId, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) Boolean isEnd) { + if (ObjectUtils.isEmpty(mediaServerId)) { + mediaServerId = null; + } + + return cloudRecordService.queryTask(app, stream, callId, taskId, mediaServerId, isEnd, request.getScheme()); + } + + @ResponseBody + @GetMapping("/collect/add") + @Operation(summary = "添加收藏") + @Parameter(name = "app", description = "应用名", required = false) + @Parameter(name = "stream", description = "流ID", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID", required = false) + @Parameter(name = "startTime", description = "鉴权ID", required = false) + @Parameter(name = "endTime", description = "鉴权ID", required = false) + @Parameter(name = "callId", description = "鉴权ID", required = false) + @Parameter(name = "recordId", description = "录像记录的ID,用于精准收藏一个视频文件", required = false) + public int addCollect(@RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) String callId, @RequestParam(required = false) Integer recordId) { + log.info("[云端录像] 添加收藏,app={},stream={},mediaServerId={},startTime={},endTime={},callId={},recordId={}", app, stream, mediaServerId, startTime, endTime, callId, recordId); + if (recordId != null) { + return cloudRecordService.changeCollectById(recordId, true); + } else { + return cloudRecordService.changeCollect(true, app, stream, mediaServerId, startTime, endTime, callId); + } + } + + @ResponseBody + @GetMapping("/collect/delete") + @Operation(summary = "移除收藏") + @Parameter(name = "app", description = "应用名", required = false) + @Parameter(name = "stream", description = "流ID", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID", required = false) + @Parameter(name = "startTime", description = "鉴权ID", required = false) + @Parameter(name = "endTime", description = "鉴权ID", required = false) + @Parameter(name = "callId", description = "鉴权ID", required = false) + @Parameter(name = "recordId", description = "录像记录的ID,用于精准精准移除一个视频文件的收藏", required = false) + public int deleteCollect(@RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) String callId, @RequestParam(required = false) Integer recordId) { + log.info("[云端录像] 移除收藏,app={},stream={},mediaServerId={},startTime={},endTime={},callId={},recordId={}", app, stream, mediaServerId, startTime, endTime, callId, recordId); + if (recordId != null) { + return cloudRecordService.changeCollectById(recordId, false); + } else { + return cloudRecordService.changeCollect(false, app, stream, mediaServerId, startTime, endTime, callId); + } + } + + @ResponseBody + @GetMapping("/play/path") + @Operation(summary = "获取播放地址") + @Parameter(name = "recordId", description = "录像记录的ID", required = true) + public DownloadFileInfo getPlayUrlPath(@RequestParam(required = true) Integer recordId) { + return cloudRecordService.getPlayUrlPath(recordId); + } + + @ResponseBody + @GetMapping("/loadRecord") + @Operation(summary = "加载录像文件形成播放地址") + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @Parameter(name = "cloudRecordId", description = "云端录像ID", required = true) + public DeferredResult> loadRecord( + HttpServletRequest request, + @RequestParam(required = true) String app, + @RequestParam(required = true) String stream, + @RequestParam(required = true) int cloudRecordId + ) { + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + log.info("[加载录像文件超时] app={}, stream={}, cloudRecordId={}", app, stream, cloudRecordId); + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("加载录像文件超时"); + result.setResult(wvpResult); + }); + + ErrorCallback callback = (code, msg, streamInfo) -> { + + WVPResult wvpResult = new WVPResult<>(); + if (code == InviteErrorCode.SUCCESS.getCode()) { + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); + + if (streamInfo != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + if (!org.springframework.util.ObjectUtils.isEmpty(streamInfo.getMediaServer().getTranscodeSuffix()) && !"null".equalsIgnoreCase(streamInfo.getMediaServer().getTranscodeSuffix())) { + streamInfo.setStream(streamInfo.getStream() + "_" + streamInfo.getMediaServer().getTranscodeSuffix()); + } + wvpResult.setData(new StreamContent(streamInfo)); + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + result.setResult(wvpResult); + }; + + cloudRecordService.loadMP4File(app, stream, cloudRecordId, callback); + return result; + } + + @ResponseBody + @GetMapping("/seek") + @Operation(summary = "定位录像播放到制定位置") + @Parameter(name = "mediaServerId", description = "使用的节点Id", required = true) + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @Parameter(name = "seek", description = "要定位的时间位置,从录像开始的时间算起", required = true) + public void seekRecord( + @RequestParam(required = true) String mediaServerId, + @RequestParam(required = true) String app, + @RequestParam(required = true) String stream, + @RequestParam(required = true) Double seek, + @RequestParam(required = false) String schema + ) { + if (schema == null) { + schema = "ts"; + } + cloudRecordService.seekRecord(mediaServerId, app, stream, seek, schema); + } + + @ResponseBody + @GetMapping("/speed") + @Operation(summary = "设置录像播放速度") + @Parameter(name = "mediaServerId", description = "使用的节点Id", required = true) + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @Parameter(name = "speed", description = "要设置的录像倍速", required = true) + public void setRecordSpeed( + @RequestParam(required = true) String mediaServerId, + @RequestParam(required = true) String app, + @RequestParam(required = true) String stream, + @RequestParam(required = true) Integer speed, + @RequestParam(required = false) String schema + ) { + if (schema == null) { + schema = "ts"; + } + + cloudRecordService.setRecordSpeed(mediaServerId, app, stream, speed, schema); + } + + @ResponseBody + @DeleteMapping("/delete") + @Operation(summary = "删除录像文件") + @Parameter(name = "ids", description = "文件ID集合", required = true) + public void deleteFileByIds(@RequestBody BatchRemoveParam ids) { + cloudRecordService.deleteFileByIds(ids.getIds()); + } + + @ResponseBody + @GetMapping("/download/zip") + public void downloadZipFileFromUrl(HttpServletResponse response, Integer[] ids) { + log.info("[下载指定录像文件的压缩包] 查询 ids->{}", ids); + List arrayList = new ArrayList<>(List.of(ids)); + List cloudRecordItemList = cloudRecordService.getUrlListByIds(arrayList); + if (ObjectUtils.isEmpty(cloudRecordItemList)) { + log.warn("[下载指定录像文件的压缩包] 未找到录像文件,ids->{}", ids); + return; + } + + // 设置响应头 + response.setContentType("application/zip"); + response.setCharacterEncoding("UTF-8"); + response.setHeader("Content-Disposition", "attachment; filename=record_" + System.currentTimeMillis() + ".zip"); + + try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) { + for (CloudRecordUrl recordUrl : cloudRecordItemList) { + try { + zos.putNextEntry(new ZipEntry(recordUrl.getFileName())); + boolean downloadSuccess = HttpUtils.downLoadFile(recordUrl.getDownloadUrl(), zos); + if (!downloadSuccess) { + log.warn("[下载指定录像文件的压缩包] 下载文件失败: {}", recordUrl.getDownloadUrl()); + zos.closeEntry(); + continue; + } + +// try (FileInputStream fis = new FileInputStream(recordUrl.getFilePath())) { +// byte[] buf = new byte[8192]; // 8KB 缓冲区,提高性能 +// int len; +// while ((len = fis.read(buf)) != -1) { +// zos.write(buf, 0, len); +// } +// } + + zos.closeEntry(); + } catch (Exception e) { + log.error("[下载指定录像文件的压缩包] 处理文件失败: {}, 错误: {}", recordUrl.getFileName(), e.getMessage()); + // 继续处理下一个文件 + } + } + } catch (IOException e) { + log.error("[下载指定录像文件的压缩包] 创建压缩包失败,查询 ids->{}", ids, e); + } + } + + + + + + /************************* 以下这些接口只适合wvp和zlm部署在同一台服务器的情况,且wvp只有一个zlm节点的情况 ***************************************/ + + /** + * 下载指定录像文件的压缩包 + * @param query 检索内容 + * @param app 应用名 + * @param stream 流ID + * @param startTime 开始时间(yyyy-MM-dd HH:mm:ss) + * @param endTime 结束时间(yyyy-MM-dd HH:mm:ss) + * @param mediaServerId 流媒体ID,置空则查询全部流媒体 + * @param callId 每次录像的唯一标识,置空则查询全部流媒体 + * @param ids 指定的Id + */ + @ResponseBody + @GetMapping("/zip") + public void downloadZipFile(HttpServletResponse response, @RequestParam(required = false) String query, @RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) String callId, @RequestParam(required = false) List ids + + ) { + log.info("[下载指定录像文件的压缩包] 查询 app->{}, stream->{}, mediaServerId->{}, startTime->{}, endTime->{}, callId->{}", app, stream, mediaServerId, startTime, endTime, callId); + + List mediaServers; + if (!ObjectUtils.isEmpty(mediaServerId)) { + mediaServers = new ArrayList<>(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体: " + mediaServerId); + } + mediaServers.add(mediaServer); + } else { + mediaServers = mediaServerService.getAll(); + } + if (mediaServers.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "当前无流媒体"); + } + if (query != null && ObjectUtils.isEmpty(query.trim())) { + query = null; + } + if (app != null && ObjectUtils.isEmpty(app.trim())) { + app = null; + } + if (stream != null && ObjectUtils.isEmpty(stream.trim())) { + stream = null; + } + if (startTime != null && ObjectUtils.isEmpty(startTime.trim())) { + startTime = null; + } + if (endTime != null && ObjectUtils.isEmpty(endTime.trim())) { + endTime = null; + } + if (callId != null && ObjectUtils.isEmpty(callId.trim())) { + callId = null; + } + // 设置响应头 + response.setContentType("application/zip"); + response.setCharacterEncoding("UTF-8"); + if (stream != null && callId != null) { + response.setHeader("Content-Disposition", "attachment;filename=" + stream + "_" + callId + ".zip"); + } else { + response.setHeader("Content-Disposition", "attachment;filename=cloud_record_" + System.currentTimeMillis() + ".zip"); + } + List cloudRecordItemList = cloudRecordService.getAllList(query, app, stream, startTime, endTime, mediaServers, callId, ids); + if (ObjectUtils.isEmpty(cloudRecordItemList)) { + return; + } + try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) { + for (CloudRecordItem cloudRecordItem : cloudRecordItemList) { + try { + String fileName = DateUtil.timestampMsToUrlToyyyy_MM_dd_HH_mm_ss((long)cloudRecordItem.getStartTime()) + ".mp4"; + zos.putNextEntry(new ZipEntry(fileName)); + + File file = new File(cloudRecordItem.getFilePath()); + if (!file.exists() || file.isDirectory()) { + log.warn("[下载指定录像文件的压缩包] 文件不存在或为目录: {}", cloudRecordItem.getFilePath()); + zos.closeEntry(); + continue; + } + + try (FileInputStream fis = new FileInputStream(cloudRecordItem.getFilePath())) { + byte[] buf = new byte[8192]; // 8KB 缓冲区,提高性能 + int len; + while ((len = fis.read(buf)) != -1) { + zos.write(buf, 0, len); + } + } + zos.closeEntry(); + log.debug("[下载指定录像文件的压缩包] 成功添加文件: {}", fileName); + } catch (Exception e) { + log.error("[下载指定录像文件的压缩包] 处理文件失败: {}, 错误: {}", cloudRecordItem.getFilePath(), e.getMessage()); + // 继续处理下一个文件 + } + } + } catch (IOException e) { + log.error("[下载指定录像文件的压缩包] 失败: 查询 app->{}, stream->{}, mediaServerId->{}, startTime->{}, endTime->{}, callId->{}", app, stream, mediaServerId, startTime, endTime, callId, e); + } + } + + /** + * + * @param query 检索内容 + * @param app 应用名 + * @param stream 流ID + * @param startTime 开始时间(yyyy-MM-dd HH:mm:ss) + * @param endTime 结束时间(yyyy-MM-dd HH:mm:ss) + * @param mediaServerId 流媒体ID,置空则查询全部流媒体 + * @param callId 每次录像的唯一标识,置空则查询全部流媒体 + * @param remoteHost 拼接播放地址时使用的远程地址 + */ + @ResponseBody + @GetMapping("/list-url") + @Operation(summary = "分页查询云端录像", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "query", description = "检索内容", required = false) + @Parameter(name = "app", description = "应用名", required = false) + @Parameter(name = "stream", description = "流ID", required = false) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "startTime", description = "开始时间(yyyy-MM-dd HH:mm:ss)", required = false) + @Parameter(name = "endTime", description = "结束时间(yyyy-MM-dd HH:mm:ss)", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID,置空则查询全部流媒体", required = false) + @Parameter(name = "callId", description = "每次录像的唯一标识,置空则查询全部流媒体", required = false) + public PageInfo getListWithUrl(HttpServletRequest request, @RequestParam(required = false) String query, @RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam int page, @RequestParam int count, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) String callId, @RequestParam(required = false) String remoteHost + + ) { + log.info("[云端录像] 查询URL app->{}, stream->{}, mediaServerId->{}, page->{}, count->{}, startTime->{}, endTime->{}, callId->{}", app, stream, mediaServerId, page, count, startTime, endTime, callId); + + List mediaServers; + if (!ObjectUtils.isEmpty(mediaServerId)) { + mediaServers = new ArrayList<>(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体: " + mediaServerId); + } + mediaServers.add(mediaServer); + } else { + mediaServers = null; + } + if (query != null && ObjectUtils.isEmpty(query.trim())) { + query = null; + } + if (app != null && ObjectUtils.isEmpty(app.trim())) { + app = null; + } + if (stream != null && ObjectUtils.isEmpty(stream.trim())) { + stream = null; + } + if (startTime != null && ObjectUtils.isEmpty(startTime.trim())) { + startTime = null; + } + if (endTime != null && ObjectUtils.isEmpty(endTime.trim())) { + endTime = null; + } + if (callId != null && ObjectUtils.isEmpty(callId.trim())) { + callId = null; + } + MediaServer mediaServer = mediaServerService.getDefaultMediaServer(); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体节点"); + } + if (remoteHost == null) { + remoteHost = request.getScheme() + "://" + request.getLocalAddr() + ":" + (request.getScheme().equals("https") ? mediaServer.getHttpSSlPort() : mediaServer.getHttpPort()); + } + PageInfo cloudRecordItemPageInfo = cloudRecordService.getList(page, count, query, app, stream, startTime, endTime, mediaServers, callId, null); + PageInfo cloudRecordUrlPageInfo = new PageInfo<>(); + if (!ObjectUtils.isEmpty(cloudRecordItemPageInfo)) { + cloudRecordUrlPageInfo.setPageNum(cloudRecordItemPageInfo.getPageNum()); + cloudRecordUrlPageInfo.setPageSize(cloudRecordItemPageInfo.getPageSize()); + cloudRecordUrlPageInfo.setSize(cloudRecordItemPageInfo.getSize()); + cloudRecordUrlPageInfo.setEndRow(cloudRecordItemPageInfo.getEndRow()); + cloudRecordUrlPageInfo.setStartRow(cloudRecordItemPageInfo.getStartRow()); + cloudRecordUrlPageInfo.setPages(cloudRecordItemPageInfo.getPages()); + cloudRecordUrlPageInfo.setPrePage(cloudRecordItemPageInfo.getPrePage()); + cloudRecordUrlPageInfo.setNextPage(cloudRecordItemPageInfo.getNextPage()); + cloudRecordUrlPageInfo.setIsFirstPage(cloudRecordItemPageInfo.isIsFirstPage()); + cloudRecordUrlPageInfo.setIsLastPage(cloudRecordItemPageInfo.isIsLastPage()); + cloudRecordUrlPageInfo.setHasPreviousPage(cloudRecordItemPageInfo.isHasPreviousPage()); + cloudRecordUrlPageInfo.setHasNextPage(cloudRecordItemPageInfo.isHasNextPage()); + cloudRecordUrlPageInfo.setNavigatePages(cloudRecordItemPageInfo.getNavigatePages()); + cloudRecordUrlPageInfo.setNavigateFirstPage(cloudRecordItemPageInfo.getNavigateFirstPage()); + cloudRecordUrlPageInfo.setNavigateLastPage(cloudRecordItemPageInfo.getNavigateLastPage()); + cloudRecordUrlPageInfo.setNavigatepageNums(cloudRecordItemPageInfo.getNavigatepageNums()); + cloudRecordUrlPageInfo.setTotal(cloudRecordItemPageInfo.getTotal()); + List cloudRecordUrlList = new ArrayList<>(cloudRecordItemPageInfo.getList().size()); + List cloudRecordItemList = cloudRecordItemPageInfo.getList(); + for (CloudRecordItem cloudRecordItem : cloudRecordItemList) { + CloudRecordUrl cloudRecordUrl = new CloudRecordUrl(); + cloudRecordUrl.setId(cloudRecordItem.getId()); + cloudRecordUrl.setDownloadUrl(remoteHost + "/index/api/downloadFile?file_path=" + cloudRecordItem.getFilePath() + "&save_name=" + cloudRecordItem.getStream() + "_" + cloudRecordItem.getCallId() + "_" + DateUtil.timestampMsToUrlToyyyy_MM_dd_HH_mm_ss((long)cloudRecordItem.getStartTime())); + cloudRecordUrl.setPlayUrl(remoteHost + "/index/api/downloadFile?file_path=" + cloudRecordItem.getFilePath()); + cloudRecordUrlList.add(cloudRecordUrl); + } + cloudRecordUrlPageInfo.setList(cloudRecordUrlList); + } + return cloudRecordUrlPageInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/cloudRecord/bean/CloudRecordUrl.java b/src/main/java/com/genersoft/iot/vmp/vmanager/cloudRecord/bean/CloudRecordUrl.java new file mode 100644 index 0000000..bb75a09 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/cloudRecord/bean/CloudRecordUrl.java @@ -0,0 +1,16 @@ +package com.genersoft.iot.vmp.vmanager.cloudRecord.bean; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class CloudRecordUrl { + + private String filePath; + private String playUrl; + private String downloadUrl; + private String fileName; + private int id; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/log/LogController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/log/LogController.java new file mode 100755 index 0000000..58f861a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/log/LogController.java @@ -0,0 +1,81 @@ +package com.genersoft.iot.vmp.vmanager.log; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.service.ILogService; +import com.genersoft.iot.vmp.service.bean.LogFileInfo; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.List; + +@SuppressWarnings("rawtypes") +@Tag(name = "日志文件查询接口") +@Slf4j +@RestController +@RequestMapping("/api/log") +public class LogController { + + @Autowired + private ILogService logService; + + + @ResponseBody + @GetMapping("/list") + @Operation(summary = "分页查询日志文件", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "query", description = "检索内容", required = false) + @Parameter(name = "startTime", description = "开始时间(yyyy-MM-dd HH:mm:ss)", required = false) + @Parameter(name = "endTime", description = "结束时间(yyyy-MM-dd HH:mm:ss)", required = false) + public List queryList(@RequestParam(required = false) String query, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime + + ) { + if (ObjectUtils.isEmpty(query)) { + query = null; + } + if (ObjectUtils.isEmpty(startTime)) { + startTime = null; + } + if (ObjectUtils.isEmpty(endTime)) { + endTime = null; + } + return logService.queryList(query, startTime, endTime); + } + + /** + * 下载指定日志文件 + */ + @ResponseBody + @GetMapping("/file/{fileName}") + public void downloadFile(HttpServletResponse response, @PathVariable String fileName) { + try { + File file = logService.getFileByName(fileName); + if (file == null || !file.exists() || !file.isFile()) { + throw new ControllerException(ErrorCode.ERROR400); + } + final InputStream in = Files.newInputStream(file.toPath()); + response.setContentType(MediaType.TEXT_PLAIN_VALUE); + ServletOutputStream outputStream = response.getOutputStream(); + IOUtils.copy(in, response.getOutputStream()); + in.close(); + outputStream.close(); + } catch (IOException e) { + response.setStatus(HttpServletResponse.SC_NO_CONTENT); + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/ps/PsController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/ps/PsController.java new file mode 100755 index 0000000..efffbf9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/ps/PsController.java @@ -0,0 +1,281 @@ +package com.genersoft.iot.vmp.vmanager.ps; + +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import com.genersoft.iot.vmp.utils.redis.RedisUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.OtherPsSendInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@SuppressWarnings("rawtypes") +@Tag(name = "第三方PS服务对接") +@Slf4j +@RestController +@RequestMapping("/api/ps") +public class PsController { + + @Autowired + private HookSubscribe hookSubscribe; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private DynamicTask dynamicTask; + + + @Autowired + private RedisTemplate redisTemplate; + + + @GetMapping(value = "/receive/open") + @ResponseBody + @Operation(summary = "开启收流和获取发流信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "isSend", description = "是否发送,false时只开启收流, true同时返回推流信息", required = true) + @Parameter(name = "callId", description = "整个过程的唯一标识,为了与后续接口关联", required = true) + @Parameter(name = "ssrc", description = "来源流的SSRC,不传则不校验来源ssrc", required = false) + @Parameter(name = "stream", description = "形成的流的ID", required = true) + @Parameter(name = "tcpMode", description = "收流模式, 0为UDP, 1为TCP被动", required = true) + @Parameter(name = "callBack", description = "回调地址,如果收流超时会通道回调通知,回调为get请求,参数为callId", required = true) + public OtherPsSendInfo openRtpServer(Boolean isSend, @RequestParam(required = false)String ssrc, String callId, String stream, Integer tcpMode, String callBack) { + + log.info("[第三方PS服务对接->开启收流和获取发流信息] isSend->{}, ssrc->{}, callId->{}, stream->{}, tcpMode->{}, callBack->{}", + isSend, ssrc, callId, stream, tcpMode==0?"UDP":"TCP被动", callBack); + + MediaServer mediaServer = mediaServerService.getDefaultMediaServer(); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"没有可用的MediaServer"); + } + if (stream == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"stream参数不可为空"); + } + if (isSend != null && isSend && callId == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"isSend为true时,CallID不能为空"); + } + long ssrcInt = 0; + if (ssrc != null) { + try { + ssrcInt = Long.parseLong(ssrc); + }catch (NumberFormatException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"ssrc格式错误"); + } + } + String receiveKey = VideoManagerConstants.WVP_OTHER_RECEIVE_PS_INFO + userSetting.getServerId() + "_" + callId + "_" + stream; + SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServer, stream, ssrcInt + "", false, false, null, false, false, false, tcpMode); + + if (ssrcInfo.getPort() == 0) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "获取端口失败"); + } + // 注册回调如果rtp收流超时则通过回调发送通知 + if (callBack != null) { + Hook hook = Hook.getInstance(HookType.on_rtp_server_timeout, "rtp", stream, mediaServer.getId()); + // 订阅 zlm启动事件, 新的zlm也会从这里进入系统 + hookSubscribe.addSubscribe(hook, + (hookData)->{ + if (stream.equals(hookData.getStream())) { + log.info("[第三方PS服务对接->开启收流和获取发流信息] 等待收流超时 callId->{}, 发送回调", callId); + // 将信息写入redis中,以备后用 + redisTemplate.delete(receiveKey); + OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); + OkHttpClient client = httpClientBuilder.build(); + String url = callBack + "?callId=" + callId; + Request request = new Request.Builder().get().url(url).build(); + try { + client.newCall(request).execute(); + } catch (IOException e) { + log.error("[第三方PS服务对接->开启收流和获取发流信息] 等待收流超时 callId->{}, 发送回调失败", callId, e); + } + hookSubscribe.removeSubscribe(hook); + } + }); + } + OtherPsSendInfo otherPsSendInfo = new OtherPsSendInfo(); + otherPsSendInfo.setReceiveIp(mediaServer.getSdpIp()); + otherPsSendInfo.setReceivePort(ssrcInfo.getPort()); + otherPsSendInfo.setCallId(callId); + otherPsSendInfo.setStream(stream); + + // 将信息写入redis中,以备后用 + redisTemplate.opsForValue().set(receiveKey, otherPsSendInfo); + if (isSend != null && isSend) { + String key = VideoManagerConstants.WVP_OTHER_SEND_PS_INFO + userSetting.getServerId() + "_" + callId; + // 预创建发流信息 + int port = sendRtpServerService.getNextPort(mediaServer); + + otherPsSendInfo.setSendLocalIp(mediaServer.getSdpIp()); + otherPsSendInfo.setSendLocalPort(port); + // 将信息写入redis中,以备后用 + redisTemplate.opsForValue().set(key, otherPsSendInfo, 300, TimeUnit.SECONDS); + log.info("[第三方PS服务对接->开启收流和获取发流信息] 结果,callId->{}, {}", callId, otherPsSendInfo); + } + return otherPsSendInfo; + } + + @GetMapping(value = "/receive/close") + @ResponseBody + @Operation(summary = "关闭收流", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "stream", description = "流的ID", required = true) + public void closeRtpServer(String stream) { + log.info("[第三方PS服务对接->关闭收流] stream->{}", stream); + MediaServer mediaServerItem = mediaServerService.getDefaultMediaServer(); + mediaServerService.closeRTPServer(mediaServerItem, stream); + String receiveKey = VideoManagerConstants.WVP_OTHER_RECEIVE_PS_INFO + userSetting.getServerId() + "_*_" + stream; + List scan = RedisUtil.scan(redisTemplate, receiveKey); + if (!scan.isEmpty()) { + for (Object key : scan) { + // 将信息写入redis中,以备后用 + redisTemplate.delete(key); + } + } + } + + @GetMapping(value = "/send/start") + @ResponseBody + @Operation(summary = "发送流", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "ssrc", description = "发送流的SSRC", required = true) + @Parameter(name = "dstIp", description = "目标收流IP", required = true) + @Parameter(name = "dstPort", description = "目标收流端口", required = true) + @Parameter(name = "app", description = "待发送应用名", required = true) + @Parameter(name = "stream", description = "待发送流Id", required = true) + @Parameter(name = "callId", description = "整个过程的唯一标识,不传则使用随机端口发流", required = true) + @Parameter(name = "isUdp", description = "是否为UDP", required = true) + public void sendRTP(String ssrc, + String dstIp, + Integer dstPort, + String app, + String stream, + String callId, + Boolean isUdp + ) { + log.info("[第三方PS服务对接->发送流] " + + "ssrc->{}, \r\n" + + "dstIp->{}, \n" + + "dstPort->{}, \n" + + "app->{}, \n" + + "stream->{}, \n" + + "callId->{} \n", + ssrc, + dstIp, + dstPort, + app, + stream, + callId); + MediaServer mediaServer = mediaServerService.getDefaultMediaServer(); + String key = VideoManagerConstants.WVP_OTHER_SEND_PS_INFO + userSetting.getServerId() + "_" + callId; + OtherPsSendInfo sendInfo = (OtherPsSendInfo)redisTemplate.opsForValue().get(key); + if (sendInfo == null) { + sendInfo = new OtherPsSendInfo(); + } + sendInfo.setPushApp(app); + sendInfo.setPushStream(stream); + sendInfo.setPushSSRC(ssrc); + SendRtpInfo sendRtpItem = SendRtpInfo.getInstance(app, stream, ssrc, dstIp, dstPort, !isUdp, sendInfo.getSendLocalPort(), null); + Boolean streamReady = mediaServerService.isStreamReady(mediaServer, app, stream); + if (streamReady) { + mediaServerService.startSendRtp(mediaServer, sendRtpItem); + log.info("[第三方PS服务对接->发送流] 视频流发流成功,callId->{},param->{}", callId, sendRtpItem); + redisTemplate.opsForValue().set(key, sendInfo); + }else { + log.info("[第三方PS服务对接->发送流] 流不存在,等待流上线,callId->{}", callId); + String uuid = UUID.randomUUID().toString(); + Hook hook = Hook.getInstance(HookType.on_media_arrival, app, stream, mediaServer.getId()); + dynamicTask.startDelay(uuid, ()->{ + log.info("[第三方PS服务对接->发送流] 等待流上线超时 callId->{}", callId); + redisTemplate.delete(key); + hookSubscribe.removeSubscribe(hook); + }, 10000); + + // 订阅 zlm启动事件, 新的zlm也会从这里进入系统 + OtherPsSendInfo finalSendInfo = sendInfo; + hookSubscribe.removeSubscribe(hook); + hookSubscribe.addSubscribe(hook, + (hookData)->{ + dynamicTask.stop(uuid); + log.info("[第三方PS服务对接->发送流] 流上线,开始发流 callId->{}", callId); + try { + Thread.sleep(400); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + mediaServerService.startSendRtp(mediaServer, sendRtpItem); + log.info("[第三方PS服务对接->发送流] 视频流发流成功,callId->{},param->{}", callId, sendRtpItem); + redisTemplate.opsForValue().set(key, finalSendInfo); + hookSubscribe.removeSubscribe(hook); + }); + } + } + + @GetMapping(value = "/send/stop") + @ResponseBody + @Operation(summary = "关闭发送流") + @Parameter(name = "callId", description = "整个过程的唯一标识,不传则使用随机端口发流", required = true) + public void closeSendRTP(String callId) { + log.info("[第三方PS服务对接->关闭发送流] callId->{}", callId); + String key = VideoManagerConstants.WVP_OTHER_SEND_PS_INFO + userSetting.getServerId() + "_" + callId; + OtherPsSendInfo sendInfo = (OtherPsSendInfo)redisTemplate.opsForValue().get(key); + if (sendInfo == null){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未开启发流"); + } + MediaServer mediaServerItem = mediaServerService.getDefaultMediaServer(); + boolean result = mediaServerService.stopSendRtp(mediaServerItem, sendInfo.getPushApp(), sendInfo.getStream(), sendInfo.getPushSSRC()); + if (!result) { + log.info("[第三方PS服务对接->关闭发送流] 失败 callId->{}", callId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "停止发流失败"); + }else { + log.info("[第三方PS服务对接->关闭发送流] 成功 callId->{}", callId); + } + redisTemplate.delete(key); + } + + + @GetMapping(value = "/getTestPort") + @ResponseBody + public int getTestPort() { + MediaServer defaultMediaServer = mediaServerService.getDefaultMediaServer(); + +// for (int i = 0; i <300; i++) { +// new Thread(() -> { +// int nextPort = sendRtpPortManager.getNextPort(defaultMediaServer); +// try { +// Thread.sleep((int)Math.random()*10); +// } catch (InterruptedException e) { +// throw new RuntimeException(e); +// } +// System.out.println(nextPort); +// }).start(); +// } + + return sendRtpServerService.getNextPort(defaultMediaServer); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java new file mode 100644 index 0000000..8833326 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java @@ -0,0 +1,150 @@ +package com.genersoft.iot.vmp.vmanager.recordPlan; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.service.IRecordPlanService; +import com.genersoft.iot.vmp.service.bean.RecordPlan; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.recordPlan.bean.RecordPlanParam; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; + +@Tag(name = "录制计划") +@Slf4j +@RestController +@RequestMapping("/api/record/plan") +public class RecordPlanController { + + @Autowired + private IRecordPlanService recordPlanService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + + @ResponseBody + @PostMapping("/add") + @Operation(summary = "添加录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "plan", description = "计划", required = true) + public void add(@RequestBody RecordPlan plan) { + if (plan.getPlanItemList() == null || plan.getPlanItemList().isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "添加录制计划时,录制计划不可为空"); + } + recordPlanService.add(plan); + } + + @ResponseBody + @PostMapping("/link") + @Operation(summary = "通道关联录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "param", description = "通道关联录制计划", required = true) + public void link(@RequestBody RecordPlanParam param) { + if (param.getAllLink() != null) { + if (param.getAllLink()) { + recordPlanService.linkAll(param.getPlanId()); + }else { + recordPlanService.cleanAll(param.getPlanId()); + } + return; + } + + if (param.getChannelIds() == null && param.getDeviceDbIds() == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道ID和国标设备ID不可都为NULL"); + } + + List channelIds = new ArrayList<>(); + if (param.getChannelIds() != null) { + channelIds.addAll(param.getChannelIds()); + }else { + List chanelIdList = deviceChannelService.queryChaneIdListByDeviceDbIds(param.getDeviceDbIds()); + if (chanelIdList != null && !chanelIdList.isEmpty()) { + channelIds = chanelIdList; + } + } + recordPlanService.link(channelIds, param.getPlanId()); + } + + @ResponseBody + @GetMapping("/get") + @Operation(summary = "查询录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "planId", description = "计划ID", required = true) + public RecordPlan get(Integer planId) { + if (planId == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "计划ID不可为NULL"); + } + return recordPlanService.get(planId); + } + + @ResponseBody + @GetMapping("/query") + @Operation(summary = "查询录制计划列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "query", description = "检索内容", required = false) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + public PageInfo query(@RequestParam(required = false) String query, @RequestParam Integer page, @RequestParam Integer count) { + if (query != null && ObjectUtils.isEmpty(query.trim())) { + query = null; + } + return recordPlanService.query(page, count, query); + } + + @Operation(summary = "分页查询录制计划关联的所有通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页条数", required = true) + @Parameter(name = "planId", description = "录制计划ID") + @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理") + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "hasLink", description = "是否已经关联") + @GetMapping("/channel/list") + @ResponseBody + public PageInfo queryChannelList(int page, int count, + @RequestParam(required = false) Integer planId, + @RequestParam(required = false) String query, + @RequestParam(required = false) Integer channelType, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Boolean hasLink) { + + Assert.notNull(planId, "录制计划ID不可为NULL"); + if (org.springframework.util.ObjectUtils.isEmpty(query)) { + query = null; + } + + return recordPlanService.queryChannelList(page, count, query, channelType, online, planId, hasLink); + } + + @ResponseBody + @PostMapping("/update") + @Operation(summary = "更新录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "plan", description = "计划", required = true) + public void update(@RequestBody RecordPlan plan) { + if (plan == null || plan.getId() == 0) { + throw new ControllerException(ErrorCode.ERROR400); + } + recordPlanService.update(plan); + } + + @ResponseBody + @DeleteMapping("/delete") + @Operation(summary = "删除录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "planId", description = "计划ID", required = true) + public void delete(Integer planId) { + if (planId == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "计划IDID不可为NULL"); + } + recordPlanService.delete(planId); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java new file mode 100644 index 0000000..9f56a0f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.vmanager.recordPlan.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "录制计划-添加/编辑参数") +public class RecordPlanParam { + + @Schema(description = "关联的通道ID") + private List channelIds; + + @Schema(description = "关联的设备ID,会为设备下的所有通道关联此录制计划,channelId存在是此项不生效,") + private List deviceDbIds; + + @Schema(description = "全部关联/全部取消关联") + private Boolean allLink; + + @Schema(description = "录制计划ID, ID为空是删除关联的计划") + private Integer planId; +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/rtp/RtpController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/rtp/RtpController.java new file mode 100755 index 0000000..b78e9a6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/rtp/RtpController.java @@ -0,0 +1,307 @@ +package com.genersoft.iot.vmp.vmanager.rtp; + +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import com.genersoft.iot.vmp.utils.redis.RedisUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.OtherRtpSendInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@SuppressWarnings("rawtypes") +@Tag(name = "第三方服务对接") +@Slf4j +@RestController +@RequestMapping("/api/rtp") +public class RtpController { + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private HookSubscribe hookSubscribe; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private RedisTemplate redisTemplate; + + + @GetMapping(value = "/receive/open") + @ResponseBody + @Operation(summary = "开启收流和获取发流信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "isSend", description = "是否发送,false时只开启收流, true同时返回推流信息", required = true) + @Parameter(name = "callId", description = "整个过程的唯一标识,为了与后续接口关联", required = true) + @Parameter(name = "ssrc", description = "来源流的SSRC,不传则不校验来源ssrc", required = false) + @Parameter(name = "stream", description = "形成的流的ID", required = true) + @Parameter(name = "tcpMode", description = "收流模式, 0为UDP, 1为TCP被动", required = true) + @Parameter(name = "callBack", description = "回调地址,如果收流超时会通道回调通知,回调为get请求,参数为callId", required = true) + public OtherRtpSendInfo openRtpServer(Boolean isSend, @RequestParam(required = false)String ssrc, String callId, String stream, Integer tcpMode, String callBack) { + + log.info("[第三方服务对接->开启收流和获取发流信息] isSend->{}, ssrc->{}, callId->{}, stream->{}, tcpMode->{}, callBack->{}", + isSend, ssrc, callId, stream, tcpMode==0?"UDP":"TCP被动", callBack); + + MediaServer mediaServer = mediaServerService.getDefaultMediaServer(); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"没有可用的MediaServer"); + } + if (stream == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"stream参数不可为空"); + } + if (isSend != null && isSend && callId == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"isSend为true时,CallID不能为空"); + } + long ssrcInt = 0; + if (ssrc != null) { + try { + ssrcInt = Long.parseLong(ssrc); + }catch (NumberFormatException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"ssrc格式错误"); + } + } + String receiveKey = VideoManagerConstants.WVP_OTHER_RECEIVE_RTP_INFO + userSetting.getServerId() + "_" + callId + "_" + stream; + SSRCInfo ssrcInfoForVideo = mediaServerService.openRTPServer(mediaServer, stream, ssrcInt + "",false,false, null, false, false, false, tcpMode); + SSRCInfo ssrcInfoForAudio = mediaServerService.openRTPServer(mediaServer, stream + "_a", ssrcInt + "", false, false, null, false,false,false, tcpMode); + if (ssrcInfoForVideo.getPort() == 0 || ssrcInfoForAudio.getPort() == 0) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "获取端口失败"); + } + // 注册回调如果rtp收流超时则通过回调发送通知 + if (callBack != null) { + Hook hook = Hook.getInstance(HookType.on_rtp_server_timeout, "rtp", stream, mediaServer.getId()); + // 订阅 zlm启动事件, 新的zlm也会从这里进入系统 + hookSubscribe.addSubscribe(hook, + (hookData)->{ + if (stream.equals(hookData.getStream())) { + log.info("[开启收流和获取发流信息] 等待收流超时 callId->{}, 发送回调", callId); + OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); + OkHttpClient client = httpClientBuilder.build(); + String url = callBack + "?callId=" + callId; + Request request = new Request.Builder().get().url(url).build(); + try { + client.newCall(request).execute(); + } catch (IOException e) { + log.error("[第三方服务对接->开启收流和获取发流信息] 等待收流超时 callId->{}, 发送回调失败", callId, e); + } + hookSubscribe.removeSubscribe(hook); + } + }); + } + String key = VideoManagerConstants.WVP_OTHER_SEND_RTP_INFO + userSetting.getServerId() + "_" + callId; + OtherRtpSendInfo otherRtpSendInfo = new OtherRtpSendInfo(); + otherRtpSendInfo.setReceiveIp(mediaServer.getSdpIp()); + otherRtpSendInfo.setReceivePortForVideo(ssrcInfoForVideo.getPort()); + otherRtpSendInfo.setReceivePortForAudio(ssrcInfoForAudio.getPort()); + otherRtpSendInfo.setCallId(callId); + otherRtpSendInfo.setStream(stream); + + // 将信息写入redis中,以备后用 + redisTemplate.opsForValue().set(receiveKey, otherRtpSendInfo); + if (isSend != null && isSend) { + // 预创建发流信息 + int portForVideo = sendRtpServerService.getNextPort(mediaServer); + int portForAudio = sendRtpServerService.getNextPort(mediaServer); + + otherRtpSendInfo.setSendLocalIp(mediaServer.getSdpIp()); + otherRtpSendInfo.setSendLocalPortForVideo(portForVideo); + otherRtpSendInfo.setSendLocalPortForAudio(portForAudio); + // 将信息写入redis中,以备后用 + redisTemplate.opsForValue().set(key, otherRtpSendInfo, 300, TimeUnit.SECONDS); + log.info("[第三方服务对接->开启收流和获取发流信息] 结果,callId->{}, {}", callId, otherRtpSendInfo); + } + // 将信息写入redis中,以备后用 + redisTemplate.opsForValue().set(key, otherRtpSendInfo, 300, TimeUnit.SECONDS); + return otherRtpSendInfo; + } + + @GetMapping(value = "/receive/close") + @ResponseBody + @Operation(summary = "关闭收流", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "stream", description = "流的ID", required = true) + public void closeRtpServer(String stream) { + log.info("[第三方服务对接->关闭收流] stream->{}", stream); + MediaServer mediaServerItem = mediaServerService.getDefaultMediaServer(); + mediaServerService.closeRTPServer(mediaServerItem, stream); + mediaServerService.closeRTPServer(mediaServerItem, stream+ "_a"); + String receiveKey = VideoManagerConstants.WVP_OTHER_RECEIVE_RTP_INFO + userSetting.getServerId() + "_*_" + stream; + List scan = RedisUtil.scan(redisTemplate, receiveKey); + if (scan.size() > 0) { + for (Object key : scan) { + // 将信息写入redis中,以备后用 + redisTemplate.delete(key); + } + } + } + + @GetMapping(value = "/send/start") + @ResponseBody + @Operation(summary = "发送流", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "ssrc", description = "发送流的SSRC", required = true) + @Parameter(name = "dstIpForAudio", description = "目标音频收流IP", required = false) + @Parameter(name = "dstIpForVideo", description = "目标视频收流IP", required = false) + @Parameter(name = "dstPortForAudio", description = "目标音频收流端口", required = false) + @Parameter(name = "dstPortForVideo", description = "目标视频收流端口", required = false) + @Parameter(name = "app", description = "待发送应用名", required = true) + @Parameter(name = "stream", description = "待发送流Id", required = true) + @Parameter(name = "callId", description = "整个过程的唯一标识,不传则使用随机端口发流", required = true) + @Parameter(name = "isUdp", description = "是否为UDP", required = true) + @Parameter(name = "ptForAudio", description = "rtp的音频pt", required = false) + @Parameter(name = "ptForVideo", description = "rtp的视频pt", required = false) + public void sendRTP(String ssrc, + @RequestParam(required = false)String dstIpForAudio, + @RequestParam(required = false)String dstIpForVideo, + @RequestParam(required = false)Integer dstPortForAudio, + @RequestParam(required = false)Integer dstPortForVideo, + String app, + String stream, + String callId, + Boolean isUdp, + @RequestParam(required = false)Integer ptForAudio, + @RequestParam(required = false)Integer ptForVideo + ) { + log.info("[第三方服务对接->发送流] " + + "ssrc->{}, \r\n" + + "dstIpForAudio->{}, \n" + + "dstIpForAudio->{}, \n" + + "dstPortForAudio->{}, \n" + + "dstPortForVideo->{}, \n" + + "app->{}, \n" + + "stream->{}, \n" + + "callId->{}, \n" + + "ptForAudio->{}, \n" + + "ptForVideo->{}", + ssrc, + dstIpForAudio, + dstIpForVideo, + dstPortForAudio, + dstPortForVideo, + app, + stream, + callId, + ptForAudio, + ptForVideo); + if (!((dstPortForAudio > 0 && !ObjectUtils.isEmpty(dstPortForAudio) || (dstPortForVideo > 0 && !ObjectUtils.isEmpty(dstIpForVideo))))) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "至少应该存在一组音频或视频发送参数"); + } + MediaServer mediaServer = mediaServerService.getDefaultMediaServer(); + String key = VideoManagerConstants.WVP_OTHER_SEND_RTP_INFO + userSetting.getServerId() + "_" + callId; + OtherRtpSendInfo sendInfo = (OtherRtpSendInfo)redisTemplate.opsForValue().get(key); + if (sendInfo == null) { + sendInfo = new OtherRtpSendInfo(); + } + sendInfo.setPushApp(app); + sendInfo.setPushStream(stream); + sendInfo.setPushSSRC(ssrc); + + + SendRtpInfo sendRtpItemForVideo; + SendRtpInfo sendRtpItemForAudio; + if (!ObjectUtils.isEmpty(dstIpForAudio) && dstPortForAudio > 0) { + sendRtpItemForAudio = SendRtpInfo.getInstance(app, stream, ssrc, dstIpForAudio, dstPortForAudio, !isUdp, sendInfo.getSendLocalPortForAudio(), ptForAudio); + } else { + sendRtpItemForAudio = null; + } + if (!ObjectUtils.isEmpty(dstIpForVideo) && dstPortForVideo > 0) { + sendRtpItemForVideo = SendRtpInfo.getInstance(app, stream, ssrc, dstIpForAudio, dstPortForAudio, !isUdp, sendInfo.getSendLocalPortForVideo(), ptForVideo); + } else { + sendRtpItemForVideo = null; + } + + Boolean streamReady = mediaServerService.isStreamReady(mediaServer, app, stream); + if (streamReady) { + if (sendRtpItemForVideo != null) { + mediaServerService.startSendRtp(mediaServer, sendRtpItemForVideo); + log.info("[第三方服务对接->发送流] 视频流发流成功,callId->{},param->{}", callId, sendRtpItemForVideo); + redisTemplate.opsForValue().set(key, sendInfo); + } + if(sendRtpItemForAudio != null) { + mediaServerService.startSendRtp(mediaServer, sendRtpItemForAudio); + log.info("[第三方服务对接->发送流] 音频流发流成功,callId->{},param->{}", callId, sendRtpItemForAudio); + redisTemplate.opsForValue().set(key, sendInfo); + } + }else { + log.info("[第三方服务对接->发送流] 流不存在,等待流上线,callId->{}", callId); + String uuid = UUID.randomUUID().toString(); + Hook hook = Hook.getInstance(HookType.on_media_arrival, app, stream, mediaServer.getId()); + dynamicTask.startDelay(uuid, ()->{ + log.info("[第三方服务对接->发送流] 等待流上线超时 callId->{}", callId); + redisTemplate.delete(key); + hookSubscribe.removeSubscribe(hook); + }, 10000); + + // 订阅 zlm启动事件, 新的zlm也会从这里进入系统 + hookSubscribe.removeSubscribe(hook); + OtherRtpSendInfo finalSendInfo = sendInfo; + hookSubscribe.addSubscribe(hook, + (hookData)->{ + dynamicTask.stop(uuid); + log.info("[第三方服务对接->发送流] 流上线,开始发流 callId->{}", callId); + try { + Thread.sleep(400); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (sendRtpItemForVideo != null) { + mediaServerService.startSendRtp(mediaServer, sendRtpItemForVideo); + log.info("[第三方服务对接->发送流] 视频流发流成功,callId->{},param->{}", callId, sendRtpItemForVideo); + redisTemplate.opsForValue().set(key, finalSendInfo); + } + if(sendRtpItemForAudio != null) { + mediaServerService.startSendRtp(mediaServer, sendRtpItemForAudio); + log.info("[第三方服务对接->发送流] 音频流发流成功,callId->{},param->{}", callId, sendRtpItemForAudio); + redisTemplate.opsForValue().set(key, finalSendInfo); + } + hookSubscribe.removeSubscribe(hook); + }); + } + } + + @GetMapping(value = "/send/stop") + @ResponseBody + @Operation(summary = "关闭发送流", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "callId", description = "整个过程的唯一标识,不传则使用随机端口发流", required = true) + public void closeSendRTP(String callId) { + log.info("[第三方服务对接->关闭发送流] callId->{}", callId); + String key = VideoManagerConstants.WVP_OTHER_SEND_RTP_INFO + userSetting.getServerId() + "_" + callId; + OtherRtpSendInfo sendInfo = (OtherRtpSendInfo)redisTemplate.opsForValue().get(key); + if (sendInfo == null){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未开启发流"); + } + MediaServer mediaServerItem = mediaServerService.getDefaultMediaServer(); + mediaServerService.stopSendRtp(mediaServerItem, sendInfo.getPushApp(), sendInfo.getPushStream(), sendInfo.getPushSSRC()); + log.info("[第三方服务对接->关闭发送流] 成功 callId->{}", callId); + redisTemplate.delete(key); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/server/ServerController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/server/ServerController.java new file mode 100755 index 0000000..2361573 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/server/ServerController.java @@ -0,0 +1,384 @@ +package com.genersoft.iot.vmp.vmanager.server; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.SystemAllInfo; +import com.genersoft.iot.vmp.common.VersionPo; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.VersionInfo; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.jt1078.config.JT1078Config; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerChangeEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.IMapService; +import com.genersoft.iot.vmp.service.bean.MediaServerLoad; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyService; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.vmanager.bean.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.hardware.NetworkIF; +import oshi.software.os.OperatingSystem; + +import java.io.File; +import java.text.DecimalFormat; +import java.util.*; + +@SuppressWarnings("rawtypes") +@Tag(name = "服务控制") +@Slf4j +@RestController +@RequestMapping("/api/server") +public class ServerController { + + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private VersionInfo versionInfo; + + @Autowired + private SipConfig sipConfig; + + @Autowired + private UserSetting userSetting; + + @Autowired + private JT1078Config jt1078Config; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService channelService; + + @Autowired + private IStreamPushService pushService; + + @Autowired + private IStreamProxyService proxyService; + + + @Autowired(required = false) + private IMapService mapService; + + @Value("${server.port}") + private int serverPort; + + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + + @GetMapping(value = "/media_server/list") + @ResponseBody + @Operation(summary = "流媒体服务列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List getMediaServerList() { + return mediaServerService.getAll(); + } + + @GetMapping(value = "/media_server/online/list") + @ResponseBody + @Operation(summary = "在线流媒体服务列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List getOnlineMediaServerList() { + return mediaServerService.getAllOnline(); + } + + @GetMapping(value = "/media_server/one/{id}") + @ResponseBody + @Operation(summary = "停止视频回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "流媒体服务ID", required = true) + public MediaServer getMediaServer(@PathVariable String id) { + return mediaServerService.getOne(id); + } + + @Operation(summary = "测试流媒体服务", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "ip", description = "流媒体服务IP", required = true) + @Parameter(name = "port", description = "流媒体服务HTT端口", required = true) + @Parameter(name = "secret", description = "流媒体服务secret", required = true) + @GetMapping(value = "/media_server/check") + @ResponseBody + public MediaServer checkMediaServer(@RequestParam String ip, @RequestParam int port, @RequestParam String secret, @RequestParam String type) { + return mediaServerService.checkMediaServer(ip, port, secret, type); + } + + @Operation(summary = "测试流媒体录像管理服务", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "ip", description = "流媒体服务IP", required = true) + @Parameter(name = "port", description = "流媒体服务HTT端口", required = true) + @GetMapping(value = "/media_server/record/check") + @ResponseBody + public void checkMediaRecordServer(@RequestParam String ip, @RequestParam int port) { + boolean checkResult = mediaServerService.checkMediaRecordServer(ip, port); + if (!checkResult) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "连接失败"); + } + } + + @Operation(summary = "保存流媒体服务", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "mediaServerItem", description = "流媒体信息", required = true) + @PostMapping(value = "/media_server/save") + @ResponseBody + public void saveMediaServer(@RequestBody MediaServer mediaServer) { + MediaServer mediaServerItemInDatabase = mediaServerService.getOneFromDatabase(mediaServer.getId()); + + if (mediaServerItemInDatabase != null) { + mediaServerService.update(mediaServer); + } else { + mediaServerService.add(mediaServer); + // 发送事件 + MediaServerChangeEvent event = new MediaServerChangeEvent(this); + event.setMediaServerItemList(mediaServer); + applicationEventPublisher.publishEvent(event); + } + } + + @Operation(summary = "移除流媒体服务", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "流媒体ID", required = true) + @DeleteMapping(value = "/media_server/delete") + @ResponseBody + public void deleteMediaServer(@RequestParam String id) { + MediaServer mediaServer = mediaServerService.getOne(id); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "流媒体不存在"); + } + mediaServerService.delete(mediaServer); + } + + @Operation(summary = "获取流信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @Parameter(name = "mediaServerId", description = "流媒体ID", required = true) + @GetMapping(value = "/media_server/media_info") + @ResponseBody + public MediaInfo getMediaInfo(String app, String stream, String mediaServerId) { + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "流媒体不存在"); + } + return mediaServerService.getMediaInfo(mediaServer, app, stream); + } + + + @Operation(summary = "关闭服务", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping(value = "/shutdown") + @ResponseBody + public void shutdown() { + log.info("正在关闭服务。。。"); + System.exit(1); + } + + @Operation(summary = "获取系统配置信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping(value = "/system/configInfo") + @ResponseBody + public SystemConfigInfo getConfigInfo() { + SystemConfigInfo systemConfigInfo = new SystemConfigInfo(); + systemConfigInfo.setVersion(versionInfo.getVersion()); + systemConfigInfo.setSip(sipConfig); + systemConfigInfo.setAddOn(userSetting); + systemConfigInfo.setServerPort(serverPort); + systemConfigInfo.setJt1078Config(jt1078Config); + return systemConfigInfo; + } + + @Operation(summary = "获取版本信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping(value = "/version") + @ResponseBody + public VersionPo VersionPogetVersion() { + return versionInfo.getVersion(); + } + + @GetMapping(value = "/config") + @Operation(summary = "获取配置信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "type", description = "配置类型(sip, base)", required = true) + @ResponseBody + public JSONObject getVersion(String type) { + + JSONObject jsonObject = new JSONObject(); + jsonObject.put("server.port", serverPort); + if (ObjectUtils.isEmpty(type)) { + jsonObject.put("sip", JSON.toJSON(sipConfig)); + jsonObject.put("base", JSON.toJSON(userSetting)); + } else { + switch (type) { + case "sip": + jsonObject.put("sip", sipConfig); + break; + case "base": + jsonObject.put("base", userSetting); + break; + default: + break; + } + } + return jsonObject; + } + + @GetMapping(value = "/system/info") + @ResponseBody + @Operation(summary = "获取系统信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public SystemAllInfo getSystemInfo() { + SystemAllInfo systemAllInfo = redisCatchStorage.getSystemInfo(); + + return systemAllInfo; + } + + @GetMapping(value = "/media_server/load") + @ResponseBody + @Operation(summary = "获取负载信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List getMediaLoad() { + List result = new ArrayList<>(); + List allOnline = mediaServerService.getAllOnline(); + if (allOnline.isEmpty()) { + return result; + } else { + for (MediaServer mediaServerItem : allOnline) { + result.add(mediaServerService.getLoad(mediaServerItem)); + } + } + return result; + } + + @GetMapping(value = "/resource/info") + @ResponseBody + @Operation(summary = "获取负载信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public ResourceInfo getResourceInfo() { + ResourceInfo result = new ResourceInfo(); + ResourceBaseInfo deviceInfo = deviceService.getOverview(); + result.setDevice(deviceInfo); + ResourceBaseInfo channelInfo = channelService.getOverview(); + result.setChannel(channelInfo); + ResourceBaseInfo pushInfo = pushService.getOverview(); + result.setPush(pushInfo); + ResourceBaseInfo proxyInfo = proxyService.getOverview(); + result.setProxy(proxyInfo); + + return result; + } + + @GetMapping(value = "/info") + @ResponseBody + @Operation(summary = "获取系统信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public Map> getInfo(HttpServletRequest request) { + Map> result = new LinkedHashMap<>(); + Map hardwareMap = new LinkedHashMap<>(); + result.put("硬件信息", hardwareMap); + + SystemInfo systemInfo = new SystemInfo(); + HardwareAbstractionLayer hardware = systemInfo.getHardware(); + // 获取CPU信息 + CentralProcessor.ProcessorIdentifier processorIdentifier = hardware.getProcessor().getProcessorIdentifier(); + hardwareMap.put("CPU", processorIdentifier.getName()); + // 获取内存 + GlobalMemory memory = hardware.getMemory(); + hardwareMap.put("内存", formatByte(memory.getTotal() - memory.getAvailable()) + "/" + formatByte(memory.getTotal())); + hardwareMap.put("制造商", systemInfo.getHardware().getComputerSystem().getManufacturer()); + hardwareMap.put("产品名称", systemInfo.getHardware().getComputerSystem().getModel()); + // 网卡 + List networkIFs = hardware.getNetworkIFs(); + StringBuilder ips = new StringBuilder(); + for (int i = 0; i < networkIFs.size(); i++) { + NetworkIF networkIF = networkIFs.get(i); + String ipsStr = StringUtils.join(networkIF.getIPv4addr()); + if (ObjectUtils.isEmpty(ipsStr)) { + continue; + } + ips.append(ipsStr); + if (i < networkIFs.size() - 1) { + ips.append(","); + } + } + hardwareMap.put("网卡", ips.toString()); + + Map operatingSystemMap = new LinkedHashMap<>(); + result.put("操作系统", operatingSystemMap); + OperatingSystem operatingSystem = systemInfo.getOperatingSystem(); + operatingSystemMap.put("名称", operatingSystem.getFamily() + " " + operatingSystem.getVersionInfo().getVersion()); + operatingSystemMap.put("类型", operatingSystem.getManufacturer()); + + Map platformMap = new LinkedHashMap<>(); + result.put("平台信息", platformMap); + VersionPo version = versionInfo.getVersion(); + platformMap.put("版本", version.getVersion()); + platformMap.put("构建日期", version.getBUILD_DATE()); + platformMap.put("GIT分支", version.getGIT_BRANCH()); + platformMap.put("GIT地址", version.getGIT_URL()); + platformMap.put("GIT日期", version.getGIT_DATE()); + platformMap.put("GIT版本", version.getGIT_Revision_SHORT()); + platformMap.put("DOCKER环境", new File("/.dockerenv").exists()?"是":"否"); + + Map docmap = new LinkedHashMap<>(); + result.put("文档地址", docmap); + docmap.put("部署文档", "https://doc.wvp-pro.cn"); + docmap.put("接口文档", String.format("%s://%s:%s/doc.html", request.getScheme(), request.getServerName(), request.getServerPort())); + + + return result; + } + + /** + * 单位转换 + */ + private static String formatByte(long byteNumber) { + //换算单位 + double FORMAT = 1024.0; + double kbNumber = byteNumber / FORMAT; + if (kbNumber < FORMAT) { + return new DecimalFormat("#.##KB").format(kbNumber); + } + double mbNumber = kbNumber / FORMAT; + if (mbNumber < FORMAT) { + return new DecimalFormat("#.##MB").format(mbNumber); + } + double gbNumber = mbNumber / FORMAT; + if (gbNumber < FORMAT) { + return new DecimalFormat("#.##GB").format(gbNumber); + } + double tbNumber = gbNumber / FORMAT; + return new DecimalFormat("#.##TB").format(tbNumber); + } + + @GetMapping(value = "/map/config") + @ResponseBody + @Operation(summary = "获取地图配置", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List getMapConfig() { + if (mapService == null) { + return Collections.emptyList(); + } + return mapService.getConfig(); + } + + @GetMapping(value = "/map/model-icon/list") + @ResponseBody + @Operation(summary = "获取地图配置图标", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List getMapModelIconList() { + if (mapService == null) { + return Collections.emptyList(); + } + return mapService.getModelList(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/user/RoleController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/user/RoleController.java new file mode 100755 index 0000000..9969087 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/user/RoleController.java @@ -0,0 +1,76 @@ +package com.genersoft.iot.vmp.vmanager.user; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.conf.security.SecurityUtils; +import com.genersoft.iot.vmp.service.IRoleService; +import com.genersoft.iot.vmp.storager.dao.dto.Role; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "角色管理") + +@RestController +@RequestMapping("/api/role") +public class RoleController { + + @Autowired + private IRoleService roleService; + + @PostMapping("/add") + @Operation(summary = "添加角色", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "name", description = "角色名", required = true) + @Parameter(name = "authority", description = "权限(自行定义内容,目前未使用)", required = true) + public void add(@RequestParam String name, + @RequestParam(required = false) String authority){ + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为0才可以删除和添加用户 + throw new ControllerException(ErrorCode.ERROR403); + } + + Role role = new Role(); + role.setName(name); + role.setAuthority(authority); + role.setCreateTime(DateUtil.getNow()); + role.setUpdateTime(DateUtil.getNow()); + + int addResult = roleService.add(role); + if (addResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @DeleteMapping("/delete") + @Operation(summary = "删除角色", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "用户Id", required = true) + public void delete(@RequestParam Integer id){ + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为0才可以删除和添加用户 + throw new ControllerException(ErrorCode.ERROR403); + } + int deleteResult = roleService.delete(id); + + if (deleteResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @GetMapping("/all") + @Operation(summary = "查询角色", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List all(){ + // 获取当前登录用户id + return roleService.getAll(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserApiKeyController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserApiKeyController.java new file mode 100644 index 0000000..1f4566b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserApiKeyController.java @@ -0,0 +1,251 @@ +package com.genersoft.iot.vmp.vmanager.user; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.conf.security.SecurityUtils; +import com.genersoft.iot.vmp.service.IUserApiKeyService; +import com.genersoft.iot.vmp.service.IUserService; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import com.genersoft.iot.vmp.storager.dao.dto.UserApiKey; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@Tag(name = "用户ApiKey管理") +@RestController +@RequestMapping("/api/userApiKey") +public class UserApiKeyController { + + public static final int EXPIRATION_TIME = Integer.MAX_VALUE; + @Autowired + private IUserService userService; + + @Autowired + private IUserApiKeyService userApiKeyService; + + /** + * 添加用户ApiKey + * + * @param userId + * @param app + * @param remark + * @param expiresAt + * @param enable + */ + @PostMapping("/add") + @Operation(summary = "添加用户ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "userId", description = "用户Id", required = true) + @Parameter(name = "app", description = "应用名称", required = false) + @Parameter(name = "remark", description = "备注信息", required = false) + @Parameter(name = "expiredAt", description = "过期时间(不传代表永不过期)", required = false) + @Transactional + public synchronized void add( + @RequestParam(required = true) int userId, + @RequestParam(required = false) String app, + @RequestParam(required = false) String remark, + @RequestParam(required = false) String expiresAt, + @RequestParam(required = false) Boolean enable + ) { + User user = userService.getUserById(userId); + if (user == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "用户不存在"); + } + + Long expirationTime = null; + if (expiresAt != null) { + expirationTime = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(expiresAt); + long difference = (expirationTime - System.currentTimeMillis()) / (60 * 1000); + if (difference < 0) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "过期时间不能早于当前时间"); + } + } + + UserApiKey userApiKey = new UserApiKey(); + userApiKey.setUserId(userId); + userApiKey.setApp(app); + userApiKey.setApiKey(null); + userApiKey.setRemark(remark); + userApiKey.setExpiredAt(expirationTime != null ? expirationTime : 0); + userApiKey.setEnable(enable != null ? enable : false); + userApiKey.setCreateTime(DateUtil.getNow()); + userApiKey.setUpdateTime(DateUtil.getNow()); + + int addResult = userApiKeyService.addApiKey(userApiKey); + + if (addResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + + String apiKey; + do { + Map extra = new HashMap<>(1); + extra.put("apiKeyId", userApiKey.getId()); + apiKey = JwtUtils.createToken(user.getUsername(), expirationTime, extra); + } while (userApiKeyService.isApiKeyExists(apiKey)); + + int resetResult = userApiKeyService.reset(userApiKey.getId(), apiKey); + + if (resetResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + /** + * 分页查询ApiKey + * + * @param page 当前页 + * @param count 每页查询数量 + * @return 分页ApiKey列表 + */ + @GetMapping("/userApiKeys") + @Operation(summary = "分页查询用户", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Transactional + public PageInfo userApiKeys(@RequestParam(required = true) int page, @RequestParam(required = true) int count) { + return userApiKeyService.getUserApiKeys(page, count); + } + + @PostMapping("/enable") + @Operation(summary = "启用用户ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "用户ApiKeyId", required = true) + @Transactional + public void enable(@RequestParam(required = true) Integer id) { + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为1才可以管理UserApiKey + throw new ControllerException(ErrorCode.ERROR403); + } + UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(id); + if (userApiKey == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey不存在"); + } + + int enableResult = userApiKeyService.enable(id); + + if (enableResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @PostMapping("/disable") + @Operation(summary = "停用用户ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "用户ApiKeyId", required = true) + @Transactional + public void disable(@RequestParam(required = true) Integer id) { + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为1才可以管理UserApiKey + throw new ControllerException(ErrorCode.ERROR403); + } + UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(id); + if (userApiKey == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey不存在"); + } + + int disableResult = userApiKeyService.disable(id); + + if (disableResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @PostMapping("/reset") + @Operation(summary = "重置用户ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "用户ApiKeyId", required = true) + @Transactional + public void reset(@RequestParam(required = true) Integer id) { + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为1才可以管理UserApiKey + throw new ControllerException(ErrorCode.ERROR403); + } + UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(id); + if (userApiKey == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey不存在"); + } + User user = userService.getUserById(userApiKey.getUserId()); + if (user == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "用户不存在"); + } + Long expirationTime = null; + if (userApiKey.getExpiredAt() > 0) { + long timestamp = userApiKey.getExpiredAt(); + expirationTime = (timestamp - System.currentTimeMillis()) / (60 * 1000); + if (expirationTime < 0) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey已失效"); + } + } + String apiKey; + do { + Map extra = new HashMap<>(1); + extra.put("apiKeyId", userApiKey.getId()); + apiKey = JwtUtils.createToken(user.getUsername(), expirationTime, extra); + } while (userApiKeyService.isApiKeyExists(apiKey)); + + int resetResult = userApiKeyService.reset(id, apiKey); + + if (resetResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @PostMapping("/remark") + @Operation(summary = "备注用户ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "用户ApiKeyId", required = true) + @Parameter(name = "remark", description = "用户ApiKey备注", required = false) + @Transactional + public void remark(@RequestParam(required = true) Integer id, @RequestParam(required = false) String remark) { + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为1才可以管理UserApiKey + throw new ControllerException(ErrorCode.ERROR403); + } + UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(id); + if (userApiKey == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey不存在"); + } + int remarkResult = userApiKeyService.remark(id, remark); + + if (remarkResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @DeleteMapping("/delete") + @Operation(summary = "删除用户ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "用户ApiKeyId", required = true) + @Transactional + public void delete(@RequestParam(required = true) Integer id) { + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为1才可以管理UserApiKey + throw new ControllerException(ErrorCode.ERROR403); + } + UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(id); + if (userApiKey == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey不存在"); + } + + int deleteResult = userApiKeyService.delete(id); + + if (deleteResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java new file mode 100755 index 0000000..0525722 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java @@ -0,0 +1,228 @@ +package com.genersoft.iot.vmp.vmanager.user; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.conf.security.SecurityUtils; +import com.genersoft.iot.vmp.conf.security.dto.LoginUser; +import com.genersoft.iot.vmp.service.IRoleService; +import com.genersoft.iot.vmp.service.IUserService; +import com.genersoft.iot.vmp.storager.dao.dto.Role; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.util.DigestUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; + +import javax.security.sasl.AuthenticationException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.time.LocalDateTime; +import java.util.List; + +@Tag(name = "用户管理") +@RestController +@RequestMapping("/api/user") +public class UserController { + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private IUserService userService; + + @Autowired + private IRoleService roleService; + + @Autowired + private UserSetting userSetting; + + @GetMapping("/login") + @PostMapping("/login") + @Operation(summary = "登录", description = "登录成功后返回AccessToken, 可以从返回值获取到也可以从响应头中获取到," + + "后续的请求需要添加请求头 'access-token'或者放在参数里") + + @Parameter(name = "username", description = "用户名", required = true) + @Parameter(name = "password", description = "密码(32位md5加密)", required = true) + public LoginUser login(HttpServletRequest request, HttpServletResponse response, @RequestParam String username, @RequestParam String password){ + LoginUser user; + try { + user = SecurityUtils.login(username, password, authenticationManager); + } catch (AuthenticationException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + if (user == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "用户名或密码错误"); + }else { + String jwt = JwtUtils.createToken(username); + response.setHeader(JwtUtils.getHeader(), jwt); + user.setAccessToken(jwt); + user.setServerId(userSetting.getServerId()); + } + return user; + } + + + @PostMapping("/changePassword") + @Operation(summary = "修改密码", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "username", description = "用户名", required = true) + @Parameter(name = "oldpassword", description = "旧密码(已md5加密的密码)", required = true) + @Parameter(name = "password", description = "新密码(未md5加密的密码)", required = true) + public void changePassword(@RequestParam String oldPassword, @RequestParam String password){ + // 获取当前登录用户id + LoginUser userInfo = SecurityUtils.getUserInfo(); + if (userInfo== null) { + throw new ControllerException(ErrorCode.ERROR100); + } + String username = userInfo.getUsername(); + LoginUser user = null; + try { + user = SecurityUtils.login(username, oldPassword, authenticationManager); + if (user == null) { + throw new ControllerException(ErrorCode.ERROR100); + } + //int userId = SecurityUtils.getUserId(); + boolean result = userService.changePassword(user.getId(), DigestUtils.md5DigestAsHex(password.getBytes())); + if (!result) { + throw new ControllerException(ErrorCode.ERROR100); + } + } catch (AuthenticationException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + } + + + @PostMapping("/add") + @Operation(summary = "添加用户", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "username", description = "用户名", required = true) + @Parameter(name = "password", description = "密码(未md5加密的密码)", required = true) + @Parameter(name = "roleId", description = "角色ID", required = true) + public void add(@RequestParam String username, + @RequestParam String password, + @RequestParam Integer roleId){ + if (ObjectUtils.isEmpty(username) || ObjectUtils.isEmpty(password) || roleId == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "参数不可为空"); + } + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为1才可以删除和添加用户 + throw new ControllerException(ErrorCode.ERROR400.getCode(), "用户无权限"); + } + User user = new User(); + user.setUsername(username); + user.setPassword(DigestUtils.md5DigestAsHex(password.getBytes())); + //新增用户的pushKey的生成规则为md5(时间戳+用户名) + user.setPushKey(DigestUtils.md5DigestAsHex((System.currentTimeMillis()+password).getBytes())); + Role role = roleService.getRoleById(roleId); + + if (role == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "角色不存在"); + } + user.setRole(role); + user.setCreateTime(DateUtil.getNow()); + user.setUpdateTime(DateUtil.getNow()); + int addResult = userService.addUser(user); + if (addResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @DeleteMapping("/delete") + @Operation(summary = "删除用户", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "用户Id", required = true) + public void delete(@RequestParam Integer id){ + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为0才可以删除和添加用户 + throw new ControllerException(ErrorCode.ERROR400.getCode(), "用户无权限"); + } + int deleteResult = userService.deleteUser(id); + if (deleteResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @GetMapping("/all") + @Operation(summary = "查询用户", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List all(){ + // 获取当前登录用户id + return userService.getAllUsers(); + } + + /** + * 分页查询用户 + * + * @param page 当前页 + * @param count 每页查询数量 + * @return 分页用户列表 + */ + @GetMapping("/users") + @Operation(summary = "分页查询用户", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + public PageInfo users(int page, int count) { + return userService.getUsers(page, count); + } + + @RequestMapping("/changePushKey") + @Operation(summary = "修改pushkey", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "userId", description = "用户Id", required = true) + @Parameter(name = "pushKey", description = "新的pushKey", required = true) + public void changePushKey(@RequestParam Integer userId,@RequestParam String pushKey) { + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + WVPResult result = new WVPResult<>(); + if (currenRoleId != 1) { + // 只用角色id为0才可以删除和添加用户 + throw new ControllerException(ErrorCode.ERROR400.getCode(), "用户无权限"); + } + int resetPushKeyResult = userService.changePushKey(userId,pushKey); + if (resetPushKeyResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @PostMapping("/changePasswordForAdmin") + @Operation(summary = "管理员修改普通用户密码", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "adminId", description = "管理员id", required = true) + @Parameter(name = "userId", description = "用户id", required = true) + @Parameter(name = "password", description = "新密码(未md5加密的密码)", required = true) + public void changePasswordForAdmin(@RequestParam int userId, @RequestParam String password) { + // 获取当前登录用户id + LoginUser userInfo = SecurityUtils.getUserInfo(); + if (userInfo == null) { + throw new ControllerException(ErrorCode.ERROR100); + } + Role role = userInfo.getRole(); + if (role != null && role.getId() == 1) { + boolean result = userService.changePassword(userId, DigestUtils.md5DigestAsHex(password.getBytes())); + if (!result) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + } + + @PostMapping("/userInfo") + @Operation(summary = "管理员修改普通用户密码") + public LoginUser getUserInfo() { + // 获取当前登录用户id + LoginUser userInfo = SecurityUtils.getUserInfo(); + + if (userInfo == null) { + throw new ControllerException(ErrorCode.ERROR100); + } + User user = userService.getUser(userInfo.getUsername(), userInfo.getPassword()); + return new LoginUser(user, LocalDateTime.now()); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/CameraChannelController.java b/src/main/java/com/genersoft/iot/vmp/web/custom/CameraChannelController.java new file mode 100644 index 0000000..e6ffffd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/CameraChannelController.java @@ -0,0 +1,604 @@ +package com.genersoft.iot.vmp.web.custom; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.service.ICloudRecordService; +import com.genersoft.iot.vmp.service.bean.CloudRecordItem; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushPlayService; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.HttpUtils; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.genersoft.iot.vmp.vmanager.cloudRecord.bean.CloudRecordUrl; +import com.genersoft.iot.vmp.web.custom.bean.*; +import com.genersoft.iot.vmp.web.custom.service.CameraChannelService; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +@Tag(name = "第三方接口") +@Slf4j +@RestController +@RequestMapping(value = "/api/sy") +@ConditionalOnProperty(value = "sy.enable", havingValue = "true") +@Hidden +public class CameraChannelController { + + @Autowired + private CameraChannelService channelService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private ICloudRecordService cloudRecordService; + + @Autowired + private IStreamPushPlayService streamPushPlayService; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private IStreamPushService streamPushService; + + @Value("${sy.ptz-control-time-interval}") + private int ptzControlTimeInterval = 300; + + @GetMapping(value = "/camera/list") + @ResponseBody + @Operation(summary = "查询摄像机列表, 只查询当前虚拟组织下的", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页") + @Parameter(name = "count", description = "每页查询数量") + @Parameter(name = "groupAlias", description = "分组别名") + @Parameter(name = "geoCoordSys", description = "坐标系类型:WGS84,GCJ02、BD09") + @Parameter(name = "status", description = "摄像头状态") + public PageInfo queryList(@RequestParam(required = false, value = "page", defaultValue = "1" )Integer page, + @RequestParam(required = false, value = "count", defaultValue = "100")Integer count, + String groupAlias, + @RequestParam(required = false) String geoCoordSys, + @RequestParam(required = false) Boolean status){ + + + return channelService.queryList(page, count, groupAlias, status, geoCoordSys); + } + + @GetMapping(value = "/camera/list-with-child") + @ResponseBody + @Operation(summary = "查询摄像机列表, 查询当前虚拟组织下以及全部子节点", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页") + @Parameter(name = "count", description = "每页查询数量") + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "sortName", description = "排序字段名") + @Parameter(name = "order", description = "排序方式(true: 升序 或 false: 降序 )") + @Parameter(name = "groupAlias", description = "分组别名") + @Parameter(name = "geoCoordSys", description = "坐标系类型:WGS84,GCJ02、BD09") + @Parameter(name = "status", description = "摄像头状态") + public PageInfo queryListWithChild(@RequestParam(required = false, value = "page", defaultValue = "1" )Integer page, + + @RequestParam(required = false, value = "count", defaultValue = "100")Integer count, + @RequestParam(required = false) String query, + @RequestParam(required = false) String sortName, + @RequestParam(required = false) Boolean order, + @RequestParam(required = false) String groupAlias, + @RequestParam(required = false) String geoCoordSys, + @RequestParam(required = false) Boolean status){ + if (ObjectUtils.isEmpty(query)) { + query = null; + } + if (ObjectUtils.isEmpty(sortName)) { + sortName = null; + } + if (ObjectUtils.isEmpty(order)) { + order = null; + } + if (ObjectUtils.isEmpty(groupAlias)) { + groupAlias = null; + } + + return channelService.queryListWithChild(page, count, query, sortName, order, groupAlias, status, geoCoordSys); + } + + @GetMapping(value = "/camera/cont-with-child") + @ResponseBody + @Operation(summary = "查询摄像机列表的总数和在线数", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "groupAlias", description = "分组别名") + public List queryCountWithChild(String groupAlias){ + return channelService.queryCountWithChild(groupAlias); + } + + @GetMapping(value = "/camera/one") + @ResponseBody + @Operation(summary = "查询单个摄像头信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "通道编号") + @Parameter(name = "deviceCode", description = "摄像头设备国标编号, 对于非国标摄像头可以不设置此参数") + @Parameter(name = "geoCoordSys", description = "坐标系类型:WGS84,GCJ02、BD09") + public CameraChannel getOne(String deviceId, @RequestParam(required = false) String deviceCode, + @RequestParam(required = false) String geoCoordSys) { + return channelService.queryOne(deviceId, deviceCode, geoCoordSys); + } + + @GetMapping(value = "/camera/update") + @ResponseBody + @Operation(summary = "更新摄像头信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "通道编号") + @Parameter(name = "deviceCode", description = "摄像头设备国标编号, 对于非国标摄像头可以不设置此参数") + @Parameter(name = "name", description = "通道名称") + @Parameter(name = "longitude", description = "经度") + @Parameter(name = "latitude", description = "纬度") + @Parameter(name = "geoCoordSys", description = "坐标系类型:WGS84,GCJ02、BD09") + public void updateCamera(String deviceId, + @RequestParam(required = false) String deviceCode, + @RequestParam(required = false) String name, + @RequestParam(required = false) Double longitude, + @RequestParam(required = false) Double latitude, + @RequestParam(required = false) String geoCoordSys) { + channelService.updateCamera(deviceId, deviceCode, name, longitude, latitude, geoCoordSys); + } + + @PostMapping(value = "/camera/list/ids") + @ResponseBody + @Operation(summary = "根据编号查询多个摄像头信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List queryListByDeviceIds(@RequestBody IdsQueryParam param) { + return channelService.queryListByDeviceIds(param.getDeviceIds(), param.getGeoCoordSys()); + } + + @GetMapping(value = "/camera/list/box") + @ResponseBody + @Operation(summary = "根据矩形查询摄像头", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "minLongitude", description = "最小经度") + @Parameter(name = "maxLongitude", description = "最大经度") + @Parameter(name = "minLatitude", description = "最小纬度") + @Parameter(name = "maxLatitude", description = "最大纬度") + @Parameter(name = "level", description = "地图级别") + @Parameter(name = "groupAlias", description = "分组别名") + @Parameter(name = "geoCoordSys", description = "坐标系类型:WGS84,GCJ02、BD09") + public List queryListInBox(Double minLongitude, Double maxLongitude, + Double minLatitude, Double maxLatitude, + @RequestParam(required = false) Integer level, + String groupAlias, + @RequestParam(required = false) String geoCoordSys) { + return channelService.queryListInBox(minLongitude, maxLongitude, minLatitude, maxLatitude, level, groupAlias, geoCoordSys); + } + + @PostMapping(value = "/camera/list/polygon") + @ResponseBody + @Operation(summary = "根据多边形查询摄像头", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List queryListInPolygon(@RequestBody PolygonQueryParam param) { + return channelService.queryListInPolygon(param.getPosition(), param.getGroupAlias(), param.getLevel(), param.getGeoCoordSys()); + } + + @GetMapping(value = "/camera/list/circle") + @ResponseBody + @Operation(summary = "根据圆范围查询摄像头", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "centerLongitude", description = "圆心经度") + @Parameter(name = "centerLatitude", description = "圆心纬度") + @Parameter(name = "radius", description = "查询范围的半径,单位米") + @Parameter(name = "level", description = "地图级别") + @Parameter(name = "groupAlias", description = "分组别名") + @Parameter(name = "geoCoordSys", description = "坐标系类型:WGS84,GCJ02、BD09") + public List queryListInCircle(Double centerLongitude, Double centerLatitude, Double radius, String groupAlias, + @RequestParam(required = false) String geoCoordSys, @RequestParam(required = false) Integer level) { + return channelService.queryListInCircle(centerLongitude, centerLatitude, radius, level, groupAlias, geoCoordSys); + } + + @GetMapping(value = "/camera/list/address") + @ResponseBody + @Operation(summary = "根据安装地址和监视方位获取摄像头", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "address", description = "安装地址") + @Parameter(name = "directionType", description = "监视方位", required = false) + @Parameter(name = "geoCoordSys", description = "坐标系类型:WGS84,GCJ02、BD09") + public List queryListByAddressAndDirectionType(String address, @RequestParam(required = false) Integer directionType, @RequestParam(required = false) String geoCoordSys) { + return channelService.queryListByAddressAndDirectionType(address, directionType, geoCoordSys); + } + + @GetMapping(value = "/camera/control/play") + @ResponseBody + @Operation(summary = "播放摄像头", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "通道编号") + @Parameter(name = "deviceCode", description = "摄像头设备国标编号, 对于非国标摄像头可以不设置此参数") + public DeferredResult> play(HttpServletRequest request, String deviceId, @RequestParam(required = false) String deviceCode) { + + log.info("[SY-播放摄像头] API调用,deviceId:{} ,deviceCode:{} ",deviceId, deviceCode); + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + + ErrorCallback callback = (code, msg, cameraStreamInfo) -> { + if (code == InviteErrorCode.SUCCESS.getCode()) { + StreamInfo streamInfo = cameraStreamInfo.getStreamInfo(); + CommonGBChannel channel = cameraStreamInfo.getChannel(); + WVPResult wvpResult = WVPResult.success(); + if (cameraStreamInfo.getStreamInfo() != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + if (!ObjectUtils.isEmpty(streamInfo.getMediaServer().getTranscodeSuffix()) + && !"null".equalsIgnoreCase(streamInfo.getMediaServer().getTranscodeSuffix())) { + streamInfo.setStream(streamInfo.getStream() + "_" + streamInfo.getMediaServer().getTranscodeSuffix()); + } + CameraStreamContent cameraStreamContent = new CameraStreamContent(streamInfo); + cameraStreamContent.setName(channel.getGbName()); + if (channel.getGbPtzType() != null) { + cameraStreamContent.setControlType( + (channel.getGbPtzType() == 1 || channel.getGbPtzType() == 4 || channel.getGbPtzType() == 5) ? 1 : 0); + }else { + cameraStreamContent.setControlType(0); + } + + wvpResult.setData(cameraStreamContent); + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + result.setResult(wvpResult); + }else { + result.setResult(WVPResult.fail(code, msg)); + } + }; + channelService.play(deviceId, deviceCode, callback); + return result; + } + + @GetMapping(value = "/camera/control/stop") + @ResponseBody + @Operation(summary = "停止播放摄像头", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "通道编号") + @Parameter(name = "deviceCode", description = "摄像头设备国标编号, 对于非国标摄像头可以不设置此参数") + public void stopPlay(String deviceId, @RequestParam(required = false) String deviceCode) { + log.info("[SY-停止播放摄像头] API调用,deviceId:{} ,deviceCode:{} ",deviceId, deviceCode); + channelService.stopPlay(deviceId, deviceCode); + } + + @Operation(summary = "云台控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "通道编号") + @Parameter(name = "deviceCode", description = "摄像头设备国标编号, 对于非国标摄像头可以不设置此参数") + @Parameter(name = "command", description = "控制指令,允许值: left, right, up, down, upleft, upright, downleft, downright, zoomin, zoomout, stop", required = true) + @Parameter(name = "speed", description = "速度(0-100)", required = true) + @GetMapping("/camera/control/ptz") + public DeferredResult> ptz(String deviceId, @RequestParam(required = false) String deviceCode, String command, Integer speed){ + + log.info("[SY-云台控制] API调用,deviceId:{} ,deviceCode:{} ,command:{} ,speed:{} ",deviceId, deviceCode, command, speed); + + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + channelService.ptz(deviceId, deviceCode, command, speed, (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }); + // 设置时间间隔后自动发送停止 + if (!command.equalsIgnoreCase("stop")) { + dynamicTask.startDelay(UUID.randomUUID().toString(), () -> { + channelService.ptz(deviceId, deviceCode, "stop", speed, (code, msg, data) -> {}); + }, ptzControlTimeInterval); + } + return result; + } + + @GetMapping(value = "/camera/list-for-mobile") + @ResponseBody + @Operation(summary = "查询移动设备摄像机列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页") + @Parameter(name = "count", description = "每页查询数量") + @Parameter(name = "topGroupAlias", description = "分组别名") + public PageInfo queryListForMobile(@RequestParam(required = false, value = "page", defaultValue = "1" )Integer page, + @RequestParam(required = false, value = "count", defaultValue = "100")Integer count, + @RequestParam(required = false) String topGroupAlias){ + + return channelService.queryListForMobile(page, count, topGroupAlias); + } + + + @Operation(summary = "获取推流播放地址", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流id", required = true) + @Parameter(name = "callId", description = "推流时携带的自定义鉴权ID", required = true) + @GetMapping(value = "/push/play") + @ResponseBody + public DeferredResult> getStreamInfoByAppAndStream(HttpServletRequest request, + String app, + String stream, + String callId){ + StreamPush streamPush = streamPushService.getPush(app, stream); + Assert.notNull(streamPush, "地址不存在"); + + // 权限校验 + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(app, stream); + if (streamAuthorityInfo == null + || streamAuthorityInfo.getCallId() == null + || !streamAuthorityInfo.getCallId().equals(callId)) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "播放地址鉴权失败"); + } + + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + result.onTimeout(()->{ + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100.getCode(), "等待推流超时"); + result.setResult(fail); + }); + + streamPushPlayService.start(streamPush.getId(), (code, msg, streamInfo) -> { + if (code == 0 && streamInfo != null) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + WVPResult success = WVPResult.success(new StreamContent(streamInfo)); + result.setResult(success); + } + }, null, null); + return result; + } + + @ResponseBody + @GetMapping("/record/collect/add") + @Operation(summary = "添加收藏") + @Parameter(name = "app", description = "应用名", required = false) + @Parameter(name = "stream", description = "流ID", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID", required = false) + @Parameter(name = "startTime", description = "鉴权ID", required = false) + @Parameter(name = "endTime", description = "鉴权ID", required = false) + @Parameter(name = "callId", description = "鉴权ID", required = false) + @Parameter(name = "recordId", description = "录像记录的ID,用于精准收藏一个视频文件", required = false) + public int addCollect(@RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) String callId, @RequestParam(required = false) Integer recordId) { + log.info("[云端录像] 添加收藏,app={},stream={},mediaServerId={},startTime={},endTime={},callId={},recordId={}", app, stream, mediaServerId, startTime, endTime, callId, recordId); + if (recordId != null) { + return cloudRecordService.changeCollectById(recordId, true); + } else { + return cloudRecordService.changeCollect(true, app, stream, mediaServerId, startTime, endTime, callId); + } + } + + @ResponseBody + @GetMapping("/record/collect/delete") + @Operation(summary = "移除收藏") + @Parameter(name = "app", description = "应用名", required = false) + @Parameter(name = "stream", description = "流ID", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID", required = false) + @Parameter(name = "startTime", description = "鉴权ID", required = false) + @Parameter(name = "endTime", description = "鉴权ID", required = false) + @Parameter(name = "callId", description = "鉴权ID", required = false) + @Parameter(name = "recordId", description = "录像记录的ID,用于精准精准移除一个视频文件的收藏", required = false) + public int deleteCollect(@RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) String callId, @RequestParam(required = false) Integer recordId) { + log.info("[云端录像] 移除收藏,app={},stream={},mediaServerId={},startTime={},endTime={},callId={},recordId={}", app, stream, mediaServerId, startTime, endTime, callId, recordId); + if (recordId != null) { + return cloudRecordService.changeCollectById(recordId, false); + } else { + return cloudRecordService.changeCollect(false, app, stream, mediaServerId, startTime, endTime, callId); + } + } + + /************************* 以下这些接口只适合wvp和zlm部署在同一台服务器的情况,且wvp只有一个zlm节点的情况 ***************************************/ + + /** + * 下载指定录像文件的压缩包 + * @param app 应用名 + * @param stream 流ID + * @param callId 每次录像的唯一标识,置空则查询全部流媒体 + */ + @ResponseBody + @GetMapping("/record/zip") + public void downloadZipFile(HttpServletResponse response, + @RequestParam(required = false) String app, + @RequestParam(required = false) String stream, + @RequestParam(required = false) String callId + + ) { + log.info("[下载指定录像文件的压缩包] 查询 app->{}, stream->{}, callId->{}", app, stream, callId); + + if (app != null && ObjectUtils.isEmpty(app.trim())) { + app = null; + } + if (stream != null && ObjectUtils.isEmpty(stream.trim())) { + stream = null; + } + if (callId != null && ObjectUtils.isEmpty(callId.trim())) { + callId = null; + } + // 设置响应头 + response.setContentType("application/zip"); + response.setCharacterEncoding("UTF-8"); + if (stream != null && callId != null) { + response.addHeader("Content-Disposition", "attachment;filename=" + stream + "_" + callId + ".zip"); + } + List cloudRecordItemList = cloudRecordService.getUrlList(app, stream, callId); + if (ObjectUtils.isEmpty(cloudRecordItemList)) { + log.warn("[下载指定录像文件的压缩包] 未找到录像文件,app->{}, stream->{}, callId->{}", app, stream, callId); + return; + } + + try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) { + for (CloudRecordUrl recordUrl : cloudRecordItemList) { + try { + zos.putNextEntry(new ZipEntry(recordUrl.getFileName())); + boolean downloadSuccess = HttpUtils.downLoadFile(recordUrl.getDownloadUrl(), zos); + if (!downloadSuccess) { + log.warn("[下载指定录像文件的压缩包] 下载文件失败: {}", recordUrl.getDownloadUrl()); + zos.closeEntry(); + continue; + } + zos.closeEntry(); + } catch (Exception e) { + log.error("[下载指定录像文件的压缩包] 处理文件失败: {}, 错误: {}", recordUrl.getFileName(), e.getMessage()); + // 继续处理下一个文件 + } + } + } catch (IOException e) { + log.error("[下载指定录像文件的压缩包] 创建压缩包失败,查询 app->{}, stream->{}, callId->{}", app, stream, callId, e); + } + } + + /** + * + * @param query 检索内容 + * @param app 应用名 + * @param stream 流ID + * @param startTime 开始时间(yyyy-MM-dd HH:mm:ss) + * @param endTime 结束时间(yyyy-MM-dd HH:mm:ss) + * @param mediaServerId 流媒体ID,置空则查询全部流媒体 + * @param callId 每次录像的唯一标识,置空则查询全部流媒体 + * @param remoteHost 拼接播放地址时使用的远程地址 + */ + @ResponseBody + @GetMapping("/record/list-url") + @Operation(summary = "分页查询云端录像", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "query", description = "检索内容", required = false) + @Parameter(name = "app", description = "应用名", required = false) + @Parameter(name = "stream", description = "流ID", required = false) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "startTime", description = "开始时间(yyyy-MM-dd HH:mm:ss)", required = false) + @Parameter(name = "endTime", description = "结束时间(yyyy-MM-dd HH:mm:ss)", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID,置空则查询全部流媒体", required = false) + @Parameter(name = "callId", description = "每次录像的唯一标识,置空则查询全部流媒体", required = false) + public PageInfo getListWithUrl(HttpServletRequest request, @RequestParam(required = false) String query, @RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam int page, @RequestParam int count, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) String callId, @RequestParam(required = false) String remoteHost + + ) { + log.info("[云端录像] 查询URL app->{}, stream->{}, mediaServerId->{}, page->{}, count->{}, startTime->{}, endTime->{}, callId->{}", app, stream, mediaServerId, page, count, startTime, endTime, callId); + + List mediaServers; + if (!ObjectUtils.isEmpty(mediaServerId)) { + mediaServers = new ArrayList<>(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体: " + mediaServerId); + } + mediaServers.add(mediaServer); + } else { + mediaServers = null; + } + if (query != null && ObjectUtils.isEmpty(query.trim())) { + query = null; + } + if (app != null && ObjectUtils.isEmpty(app.trim())) { + app = null; + } + if (stream != null && ObjectUtils.isEmpty(stream.trim())) { + stream = null; + } + if (startTime != null && ObjectUtils.isEmpty(startTime.trim())) { + startTime = null; + } + if (endTime != null && ObjectUtils.isEmpty(endTime.trim())) { + endTime = null; + } + if (callId != null && ObjectUtils.isEmpty(callId.trim())) { + callId = null; + } + MediaServer mediaServer = mediaServerService.getDefaultMediaServer(); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体节点"); + } + if (remoteHost == null) { + remoteHost = request.getScheme() + "://" + request.getLocalAddr() + ":" + (request.getScheme().equals("https") ? mediaServer.getHttpSSlPort() : mediaServer.getHttpPort()); + } + PageInfo cloudRecordItemPageInfo = cloudRecordService.getList(page, count, query, app, stream, startTime, endTime, mediaServers, callId, null); + PageInfo cloudRecordUrlPageInfo = new PageInfo<>(); + if (!ObjectUtils.isEmpty(cloudRecordItemPageInfo)) { + cloudRecordUrlPageInfo.setPageNum(cloudRecordItemPageInfo.getPageNum()); + cloudRecordUrlPageInfo.setPageSize(cloudRecordItemPageInfo.getPageSize()); + cloudRecordUrlPageInfo.setSize(cloudRecordItemPageInfo.getSize()); + cloudRecordUrlPageInfo.setEndRow(cloudRecordItemPageInfo.getEndRow()); + cloudRecordUrlPageInfo.setStartRow(cloudRecordItemPageInfo.getStartRow()); + cloudRecordUrlPageInfo.setPages(cloudRecordItemPageInfo.getPages()); + cloudRecordUrlPageInfo.setPrePage(cloudRecordItemPageInfo.getPrePage()); + cloudRecordUrlPageInfo.setNextPage(cloudRecordItemPageInfo.getNextPage()); + cloudRecordUrlPageInfo.setIsFirstPage(cloudRecordItemPageInfo.isIsFirstPage()); + cloudRecordUrlPageInfo.setIsLastPage(cloudRecordItemPageInfo.isIsLastPage()); + cloudRecordUrlPageInfo.setHasPreviousPage(cloudRecordItemPageInfo.isHasPreviousPage()); + cloudRecordUrlPageInfo.setHasNextPage(cloudRecordItemPageInfo.isHasNextPage()); + cloudRecordUrlPageInfo.setNavigatePages(cloudRecordItemPageInfo.getNavigatePages()); + cloudRecordUrlPageInfo.setNavigateFirstPage(cloudRecordItemPageInfo.getNavigateFirstPage()); + cloudRecordUrlPageInfo.setNavigateLastPage(cloudRecordItemPageInfo.getNavigateLastPage()); + cloudRecordUrlPageInfo.setNavigatepageNums(cloudRecordItemPageInfo.getNavigatepageNums()); + cloudRecordUrlPageInfo.setTotal(cloudRecordItemPageInfo.getTotal()); + List cloudRecordUrlList = new ArrayList<>(cloudRecordItemPageInfo.getList().size()); + List cloudRecordItemList = cloudRecordItemPageInfo.getList(); + for (CloudRecordItem cloudRecordItem : cloudRecordItemList) { + CloudRecordUrl cloudRecordUrl = new CloudRecordUrl(); + cloudRecordUrl.setId(cloudRecordItem.getId()); + cloudRecordUrl.setDownloadUrl(remoteHost + "/index/api/downloadFile?file_path=" + cloudRecordItem.getFilePath() + "&save_name=" + cloudRecordItem.getStream() + "_" + cloudRecordItem.getCallId() + "_" + DateUtil.timestampMsToUrlToyyyy_MM_dd_HH_mm_ss((long)cloudRecordItem.getStartTime())); + cloudRecordUrl.setPlayUrl(remoteHost + "/index/api/downloadFile?file_path=" + cloudRecordItem.getFilePath()); + cloudRecordUrlList.add(cloudRecordUrl); + } + cloudRecordUrlPageInfo.setList(cloudRecordUrlList); + } + return cloudRecordUrlPageInfo; + } + + @GetMapping(value = "/forceClose") + @ResponseBody + @Operation(summary = "强制停止推流", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public void stop(String app, String stream){ + streamPushPlayService.stop(app, stream); + } + + @GetMapping(value = "/camera/meeting/list") + @ResponseBody + @Operation(summary = "查询会议设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "topGroupAlias", description = "分组别名") + public List queryMeetingChannelList(String topGroupAlias){ + return channelService.queryMeetingChannelList(topGroupAlias); + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraChannel.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraChannel.java new file mode 100644 index 0000000..647b094 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraChannel.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + + +@Getter +@Setter +@Schema(description = "摄像头信息") +public class CameraChannel extends CommonGBChannel { + + @Schema(description = "摄像头设备国标编号") + private String deviceCode; + + + @Schema(description = "图标路径") + private String icon; + + /** + * 分组别名 + */ + @Schema(description = "所属组织结构别名") + private String groupAlias; + + /** + * 分组所属业务分组别名 + */ + @Schema(description = "所属业务分组别名") + private String topGroupGAlias; +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraCount.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraCount.java new file mode 100644 index 0000000..10187f7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraCount.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import lombok.Data; + +@Data +public class CameraCount { + + private String groupAlias; + private String deviceId; + private Long allCount; + private Long onlineCount; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraGroup.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraGroup.java new file mode 100644 index 0000000..182fa6d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraGroup.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import com.genersoft.iot.vmp.gb28181.bean.Group; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +public class CameraGroup extends Group { + + @Getter + private CameraGroup parent; + + @Getter + private final List child = new ArrayList<>(); + + public void setParent(CameraGroup parent) { + if (parent == null) { + return; + } + this.parent = parent; + parent.addChild(this); + } + + public void addChild(CameraGroup child) { + if (child == null) { + return; + } + this.child.add(child); + if (this.parent != null) { + this.parent.addChild(child); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraStreamContent.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraStreamContent.java new file mode 100644 index 0000000..5327ef7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraStreamContent.java @@ -0,0 +1,22 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CameraStreamContent extends StreamContent { + + public CameraStreamContent(StreamInfo streamInfo) { + super(streamInfo); + } + + private String name; + + // 0不可动,1可动 + private Integer controlType; + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraStreamInfo.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraStreamInfo.java new file mode 100644 index 0000000..ca71a29 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraStreamInfo.java @@ -0,0 +1,22 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CameraStreamInfo { + + + private CommonGBChannel channel; + + + private StreamInfo streamInfo; + + public CameraStreamInfo(CommonGBChannel channel, StreamInfo streamInfo) { + this.channel = channel; + this.streamInfo = streamInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/ChannelParam.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/ChannelParam.java new file mode 100644 index 0000000..6c676ef --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/ChannelParam.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "通道信息") +public class ChannelParam { + + @Schema(description = "摄像头设备国标编号, 对于非国标摄像头可以不设置此参数") + private String deviceCode; + + @Schema(description = "通道编号") + private String deviceId; +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/IdsQueryParam.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/IdsQueryParam.java new file mode 100644 index 0000000..dfa6303 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/IdsQueryParam.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "根据多个ID获取摄像头列表") +public class IdsQueryParam { + + @Schema(description = "通道编号列表") + private List deviceIds; + + @Schema(description = "坐标系类型:WGS84,GCJ02、BD09") + private String geoCoordSys; +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/Point.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/Point.java new file mode 100644 index 0000000..0f0deb7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/Point.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "坐标") +public class Point { + + private double lng; + private double lat; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/PolygonQueryParam.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/PolygonQueryParam.java new file mode 100644 index 0000000..23613da --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/PolygonQueryParam.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "多边形检索摄像头参数") +public class PolygonQueryParam { + + @Schema(description = "多边形位置,格式: [{'lng':116.32, 'lat': 39: 39.2}, {'lng':115.32, 'lat': 39: 38.2}, {'lng':125.32, 'lat': 39: 38.2}]") + private List position; + + @Schema(description = "地图级别") + private Integer level; + + @Schema(description = "分组别名") + private String groupAlias; + + @Schema(description = "坐标系类型:WGS84,GCJ02、BD09") + private String geoCoordSys; +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/conf/CachedBodyHttpServletRequest.java b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/CachedBodyHttpServletRequest.java new file mode 100644 index 0000000..3a5a999 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/CachedBodyHttpServletRequest.java @@ -0,0 +1,125 @@ +package com.genersoft.iot.vmp.web.custom.conf; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import lombok.extern.slf4j.Slf4j; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +/** + * 自定义请求包装器,用于缓存请求体内容 + * 解决流只能读取一次的问题 + */ +@Slf4j +public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { + + private byte[] cachedBody; + private String cachedBodyString; + + public CachedBodyHttpServletRequest(HttpServletRequest request) { + super(request); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + if (cachedBody == null) { + cacheInputStream(); + } + return new CachedBodyServletInputStream(cachedBody); + } + + @Override + public BufferedReader getReader() throws IOException { + if (cachedBodyString == null) { + if (cachedBody == null) { + cacheInputStream(); + } + cachedBodyString = new String(cachedBody, StandardCharsets.UTF_8); + } + return new BufferedReader(new StringReader(cachedBodyString)); + } + + /** + * 获取缓存的请求体内容 + */ + public String getCachedBody() { + if (cachedBodyString == null) { + if (cachedBody == null) { + try { + cacheInputStream(); + } catch (IOException e) { + log.warn("缓存请求体失败: {}", e.getMessage()); + return ""; + } + } + cachedBodyString = new String(cachedBody, StandardCharsets.UTF_8); + } + return cachedBodyString; + } + + /** + * 获取缓存的请求体字节数组 + */ + public byte[] getCachedBodyBytes() { + if (cachedBody == null) { + try { + cacheInputStream(); + } catch (IOException e) { + log.warn("缓存请求体失败: {}", e.getMessage()); + return new byte[0]; + } + } + return cachedBody; + } + + private void cacheInputStream() throws IOException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + InputStream inputStream = super.getInputStream()) { + + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + cachedBody = baos.toByteArray(); + log.debug("成功缓存请求体,长度: {}", cachedBody.length); + } + } + + /** + * 自定义 ServletInputStream 实现 + */ + private static class CachedBodyServletInputStream extends ServletInputStream { + private final ByteArrayInputStream inputStream; + + public CachedBodyServletInputStream(byte[] body) { + this.inputStream = new ByteArrayInputStream(body); + } + + @Override + public boolean isFinished() { + return inputStream.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + // 不需要实现 + } + + @Override + public int read() throws IOException { + return inputStream.read(); + } + } +} + + + diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SignAuthenticationFilter.java b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SignAuthenticationFilter.java new file mode 100644 index 0000000..b5c4bc0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SignAuthenticationFilter.java @@ -0,0 +1,174 @@ +package com.genersoft.iot.vmp.web.custom.conf; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.crypto.SmUtil; +import cn.hutool.crypto.symmetric.SM4; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.sip.message.Response; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +/** + * sign token 过滤器 + */ + +@Slf4j +@Component +@ConditionalOnProperty(value = "sy.enable", havingValue = "true") +public class SignAuthenticationFilter extends OncePerRequestFilter { + + + @Override + protected void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + // 忽略登录请求的token验证 + String requestURI = servletRequest.getRequestURI(); + // 包装原始请求,缓存请求体 + CachedBodyHttpServletRequest request = new CachedBodyHttpServletRequest(servletRequest); + if (!requestURI.startsWith("/api/sy")) { + chain.doFilter(request, response); + return; + } +// if (request.getParameter("ccerty") != null) { +// chain.doFilter(request, response); +// return; +// } + // 设置响应内容类型 + response.setContentType("application/json;charset=UTF-8"); + + try { + String sign = request.getParameter("sign"); + String appKey = request.getParameter("appKey"); + String accessToken = request.getParameter("accessToken"); + String timestampStr = request.getParameter("timestamp"); + + if (sign == null || appKey == null || accessToken == null || timestampStr == null) { + log.info("[SY-接口验签] 缺少关键参数:sign/appKey/accessToken/timestamp, 请求地址: {} ", requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(1, "参数非法")); + out.close(); + return; + } + if (SyTokenManager.INSTANCE.appMap.get(appKey) == null) { + log.info("[SY-接口验签] appKey {} 对应的 secret 不存在, 请求地址: {} ", appKey, requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(1, "参数非法")); + out.close(); + return; + } + + Map parameterMap = request.getParameterMap(); + // 参数排序 + Set paramKeys = new TreeSet<>(parameterMap.keySet()); + + // 拼接签名信息 + // 参数拼接 + StringBuilder beforeSign = new StringBuilder(); + for (String paramKey : paramKeys) { + if (paramKey.equals("sign")) { + continue; + } + beforeSign.append(paramKey).append(parameterMap.get(paramKey)[0]); + } + // 如果是post请求的json消息,拼接body字符串 + if (request.getContentLength() > 0 + && request.getMethod().equalsIgnoreCase("POST") + && request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) { + // 读取body内容 - 使用自定义缓存机制 + String requestBody = request.getCachedBody(); + if (!ObjectUtils.isEmpty(requestBody)) { + beforeSign.append(requestBody); + log.debug("[SY-接口验签] 读取到请求体内容,长度: {}", requestBody.length()); + } else { + log.warn("[SY-接口验签] 请求体内容为空"); + } + } + beforeSign.append(SyTokenManager.INSTANCE.appMap.get(appKey)); + // 生成签名 + String buildSign = SmUtil.sm3(beforeSign.toString()); + if (!buildSign.equals(sign)) { + log.info("[SY-接口验签] 失败,加密前内容: {}, 请求地址: {} ", beforeSign, requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(2, "签名错误")); + out.close(); + return; + } + // 验证请求时间戳 + long timestamp = Long.parseLong(timestampStr); + long currentTimeMillis = System.currentTimeMillis(); + if (currentTimeMillis > SyTokenManager.INSTANCE.expires * 60 * 1000 + timestamp ) { + log.info("[SY-接口验签] 时间戳已经过期, 请求时间戳:{}, 当前时间: {}, 过期时间: {}, 请求地址: {} ", timestamp, currentTimeMillis, timestamp + SyTokenManager.INSTANCE.expires * 60 * 1000, requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(3, "接口己过期")); + out.close(); + return; + } + // accessToken校验 + if (accessToken.equals(SyTokenManager.INSTANCE.adminToken)) { + log.info("[SY-接口验签] adminToken已经默认放行, 请求地址: {} ", requestURI); + chain.doFilter(request, response); + return; + }else { + // 对token进行解密 + SM4 sm4 = SmUtil.sm4(HexUtil.decodeHex(SyTokenManager.INSTANCE.sm4Key)); + String decryptStr = sm4.decryptStr(accessToken, CharsetUtil.CHARSET_UTF_8); + if (decryptStr == null) { + log.info("[SY-接口验签] accessToken解密失败, 请求地址: {} ", requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(2, "签名错误")); + out.close(); + return; + } + JSONObject jsonObject = JSON.parseObject(decryptStr); + Long expirationTime = jsonObject.getLong("expirationTime"); + if (expirationTime < System.currentTimeMillis()) { + log.info("[SY-接口验签] accessToken 已经过期, 请求地址: {} ", requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(4, "token已过期或错误")); + out.close(); + return; + } + } + }catch (Exception e) { + log.info("[SY-接口验签] 读取body失败, 请求地址: {} ", requestURI, e); + response.setStatus(Response.OK); + if (!response.isCommitted()) { + PrintWriter out = response.getWriter(); + out.println(getErrorResult(2, "签名错误")); + out.close(); + } + return; + } + chain.doFilter(request, response); + } + + private String getErrorResult(Integer code, String message) { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(message); + return JSON.toJSONString(wvpResult); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SyTokenManager.java b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SyTokenManager.java new file mode 100644 index 0000000..0d98244 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SyTokenManager.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.web.custom.conf; + +import java.util.HashMap; +import java.util.Map; + +public enum SyTokenManager { + INSTANCE; + + /** + * 普通用户 app Key 和 secret + */ + public final Map appMap = new HashMap<>(); + + + /** + * 管理员专属token + */ + public String adminToken; + + /** + * sm4密钥 + */ + public String sm4Key; + + /** + * 接口有效时长,单位分钟 + */ + public Long expires; + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/service/CameraChannelService.java b/src/main/java/com/genersoft/iot/vmp/web/custom/service/CameraChannelService.java new file mode 100644 index 0000000..9bf7018 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/service/CameraChannelService.java @@ -0,0 +1,654 @@ +package com.genersoft.iot.vmp.web.custom.service; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.FrontEndControlCodeForPTZ; +import com.genersoft.iot.vmp.gb28181.bean.Group; +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.GroupMapper; +import com.genersoft.iot.vmp.gb28181.event.channel.ChannelEvent; +import com.genersoft.iot.vmp.gb28181.event.subscribe.mobilePosition.MobilePositionEvent; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelControlService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.utils.Coordtransform; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.web.custom.bean.*; +import com.genersoft.iot.vmp.web.custom.conf.SyTokenManager; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.github.xiaoymin.knife4j.core.util.Assert; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.event.EventListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Slf4j +@Service +@ConditionalOnProperty(value = "sy.enable", havingValue = "true") +public class CameraChannelService implements CommandLineRunner { + + private final String REDIS_GPS_MESSAGE = "VM_MSG_MOBILE_GPS"; + private final String REDIS_CHANNEL_MESSAGE = "VM_MSG_MOBILE_CHANNEL"; + + @Autowired + private CommonGBChannelMapper channelMapper; + + @Autowired + private GroupMapper groupMapper; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private RedisTemplate redisTemplateForString; + + @Autowired + private IGbChannelPlayService channelPlayService; + + @Autowired + private IGbChannelControlService channelControlService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private DynamicTask dynamicTask; + + @Override + public void run(String... args) { + // 启动时获取全局token + String taskKey = UUID.randomUUID().toString(); + if (!refreshToken()) { + log.info("[SY-读取Token]失败,30秒后重试"); + dynamicTask.startDelay(taskKey, ()->{ + this.run(args); + }, 30000); + }else { + log.info("[SY-读取Token] 成功"); + } + } + + private boolean refreshToken() { + String adminToken = redisTemplateForString.opsForValue().get("SYSTEM_ACCESS_TOKEN"); + if (adminToken == null) { + log.warn("[SY读取TOKEN] SYSTEM_ACCESS_TOKEN 读取失败"); + return false; + } + SyTokenManager.INSTANCE.adminToken = adminToken; + + String sm4Key = redisTemplateForString.opsForValue().get("SYSTEM_SM4_KEY"); + if (sm4Key == null) { + log.warn("[SY读取TOKEN] SYSTEM_SM4_KEY 读取失败"); + return false; + } + SyTokenManager.INSTANCE.sm4Key = sm4Key; + + JSONObject appJson = (JSONObject)redisTemplate.opsForValue().get("SYSTEM_APPKEY"); + if (appJson == null) { + log.warn("[SY读取TOKEN] SYSTEM_APPKEY 读取失败"); + return false; + } + SyTokenManager.INSTANCE.appMap.put(appJson.getString("appKey"), appJson.getString("appSecret")); + + JSONObject timeJson = (JSONObject)redisTemplate.opsForValue().get("sys_INTERFACE_VALID_TIME"); + if (timeJson == null) { + log.warn("[SY读取TOKEN] sys_INTERFACE_VALID_TIME 读取失败"); + return false; + } + SyTokenManager.INSTANCE.expires = timeJson.getLong("systemValue"); + + return true; + } + + // 监听通道变化,如果是移动设备则发送redis消息 + @EventListener + public void onApplicationEvent(ChannelEvent event) { + List channels = event.getChannels(); + if (channels.isEmpty()) { + return; + } + List resultListForAdd = new ArrayList<>(); + List resultListForDelete = new ArrayList<>(); + List resultListForUpdate = new ArrayList<>(); + List resultListForOnline = new ArrayList<>(); + List resultListForOffline = new ArrayList<>(); + + switch (event.getMessageType()) { + case UPDATE: + List oldChannelList = event.getOldChannels(); + List channelList = event.getChannels(); + // 更新操作 + if (oldChannelList == null || oldChannelList.isEmpty()) { + // 无旧设备则不需要判断, 目前只有分组或行政区划转换为通道信息时没有旧的通道信息,这两个类型也是不需要发送通知的,直接忽略即可 + break; + } + // 需要比对旧数据,看看是否是新增的移动设备或者取消的移动设备 + // 将 channelList 转为以 gbDeviceId 为 key 的 Map + Map oldChannelMap = new HashMap<>(); + for (CommonGBChannel channel : oldChannelList) { + if (channel != null && channel.getGbDeviceId() != null) { + oldChannelMap.put(channel.getGbDeviceId(), channel); + } + } + for (CommonGBChannel channel : channelList) { + if (channel.getGbPtzType() != null && channel.getGbPtzType() == 99) { + CommonGBChannel oldChannel = oldChannelMap.get(channel.getGbDeviceId()); + if (channel.getGbStatus() == null) { + channel.setGbStatus(oldChannel.getGbStatus()); + } + if (oldChannel != null) { + if (oldChannel.getGbPtzType() != null && oldChannel.getGbPtzType() == 99) { + resultListForUpdate.add(channel); + }else { + resultListForAdd.add(channel); + } + }else { + resultListForAdd.add(channel); + } + }else { + CommonGBChannel oldChannel = oldChannelMap.get(channel.getGbDeviceId()); + if (oldChannel != null && oldChannel.getGbPtzType() != null && oldChannel.getGbPtzType() == 99) { + CameraChannel cameraChannel = new CameraChannel(); + cameraChannel.setGbDeviceId(channel.getGbDeviceId()); + resultListForDelete.add(cameraChannel); + } + } + } + + break; + case DEL: + for (CommonGBChannel channel : channels) { + if (channel.getGbPtzType() != null && channel.getGbPtzType() == 99) { + CameraChannel cameraChannel = new CameraChannel(); + cameraChannel.setGbDeviceId(channel.getGbDeviceId()); + resultListForDelete.add(cameraChannel); + } + } + break; + case ON: + case OFF: + case DEFECT: + case VLOST: + for (CommonGBChannel channel : channels) { + if (channel.getGbPtzType() != null && channel.getGbPtzType() == 99) { + if (event.getMessageType() == ChannelEvent.ChannelEventMessageType.ON) { + resultListForOnline.add(channel); + }else { + resultListForOffline.add(channel); + } + + } + } + break; + case ADD: + for (CommonGBChannel channel : channels) { + if (channel.getGbPtzType() != null && channel.getGbPtzType() == 99) { + resultListForAdd.add(channel); + } + } + break; + } + if (!resultListForDelete.isEmpty()) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("type", ChannelEvent.ChannelEventMessageType.DEL); + jsonObject.put("list", resultListForDelete); + log.info("[SY-redis发送通知-DEL] 发送 通道信息变化 {}: {}", REDIS_CHANNEL_MESSAGE, jsonObject.toString()); + redisTemplate.convertAndSend(REDIS_CHANNEL_MESSAGE, jsonObject); + } + if (!resultListForAdd.isEmpty()) { + sendChannelMessage(resultListForAdd, ChannelEvent.ChannelEventMessageType.ADD); + } + if (!resultListForUpdate.isEmpty()) { + sendChannelMessage(resultListForUpdate, ChannelEvent.ChannelEventMessageType.UPDATE); + } + if (!resultListForOnline.isEmpty()) { + sendChannelMessage(resultListForOnline, ChannelEvent.ChannelEventMessageType.ON); + } + if (!resultListForOffline.isEmpty()) { + sendChannelMessage(resultListForOffline, ChannelEvent.ChannelEventMessageType.OFF); + } + } + + private void sendChannelMessage(List channelList, ChannelEvent.ChannelEventMessageType type) { + if (channelList.isEmpty()) { + return; + } + List cameraChannelList = channelMapper.queryCameraChannelByIds(channelList); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("type", type); + jsonObject.put("list", cameraChannelList); + log.info("[SY-redis发送通知-{}] 发送 通道信息变化 {}: {}", type, REDIS_CHANNEL_MESSAGE, jsonObject.toString()); + redisTemplate.convertAndSend(REDIS_CHANNEL_MESSAGE, jsonObject); + } + + // 监听GPS消息,如果是移动设备则发送redis消息 + @EventListener + public void onApplicationEvent(MobilePositionEvent event) { + MobilePosition mobilePosition = event.getMobilePosition(); + Integer channelId = mobilePosition.getChannelId(); + CameraChannel cameraChannel = channelMapper.queryCameraChannelById(channelId); + + // 非移动设备类型 不发送 + if (cameraChannel == null || cameraChannel.getGbPtzType() == null || cameraChannel.getGbPtzType() != 99) { + return; + } + // 发送redis消息 + JSONObject jsonObject = new JSONObject(); + jsonObject.put("time", mobilePosition.getTime()); + jsonObject.put("deviceId", mobilePosition.getChannelDeviceId()); + jsonObject.put("longitude", mobilePosition.getLongitude()); + jsonObject.put("latitude", mobilePosition.getLatitude()); + jsonObject.put("altitude", mobilePosition.getAltitude()); + jsonObject.put("direction", mobilePosition.getDirection()); + jsonObject.put("speed", mobilePosition.getSpeed()); + jsonObject.put("topGroupGAlias", cameraChannel.getTopGroupGAlias()); + jsonObject.put("groupAlias", cameraChannel.getGroupAlias()); + log.info("[SY-redis发送通知] 发送 移动设备位置信息移动位置 {}: {}", REDIS_GPS_MESSAGE, jsonObject.toString()); + redisTemplate.convertAndSend(REDIS_GPS_MESSAGE, jsonObject); + } + + + public PageInfo queryList(Integer page, Integer count, String groupAlias, Boolean status, String geoCoordSys) { + // 构建组织结构信息 + Group group = groupMapper.queryGroupByAlias(groupAlias); + Assert.notNull(group, "组织结构不存在"); + String groupDeviceId = group.getDeviceId(); + + // 构建分页 + PageHelper.startPage(page, count); + + List all = channelMapper.queryListForSy(groupDeviceId, status); + PageInfo groupPageInfo = new PageInfo<>(all); + List list = addIconPathAndPositionForCameraChannelList(groupPageInfo.getList(), geoCoordSys); + groupPageInfo.setList(list); + return groupPageInfo; + } + + public PageInfo queryListWithChild(Integer page, Integer count, String query, String sortName, Boolean order, String groupAlias, Boolean status, String geoCoordSys) { + + List groupList = null; + // 构建组织结构信息 + if (groupAlias != null) { + CameraGroup group = groupMapper.queryGroupByAlias(groupAlias); + Assert.notNull(group, "组织结构不存在"); + String groupDeviceId = group.getDeviceId(); + // 获取所有子节点 + groupList = queryAllGroupChildren(group.getId(), group.getBusinessGroup()); + groupList.add(group); + } + + // 构建分页 + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + if (order == null) { + order = true; + } + List all = channelMapper.queryListWithChildForSy(query, sortName, order, groupList, status); + PageInfo groupPageInfo = new PageInfo<>(all); + List list = addIconPathAndPositionForCameraChannelList(groupPageInfo.getList(), geoCoordSys); + groupPageInfo.setList(list); + return groupPageInfo; + } + + // 获取所有子节点 + private List queryAllGroupChildren(int groupId, String businessGroup) { + Map groupMap = groupMapper.queryByBusinessGroupForMap(businessGroup); + for (CameraGroup cameraGroup : groupMap.values()) { + cameraGroup.setParent(groupMap.get(cameraGroup.getParentId())); + } + CameraGroup cameraGroup = groupMap.get(groupId); + if (cameraGroup == null) { + return Collections.emptyList(); + }else { + return cameraGroup.getChild(); + } + } + + public List queryCountWithChild(String groupAlias) { + // 构建组织结构信息 + CameraGroup group = groupMapper.queryGroupByAlias(groupAlias); + Assert.notNull(group, "组织结构不存在"); + // 获取所有子节点 + List groupList = queryAllGroupChildren(group.getId(), group.getBusinessGroup()); + groupList.add(group); + + // TODO 此处整理可优化,尽量让sql直接返回对应的结构 无需二次整理 + List cameraCounts = groupMapper.queryCountWithChild(groupList); + if (cameraCounts.isEmpty()) { + return Collections.emptyList(); + }else { + Map cameraGroupMap = new HashMap<>(); + for (CameraGroup cameraGroup : groupList) { + cameraGroupMap.put(cameraGroup.getDeviceId(), cameraGroup.getAlias()); + } + List result = new ArrayList<>(); + for (CameraCount cameraCount : cameraCounts) { + String alias = cameraGroupMap.get(cameraCount.getDeviceId()); + if (alias == null) { + continue; + } + cameraCount.setGroupAlias(alias); + result.add(cameraCount); + } + return result; + } + } + + /** + * 为通道增加图片信息和转换坐标系 + */ + private List addIconPathAndPositionForCameraChannelList(List channels, String geoCoordSys) { + // 读取redis 图标信息 + /* + { + "brand": "WVP", + "createdTime": 1715845840000, + "displayInSelect": true, + "id": 12, + "imagesPath": "images/lt132", + "machineName": "图传对讲单兵", + "machineType": "LT132" + }, + */ + JSONArray jsonArray = (JSONArray) redisTemplate.opsForValue().get("machineInfo"); + Map pathMap = new HashMap<>(); + if (jsonArray != null && !jsonArray.isEmpty()) { + for (int i = 0; i < jsonArray.size(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + String machineType = jsonObject.getString("machineType"); + String imagesPath = jsonObject.getString("imagesPath"); + if (machineType != null && imagesPath != null) { + pathMap.put(machineType, imagesPath); + } + } + }else { + log.warn("[读取通道图标信息失败]"); + } + for (CameraChannel channel : channels) { + if (channel.getGbModel() != null && pathMap.get(channel.getGbModel()) != null) { + channel.setIcon(pathMap.get(channel.getGbModel())); + } + // 坐标系转换 + if (geoCoordSys != null && channel.getGbLongitude() != null && channel.getGbLatitude() != null + && channel.getGbLongitude() > 0 && channel.getGbLatitude() > 0) { + if (geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] position = Coordtransform.WGS84ToGCJ02(channel.getGbLongitude(), channel.getGbLatitude()); + channel.setGbLongitude(position[0]); + channel.setGbLatitude(position[1]); + }else if (geoCoordSys.equalsIgnoreCase("BD09")) { + Double[] gcj02Position = Coordtransform.WGS84ToGCJ02(channel.getGbLongitude(), channel.getGbLatitude()); + Double[] position = Coordtransform.GCJ02ToBD09(gcj02Position[0], gcj02Position[1]); + channel.setGbLongitude(position[0]); + channel.setGbLatitude(position[1]); + } + } + } + return channels; + } + + public CameraChannel queryOne(String deviceId, String deviceCode, String geoCoordSys) { + List cameraChannels = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); + Assert.isTrue(cameraChannels.isEmpty(), "通道不存在"); + List channels = addIconPathAndPositionForCameraChannelList(cameraChannels, geoCoordSys); + CameraChannel channel = channels.get(0); + if (deviceCode != null) { + channel.setDeviceCode(deviceCode); + } + + return channel; + } + + /** + * 播放通道 + * @param deviceId 通道编号 + * @param deviceCode 通道对应的国标设备的编号 + * @param callback 点播结果的回放 + */ + public void play(String deviceId, String deviceCode, ErrorCallback callback) { + List cameraChannels = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); + Assert.isTrue(cameraChannels.isEmpty(), "通道不存在"); + CameraChannel channel = cameraChannels.get(0); + channelPlayService.play(channel, null, userSetting.getRecordSip(), (code, msg, data) -> { + callback.run(code, msg, new CameraStreamInfo(channel, data)); + }); + } + + /** + * 停止播放通道 + * @param deviceId 通道编号 + * @param deviceCode 通道对应的国标设备的编号 + */ + public void stopPlay(String deviceId, String deviceCode) { + List cameraChannels = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); + Assert.isTrue(cameraChannels.isEmpty(), "通道不存在"); + CameraChannel channel = cameraChannels.get(0); + channelPlayService.stopPlay(channel); + } + + public void ptz(String deviceId, String deviceCode, String command, Integer speed, ErrorCallback callback) { + List cameraChannels = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); + Assert.isTrue(cameraChannels.isEmpty(), "通道不存在"); + CameraChannel channel = cameraChannels.get(0); + + if (speed == null) { + speed = 50; + }else if (speed < 0 || speed > 100) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "panSpeed 为 0-100的数字"); + } + + FrontEndControlCodeForPTZ controlCode = new FrontEndControlCodeForPTZ(); + controlCode.setPanSpeed(speed); + controlCode.setTiltSpeed(speed); + controlCode.setZoomSpeed(speed); + switch (command){ + case "left": + controlCode.setPan(0); + break; + case "right": + controlCode.setPan(1); + break; + case "up": + controlCode.setTilt(0); + break; + case "down": + controlCode.setTilt(1); + break; + case "upleft": + controlCode.setPan(0); + controlCode.setTilt(0); + break; + case "upright": + controlCode.setTilt(0); + controlCode.setPan(1); + break; + case "downleft": + controlCode.setPan(0); + controlCode.setTilt(1); + break; + case "downright": + controlCode.setTilt(1); + controlCode.setPan(1); + break; + case "zoomin": + controlCode.setZoom(1); + break; + case "zoomout": + controlCode.setZoom(0); + break; + default: + break; + } + + channelControlService.ptz(channel, controlCode, callback); + } + + public void updateCamera(String deviceId, String deviceCode, String name, Double longitude, Double latitude, String geoCoordSys) { + List cameraChannels = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); + Assert.isTrue(cameraChannels.isEmpty(), "通道不存在"); + CameraChannel commonGBChannel = cameraChannels.get(0); + commonGBChannel.setGbName(name); + if (geoCoordSys != null && longitude != null && latitude != null + && longitude > 0 && latitude > 0) { + if (geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] position = Coordtransform.GCJ02ToWGS84(longitude, latitude); + commonGBChannel.setGbLongitude(position[0]); + commonGBChannel.setGbLatitude(position[1]); + }else if (geoCoordSys.equalsIgnoreCase("BD09")) { + Double[] gcj02Position = Coordtransform.BD09ToGCJ02(longitude, latitude); + Double[] position = Coordtransform.GCJ02ToWGS84(gcj02Position[0], gcj02Position[1]); + commonGBChannel.setGbLongitude(position[0]); + commonGBChannel.setGbLatitude(position[1]); + }else { + commonGBChannel.setGbLongitude(longitude); + commonGBChannel.setGbLatitude(latitude); + } + }else { + commonGBChannel.setGbLongitude(longitude); + commonGBChannel.setGbLatitude(latitude); + } + channelMapper.update(commonGBChannel); + } + + public List queryListByDeviceIds(List deviceIds, String geoCoordSys) { + List cameraChannels = channelMapper.queryListByDeviceIds(deviceIds); + return addIconPathAndPositionForCameraChannelList(cameraChannels, geoCoordSys); + } + + public List queryListByAddressAndDirectionType(String address, Integer directionType, String geoCoordSys) { + List cameraChannels = channelMapper.queryListByAddressAndDirectionType(address, directionType); + return addIconPathAndPositionForCameraChannelList(cameraChannels, geoCoordSys); + } + + + public List queryListInBox(Double minLongitude, Double maxLongitude, Double minLatitude, Double maxLatitude, Integer level, String groupAlias, String geoCoordSys) { + // 构建组织结构信息 + CameraGroup group = groupMapper.queryGroupByAlias(groupAlias); + Assert.notNull(group, "组织结构不存在"); + // 获取所有子节点 + List groupList = queryAllGroupChildren(group.getId(), group.getBusinessGroup()); + groupList.add(group); + // 参数坐标系列转换 + if (geoCoordSys != null) { + if (geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] minPosition = Coordtransform.GCJ02ToWGS84(minLongitude, minLatitude); + minLongitude = minPosition[0]; + minLatitude = minPosition[1]; + + Double[] maxPosition = Coordtransform.GCJ02ToWGS84(maxLongitude, maxLatitude); + maxLongitude = maxPosition[0]; + maxLatitude = maxPosition[1]; + }else if (geoCoordSys.equalsIgnoreCase("BD09")) { + Double[] gcj02MinPosition = Coordtransform.BD09ToGCJ02(minLongitude, minLatitude); + Double[] minPosition = Coordtransform.GCJ02ToWGS84(gcj02MinPosition[0], gcj02MinPosition[1]); + minLongitude = minPosition[0]; + minLatitude = minPosition[1]; + + Double[] gcj02MaxPosition = Coordtransform.BD09ToGCJ02(maxLongitude, maxLatitude); + Double[] maxPosition = Coordtransform.GCJ02ToWGS84(gcj02MaxPosition[0], gcj02MaxPosition[1]); + maxLongitude = maxPosition[0]; + maxLatitude = maxPosition[1]; + } + } + + List all = channelMapper.queryListInBox(minLongitude, maxLongitude, minLatitude, maxLatitude, level, groupList); + return addIconPathAndPositionForCameraChannelList(all, geoCoordSys); + } + + public List queryListInCircle(Double centerLongitude, Double centerLatitude, Double radius, Integer level, String groupAlias, String geoCoordSys) { + // 构建组织结构信息 + CameraGroup group = groupMapper.queryGroupByAlias(groupAlias); + Assert.notNull(group, "组织结构不存在"); + // 获取所有子节点 + List groupList = queryAllGroupChildren(group.getId(), group.getBusinessGroup()); + groupList.add(group); + + // 参数坐标系列转换 + if (geoCoordSys != null) { + if (geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] position = Coordtransform.GCJ02ToWGS84(centerLongitude, centerLatitude); + centerLongitude = position[0]; + centerLatitude = position[1]; + + }else if (geoCoordSys.equalsIgnoreCase("BD09")) { + Double[] gcj02Position = Coordtransform.BD09ToGCJ02(centerLongitude, centerLatitude); + Double[] position = Coordtransform.GCJ02ToWGS84(gcj02Position[0], gcj02Position[1]); + centerLongitude = position[0]; + centerLatitude = position[1]; + } + } + + List all = channelMapper.queryListInCircle(centerLongitude, centerLatitude, radius, level, groupList); + return addIconPathAndPositionForCameraChannelList(all, geoCoordSys); + } + + public List queryListInPolygon(List pointList, String groupAlias, Integer level, String geoCoordSys) { + // 构建组织结构信息 + CameraGroup group = groupMapper.queryGroupByAlias(groupAlias); + Assert.notNull(group, "组织结构不存在"); + // 获取所有子节点 + List groupList = queryAllGroupChildren(group.getId(), group.getBusinessGroup()); + groupList.add(group); + + // 参数坐标系列转换 + if (geoCoordSys != null) { + for (Point point : pointList) { + if (geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] position = Coordtransform.GCJ02ToWGS84(point.getLng(), point.getLat()); + point.setLng(position[0]); + point.setLat(position[1]); + }else if (geoCoordSys.equalsIgnoreCase("BD09")) { + Double[] gcj02Position = Coordtransform.BD09ToGCJ02(point.getLng(), point.getLat()); + Double[] position = Coordtransform.GCJ02ToWGS84(gcj02Position[0], gcj02Position[1]); + point.setLng(position[0]); + point.setLat(position[1]); + } + } + } + + List all = channelMapper.queryListInPolygon(pointList, level, groupList); + return addIconPathAndPositionForCameraChannelList(all, geoCoordSys); + } + + public PageInfo queryListForMobile(Integer page, Integer count, String topGroupAlias) { + + CameraGroup cameraGroup = groupMapper.queryGroupByAlias(topGroupAlias); + + String business = null; + if (cameraGroup != null) { + business = cameraGroup.getDeviceId(); + } + // 构建分页 + PageHelper.startPage(page, count); + List all = channelMapper.queryListForSyMobile(business); + + PageInfo groupPageInfo = new PageInfo<>(all); + List list = addIconPathAndPositionForCameraChannelList(groupPageInfo.getList(), null); + groupPageInfo.setList(list); + return groupPageInfo; + } + + + public List queryMeetingChannelList(String topGroupAlias) { + CameraGroup cameraGroup = groupMapper.queryGroupByAlias(topGroupAlias); + Assert.notNull(cameraGroup, "域不存在"); + String business = cameraGroup.getDeviceId(); + Assert.notNull(business, "域不存在"); + + return channelMapper.queryMeetingChannelList(business); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/service/SyServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/web/custom/service/SyServiceImpl.java new file mode 100644 index 0000000..0950768 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/service/SyServiceImpl.java @@ -0,0 +1,109 @@ +package com.genersoft.iot.vmp.web.custom.service; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.service.IMapService; +import com.genersoft.iot.vmp.vmanager.bean.MapConfig; +import com.genersoft.iot.vmp.vmanager.bean.MapModelIcon; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * 第三方平台适配 + */ +@Slf4j +@Service +@ConditionalOnProperty(value = "sy.enable", havingValue = "true") +public class SyServiceImpl implements IMapService { + + @Autowired + private RedisTemplate redisTemplate; + + @Override + public List getConfig() { + List configList = new ArrayList<>(); + JSONObject configObject = (JSONObject)redisTemplate.opsForValue().get("interfaceConfig1"); + if (configObject == null) { + return configList; + } + // 浅色地图 + MapConfig mapConfigForDefault = readConfig("FRAGMENTIMG_SERVER", configObject); + if (mapConfigForDefault != null) { + mapConfigForDefault.setName("浅色地图"); + configList.add(mapConfigForDefault); + } + + // 深色地图 + MapConfig mapConfigForDark = readConfig("POLARNIGHTBLUE_FRAGMENTIMG_SERVER", configObject); + if (mapConfigForDark != null) { + mapConfigForDark.setName("深色地图"); + configList.add(mapConfigForDark); + } + + // 卫星地图 + MapConfig mapConfigForSatellited = readConfig("SATELLITE_FRAGMENTIMG_SERVER", configObject); + if (mapConfigForSatellited != null) { + mapConfigForSatellited.setName("卫星地图"); + configList.add(mapConfigForSatellited); + } + return configList; + } + + private MapConfig readConfig(String key, JSONObject jsonObject) { + JSONArray fragmentimgServerArray = jsonObject.getJSONArray(key); + if (fragmentimgServerArray == null || fragmentimgServerArray.isEmpty()) { + return null; + } + JSONObject fragmentimgServer = fragmentimgServerArray.getJSONObject(0); + // 坐标系 + String geoCoordSys = fragmentimgServer.getString("csysType").toUpperCase(); + // 获取地址 + String path = fragmentimgServer.getString("path"); + String ip = fragmentimgServer.getString("ip"); + JSONObject portJson = fragmentimgServer.getJSONObject("port"); + JSONObject httpPortJson = portJson.getJSONObject("httpPort"); + String protocol = httpPortJson.getString("portType"); + Integer port = httpPortJson.getInteger("port"); + String tileUrl = String.format("%s://%s:%s%s", protocol, ip, port, path); + MapConfig mapConfig = new MapConfig(); + mapConfig.setCoordinateSystem(geoCoordSys); + mapConfig.setTilesUrl(tileUrl); + return mapConfig; + + } + + @Override + public List getModelList() { + // 读取redis 图标信息 + /* + { + "brand": "WVP", + "createdTime": 1715845840000, + "displayInSelect": true, + "id": 12, + "imagesPath": "images/lt132", + "machineName": "图传对讲单兵", + "machineType": "LT132" + }, + */ + List mapModelIconList = new ArrayList<>(); + JSONArray jsonArray = (JSONArray) redisTemplate.opsForValue().get("machineInfo"); + if (jsonArray != null && !jsonArray.isEmpty()) { + for (int i = 0; i < jsonArray.size(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + String machineType = jsonObject.getString("machineType"); + String machineName = jsonObject.getString("machineName"); + String imagesPath = jsonObject.getString("imagesPath"); + + mapModelIconList.add(MapModelIcon.getInstance(machineType, machineName, imagesPath)); + } + } + return mapModelIconList; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiControlController.java b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiControlController.java new file mode 100644 index 0000000..529aac9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiControlController.java @@ -0,0 +1,156 @@ +package com.genersoft.iot.vmp.web.gb28181; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; + +/** + * API兼容:设备控制 + */ +@Slf4j +@RestController +@RequestMapping(value = "/api/v1/control") +@Hidden +public class ApiControlController { + + @Autowired + private SIPCommander cmder; + + @Autowired + private IDeviceService deviceService; + + /** + * 设备控制 - 云台控制 + * @param serial 设备编号 + * @param command 控制指令 允许值: left, right, up, down, upleft, upright, downleft, downright, zoomin, zoomout, stop + * @param channel 通道序号 + * @param code 通道编号 + * @param speed 速度(0~255) 默认值: 129 + */ + @GetMapping(value = "/ptz") + private void ptz(String serial,String command, + @RequestParam(required = false)Integer channel, + @RequestParam(required = false)String code, + @RequestParam(required = false)Integer speed){ + + if (log.isDebugEnabled()) { + log.debug("模拟接口> 设备云台控制 API调用,deviceId:{} ,channelId:{} ,command:{} ,speed:{} ", + serial, code, command, speed); + } + if (channel == null) {channel = 0;} + if (speed == null) {speed = 0;} + Device device = deviceService.getDeviceByDeviceId(serial); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "device[ " + serial + " ]未找到"); + } + int cmdCode = -1; + switch (command){ + case "left": + cmdCode = 2; + break; + case "right": + cmdCode = 1; + break; + case "up": + cmdCode = 8; + break; + case "down": + cmdCode = 4; + break; + case "upleft": + cmdCode = 10; + break; + case "upright": + cmdCode = 9; + break; + case "downleft": + cmdCode = 6; + break; + case "downright": + cmdCode = 5; + break; + case "zoomin": + cmdCode = 16; + break; + case "zoomout": + cmdCode = 32; + break; + case "stop": + cmdCode = 0; + break; + default: + break; + } + if (cmdCode == -1) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未识别的指令:" + command); + } + // 默认值 50 + try { + cmder.frontEndCmd(device, code, cmdCode, speed, speed, speed); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 云台控制: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + /** + * 设备控制 - 预置位控制 + * @param serial 设备编号 + * @param code 通道编号,通过 /api/v1/device/channellist 获取的 ChannelList.ID, 该参数和 channel 二选一传递即可 + * @param channel 通道序号, 默认值: 1 + * @param command 控制指令 允许值: set, goto, remove + * @param preset 预置位编号(1~255) + * @param name 预置位名称, command=set 时有效 + */ + @GetMapping(value = "/preset") + private void list(String serial,String command, + @RequestParam(required = false)Integer channel, + @RequestParam(required = false)String code, + @RequestParam(required = false)String name, + @RequestParam(required = false)Integer preset){ + + if (log.isDebugEnabled()) { + log.debug("模拟接口> 预置位控制 API调用,deviceId:{} ,channelId:{} ,command:{} ,name:{} ,preset:{} ", + serial, code, command, name, preset); + } + + if (channel == null) {channel = 0;} + Device device = deviceService.getDeviceByDeviceId(serial); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "device[ " + serial + " ]未找到"); + } + int cmdCode = 0; + switch (command){ + case "set": + cmdCode = 129; + break; + case "goto": + cmdCode = 130; + break; + case "remove": + cmdCode = 131; + break; + default: + break; + } + try { + cmder.frontEndCmd(device, code, cmdCode, 0, preset, 0); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 预置位控制: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiController.java b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiController.java new file mode 100644 index 0000000..cb3fdbe --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiController.java @@ -0,0 +1,106 @@ +package com.genersoft.iot.vmp.web.gb28181; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.SipConfig; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * API兼容:系统接口 + */ +@Controller +@Slf4j +@RequestMapping(value = "/api/v1") +@Hidden +public class ApiController { + + @Autowired + private SipConfig sipConfig; + + + @GetMapping("/getserverinfo") + private JSONObject getserverinfo(){ + JSONObject result = new JSONObject(); + result.put("Authorization","ceshi"); + result.put("Hardware",""); + result.put("InterfaceVersion","2.5.5"); + result.put("IsDemo",""); + result.put("Hardware","false"); + result.put("APIAuth","false"); + result.put("RemainDays","永久"); + result.put("RunningTime",""); + result.put("ServerTime","2020-09-02 17:11"); + result.put("StartUpTime","2020-09-02 17:11"); + result.put("Server",""); + result.put("SIPSerial", sipConfig.getId()); + result.put("SIPRealm", sipConfig.getDomain()); + result.put("SIPHost", sipConfig.getShowIp()); + result.put("SIPPort", sipConfig.getPort()); + result.put("ChannelCount","1000"); + result.put("VersionType",""); + result.put("LogoMiniText",""); + result.put("LogoText",""); + result.put("CopyrightText",""); + + return result; + } + + @GetMapping(value = "/userinfo") + private JSONObject userinfo(){ +// JSONObject result = new JSONObject(); +// result.put("ID","ceshi"); +// result.put("Hardware",""); +// result.put("InterfaceVersion","2.5.5"); +// result.put("IsDemo",""); +// result.put("Hardware","false"); +// result.put("APIAuth","false"); +// result.put("RemainDays","永久"); +// result.put("RunningTime",""); +// result.put("ServerTime","2020-09-02 17:11"); +// result.put("StartUpTime","2020-09-02 17:11"); +// result.put("Server",""); +// result.put("SIPSerial", sipConfig.getId()); +// result.put("SIPRealm", sipConfig.getDomain()); +// result.put("SIPHost", sipConfig.getIp()); +// result.put("SIPPort", sipConfig.getPort()); +// result.put("ChannelCount","1000"); +// result.put("VersionType",""); +// result.put("LogoMiniText",""); +// result.put("LogoText",""); +// result.put("CopyrightText",""); + + return null; + } + + /** + * 系统接口 - 登录 + * @param username 用户名 + * @param password 密码(经过md5加密,32位长度,不带中划线,不区分大小写) + * @return + */ + @GetMapping(value = "/login") + @ResponseBody + private JSONObject login(String username,String password ){ + if (log.isDebugEnabled()) { + log.debug(String.format("模拟接口> 登录 API调用,username:%s ,password:%s ", + username, password)); + } + + JSONObject result = new JSONObject(); + result.put("CookieToken","ynBDDiKMg"); + result.put("URLToken","MOBkORkqnrnoVGcKIAHXppgfkNWRdV7utZSkDrI448Q.oxNjAxNTM4NDk3LCJwIjoiZGJjODg5NzliNzVj" + + "Nzc2YmU5MzBjM2JjNjg1ZWFiNGI5ZjhhN2Y0N2RlZjg3NWUyOTJkY2VkYjkwYmEwMTA0NyIsInQiOjE2MDA5MzM2OTcsInUiOiI" + + "4ODlkZDYyM2ViIn0eyJlIj.GciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJhb"); + result.put("TokenTimeout",604800); + result.put("AuthToken","MOBkORkqnrnoVGcKIAHXppgfkNWRdV7utZSkDrI448Q.oxNjAxNTM4NDk3LCJwIjoiZGJjODg5NzliNzVj" + + "Nzc2YmU5MzBjM2JjNjg1ZWFiNGI5ZjhhN2Y0N2RlZjg3NWUyOTJkY2VkYjkwYmEwMTA0NyIsInQiOjE2MDA5MzM2OTcsInUiOiI" + + "4ODlkZDYyM2ViIn0eyJlIj.GciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJhb"); + result.put("Token","ynBDDiKMg"); + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiDeviceController.java b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiDeviceController.java new file mode 100644 index 0000000..3b9ba81 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiDeviceController.java @@ -0,0 +1,228 @@ +package com.genersoft.iot.vmp.web.gb28181; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Preset; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.genersoft.iot.vmp.web.gb28181.dto.DeviceChannelExtend; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +import java.util.*; + +/** + * API兼容:设备信息 + */ +@SuppressWarnings("unchecked") +@Slf4j +@RestController +@RequestMapping(value = "/api/v1/device") +@Hidden +public class ApiDeviceController { + + @Autowired + private SIPCommander cmder; + @Autowired + private IDeviceChannelService channelService; + + @Autowired + private DeferredResultHolder resultHolder; + + @Autowired + private IDeviceService deviceService; + + + /** + * 分页获取设备列表 现在直接返回,尚未实现分页 + * @param start + * @param limit + * @param q + * @param online + * @return + */ + @GetMapping(value = "/list") + public JSONObject list( @RequestParam(required = false)Integer start, + @RequestParam(required = false)Integer limit, + @RequestParam(required = false)String q, + @RequestParam(required = false)Boolean online ){ + +// if (logger.isDebugEnabled()) { +// logger.debug("查询所有视频设备API调用"); +// } + + JSONObject result = new JSONObject(); + List devices; + if (start == null || limit ==null) { + devices = deviceService.getAllByStatus(online); + result.put("DeviceCount", devices.size()); + }else { + PageInfo deviceList = deviceService.getAll(start/limit, limit,null, online); + result.put("DeviceCount", deviceList.getTotal()); + devices = deviceList.getList(); + } + + JSONArray deviceJSONList = new JSONArray(); + devices.stream().forEach(device -> { + JSONObject deviceJsonObject = new JSONObject(); + deviceJsonObject.put("ID", device.getDeviceId()); + deviceJsonObject.put("Name", device.getName()); + deviceJsonObject.put("Type", "GB"); + deviceJsonObject.put("ChannelCount", device.getChannelCount()); + deviceJsonObject.put("RecvStreamIP", ""); + deviceJsonObject.put("CatalogInterval", 3600); // 通道目录抓取周期 + deviceJsonObject.put("SubscribeInterval", device.getSubscribeCycleForCatalog()); // 订阅周期(秒), 0 表示后台不周期订阅 + deviceJsonObject.put("Online", device.isOnLine()); + deviceJsonObject.put("Password", ""); + deviceJsonObject.put("MediaTransport", device.getTransport()); + deviceJsonObject.put("RemoteIP", device.getIp()); + deviceJsonObject.put("RemotePort", device.getPort()); + deviceJsonObject.put("LastRegisterAt", ""); + deviceJsonObject.put("LastKeepaliveAt", ""); + deviceJsonObject.put("UpdatedAt", ""); + deviceJsonObject.put("CreatedAt", ""); + deviceJSONList.add(deviceJsonObject); + }); + result.put("DeviceList",deviceJSONList); + return result; + } + + @GetMapping(value = "/channellist") + public JSONObject channellist( String serial, + @RequestParam(required = false)String channel_type, + @RequestParam(required = false)String code , + @RequestParam(required = false)String dir_serial , + @RequestParam(required = false)Integer start, + @RequestParam(required = false)Integer limit, + @RequestParam(required = false)String q, + @RequestParam(required = false)Boolean online ){ + + JSONObject result = new JSONObject(); + List deviceChannels; + List channelIds = null; + if (!ObjectUtils.isEmpty(code)) { + String[] split = code.trim().split(","); + channelIds = Arrays.asList(split); + } + List allDeviceChannelList = channelService.queryChannelExtendsByDeviceId(serial,channelIds,online); + if (start == null || limit ==null) { + deviceChannels = allDeviceChannelList; + result.put("ChannelCount", deviceChannels.size()); + }else { + if (start > allDeviceChannelList.size()) { + deviceChannels = new ArrayList<>(); + }else { + if (start + limit < allDeviceChannelList.size()) { + deviceChannels = allDeviceChannelList.subList(start, start + limit); + }else { + deviceChannels = allDeviceChannelList.subList(start, allDeviceChannelList.size()); + } + } + result.put("ChannelCount", allDeviceChannelList.size()); + } + JSONArray channleJSONList = new JSONArray(); + deviceChannels.stream().forEach(deviceChannelExtend -> { + JSONObject deviceJOSNChannel = new JSONObject(); + deviceJOSNChannel.put("ID", deviceChannelExtend.getChannelId()); + deviceJOSNChannel.put("DeviceID", deviceChannelExtend.getDeviceId()); + deviceJOSNChannel.put("DeviceName", deviceChannelExtend.getDeviceName()); + deviceJOSNChannel.put("DeviceOnline", deviceChannelExtend.isDeviceOnline()); + deviceJOSNChannel.put("Channel", 0); // TODO 自定义序号 + deviceJOSNChannel.put("Name", deviceChannelExtend.getName()); + deviceJOSNChannel.put("Custom", false); + deviceJOSNChannel.put("CustomName", ""); + deviceJOSNChannel.put("SubCount", deviceChannelExtend.getSubCount()); // TODO ? 子节点数, SubCount > 0 表示该通道为子目录 + deviceJOSNChannel.put("SnapURL", ""); + deviceJOSNChannel.put("Manufacturer ", deviceChannelExtend.getManufacture()); + deviceJOSNChannel.put("Model", deviceChannelExtend.getModel()); + deviceJOSNChannel.put("Owner", deviceChannelExtend.getOwner()); + deviceJOSNChannel.put("CivilCode", deviceChannelExtend.getCivilCode()); + deviceJOSNChannel.put("Address", deviceChannelExtend.getAddress()); + deviceJOSNChannel.put("Parental", deviceChannelExtend.getParental()); // 当为通道设备时, 是否有通道子设备, 1-有,0-没有 + deviceJOSNChannel.put("ParentID", deviceChannelExtend.getParentId()); // 直接上级编号 + deviceJOSNChannel.put("Secrecy", deviceChannelExtend.getSecrecy()); + deviceJOSNChannel.put("RegisterWay", 1); // 注册方式, 缺省为1, 允许值: 1, 2, 3 + // 1-IETF RFC3261, + // 2-基于口令的双向认证, + // 3-基于数字证书的双向认证 + deviceJOSNChannel.put("Status", deviceChannelExtend.getStatus()); + deviceJOSNChannel.put("Longitude", deviceChannelExtend.getLongitude()); + deviceJOSNChannel.put("Latitude", deviceChannelExtend.getLatitude()); + deviceJOSNChannel.put("PTZType ", deviceChannelExtend.getPTZType()); // 云台类型, 0 - 未知, 1 - 球机, 2 - 半球, + // 3 - 固定枪机, 4 - 遥控枪机 + deviceJOSNChannel.put("CustomPTZType", ""); + deviceJOSNChannel.put("StreamID", deviceChannelExtend.getStreamId()); // StreamID 直播流ID, 有值表示正在直播 + deviceJOSNChannel.put("NumOutputs ", -1); // 直播在线人数 + channleJSONList.add(deviceJOSNChannel); + }); + result.put("ChannelList", channleJSONList); + return result; + } + + /** + * 设备信息 - 获取下级通道预置位 + * @param serial 设备编号 + * @param code 通道编号,通过 /api/v1/device/channellist 获取的 ChannelList.ID, 该参数和 channel 二选一传递即可 + * @param channel 通道序号, 默认值: 1 + * @param fill 是否填充空置预置位,当下级返回预置位,但不够255个时,自动填充空置预置位到255个, 默认值: true, 允许值: true, false + * @param timeout 超时时间(秒) 默认值: 15 + * @return + */ + @GetMapping(value = "/fetchpreset") + private DeferredResult> list(String serial, + @RequestParam(required = false)Integer channel, + @RequestParam(required = false)String code, + @RequestParam(required = false)Boolean fill, + @RequestParam(required = false)Integer timeout){ + + if (log.isDebugEnabled()) { + log.debug("<模拟接口> 获取下级通道预置位 API调用,deviceId:{} ,channel:{} ,code:{} ,fill:{} ,timeout:{} ", + serial, channel, code, fill, timeout); + } + + Device device = deviceService.getDeviceByDeviceId(serial); + Assert.notNull(device, "设备不存在"); + DeferredResult> deferredResult = new DeferredResult<> (timeout * 1000L); + deviceService.queryPreset(device, code, (resultCode, msg, data) -> { + if (resultCode == ErrorCode.SUCCESS.getCode()) { + List presetQuerySipReqList = (List)data; + HashMap resultMap = new HashMap<>(); + resultMap.put("DeviceID", code); + resultMap.put("Result", "OK"); + resultMap.put("SumNum", presetQuerySipReqList.size()); + ArrayList> presetItemList = new ArrayList<>(presetQuerySipReqList.size()); + for (Preset presetQuerySipReq : presetQuerySipReqList) { + Map item = new HashMap<>(); + item.put("PresetID", presetQuerySipReq.getPresetId()); + item.put("PresetName", presetQuerySipReq.getPresetName()); + item.put("PresetEnable", true); + presetItemList.add(item); + } + resultMap.put("PresetItemList",presetItemList ); + deferredResult.setResult(new WVPResult<>(resultCode, msg, resultMap)); + }else { + deferredResult.setResult(new WVPResult<>(resultCode, msg, null)); + } + }); + + deferredResult.onTimeout(()->{ + log.warn("[获取设备预置位] 超时, {}", device.getDeviceId()); + deferredResult.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "wait for presetquery timeout["+timeout+"s]")); + }); + return deferredResult; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java new file mode 100644 index 0000000..a867315 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java @@ -0,0 +1,278 @@ +package com.genersoft.iot.vmp.web.gb28181; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; + +/** + * API兼容:实时直播 + */ +@SuppressWarnings(value = {"rawtypes", "unchecked"}) + +@Slf4j +@RestController +@RequestMapping(value = "/api/v1/stream") +@Hidden +public class ApiStreamController { + + @Autowired + private SIPCommander cmder; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IPlayService playService; + + @Autowired + private IInviteStreamService inviteStreamService; + + /** + * 实时直播 - 开始直播 + * @param serial 设备编号 + * @param channel 通道序号 默认值: 1 + * @param code 通道编号,通过 /api/v1/device/channellist 获取的 ChannelList.ID, 该参数和 channel 二选一传递即可 + * @param cdn 转推 CDN 地址, 形如: [rtmp|rtsp]://xxx, encodeURIComponent + * @param audio 是否开启音频, 默认 开启 + * @param transport 流传输模式, 默认 UDP + * @param checkchannelstatus 是否检查通道状态, 默认 false, 表示 拉流前不检查通道状态是否在线 + * @param transportmode 当 transport=TCP 时有效, 指示流传输主被动模式, 默认被动 + * @param timeout 拉流超时(秒), + * @return + */ + @GetMapping("/start") + private DeferredResult start(String serial , + @RequestParam(required = false)Integer channel , + @RequestParam(required = false)String code, + @RequestParam(required = false)String cdn, + @RequestParam(required = false)String audio, + @RequestParam(required = false)String transport, + @RequestParam(required = false)String checkchannelstatus , + @RequestParam(required = false)String transportmode, + @RequestParam(required = false)String timeout + + ){ + DeferredResult result = new DeferredResult<>(userSetting.getPlayTimeout().longValue() + 10); + Device device = deviceService.getDeviceByDeviceId(serial); + if (device == null ) { + JSONObject resultJSON = new JSONObject(); + resultJSON.put("error","device[ " + serial + " ]未找到"); + result.setResult(resultJSON); + return result; + }else if (!device.isOnLine()) { + JSONObject resultJSON = new JSONObject(); + resultJSON.put("error","device[ " + code + " ]offline"); + result.setResult(resultJSON); + return result; + } + + + DeviceChannel deviceChannel = deviceChannelService.getOne(serial, code); + if (deviceChannel == null) { + JSONObject resultJSON = new JSONObject(); + resultJSON.put("error","channel[ " + code + " ]未找到"); + result.setResult(resultJSON); + return result; + }else if (!deviceChannel.getStatus().equalsIgnoreCase("ON")) { + JSONObject resultJSON = new JSONObject(); + resultJSON.put("error","channel[ " + code + " ]offline"); + result.setResult(resultJSON); + return result; + } + + result.onTimeout(()->{ + log.info("播放等待超时"); + JSONObject resultJSON = new JSONObject(); + resultJSON.put("error","timeout"); + result.setResult(resultJSON); + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceChannel.getId()); + deviceChannelService.stopPlay(deviceChannel.getId()); + // 清理RTP server + }); + + MediaServer newMediaServerItem = playService.getNewMediaServerItem(device); + + playService.play(newMediaServerItem, serial, code, null, (errorCode, msg, data) -> { + if (errorCode == InviteErrorCode.SUCCESS.getCode()) { + if (data != null) { + StreamInfo streamInfo = (StreamInfo)data; + JSONObject resultJjson = new JSONObject(); + resultJjson.put("StreamID", streamInfo.getStream()); + resultJjson.put("DeviceID", serial); + resultJjson.put("ChannelID", code); + resultJjson.put("ChannelName", deviceChannel.getName()); + resultJjson.put("ChannelCustomName", ""); + if (streamInfo.getTranscodeStream() != null) { + resultJjson.put("FLV", streamInfo.getTranscodeStream().getFlv().getUrl()); + }else { + resultJjson.put("FLV", streamInfo.getFlv().getUrl()); + + } + if(streamInfo.getHttps_flv() != null) { + if (streamInfo.getTranscodeStream() != null) { + resultJjson.put("HTTPS_FLV", streamInfo.getTranscodeStream().getHttps_flv().getUrl()); + }else { + resultJjson.put("HTTPS_FLV", streamInfo.getHttps_flv().getUrl()); + } + } + + if (streamInfo.getTranscodeStream() != null) { + resultJjson.put("WS_FLV", streamInfo.getTranscodeStream().getWs_flv().getUrl()); + }else { + resultJjson.put("WS_FLV", streamInfo.getWs_flv().getUrl()); + } + + if(streamInfo.getWss_flv() != null) { + if (streamInfo.getTranscodeStream() != null) { + resultJjson.put("WSS_FLV", streamInfo.getTranscodeStream().getWss_flv().getUrl()); + }else { + resultJjson.put("WSS_FLV", streamInfo.getWss_flv().getUrl()); + } + } + resultJjson.put("RTMP", streamInfo.getRtmp().getUrl()); + if (streamInfo.getRtmps() != null) { + resultJjson.put("RTMPS", streamInfo.getRtmps().getUrl()); + } + resultJjson.put("HLS", streamInfo.getHls().getUrl()); + if (streamInfo.getHttps_hls() != null) { + resultJjson.put("HTTPS_HLS", streamInfo.getHttps_hls().getUrl()); + } + resultJjson.put("RTSP", streamInfo.getRtsp().getUrl()); + if (streamInfo.getRtsps() != null) { + resultJjson.put("RTSPS", streamInfo.getRtsps().getUrl()); + } + resultJjson.put("WEBRTC", streamInfo.getRtc().getUrl()); + if (streamInfo.getRtcs() != null) { + resultJjson.put("HTTPS_WEBRTC", streamInfo.getRtcs().getUrl()); + } + resultJjson.put("CDN", ""); + resultJjson.put("SnapURL", ""); + resultJjson.put("Transport", device.getTransport()); + resultJjson.put("StartAt", ""); + resultJjson.put("Duration", ""); + resultJjson.put("SourceVideoCodecName", ""); + resultJjson.put("SourceVideoWidth", ""); + resultJjson.put("SourceVideoHeight", ""); + resultJjson.put("SourceVideoFrameRate", ""); + resultJjson.put("SourceAudioCodecName", ""); + resultJjson.put("SourceAudioSampleRate", ""); + resultJjson.put("AudioEnable", ""); + resultJjson.put("Ondemand", ""); + resultJjson.put("InBytes", ""); + resultJjson.put("InBitRate", ""); + resultJjson.put("OutBytes", ""); + resultJjson.put("NumOutputs", ""); + resultJjson.put("CascadeSize", ""); + resultJjson.put("RelaySize", ""); + resultJjson.put("ChannelPTZType", "0"); + result.setResult(resultJjson); + }else { + JSONObject resultJjson = new JSONObject(); + resultJjson.put("error", "channel[ " + code + " ] " + msg); + result.setResult(resultJjson); + } + }else { + JSONObject resultJjson = new JSONObject(); + resultJjson.put("error", "channel[ " + code + " ] " + msg); + result.setResult(resultJjson); + } + }); + + return result; + } + + /** + * 实时直播 - 直播流停止 + * @param serial 设备编号 + * @param channel 通道序号 + * @param code 通道国标编号 + * @param check_outputs + * @return + */ + @GetMapping("/stop") + @ResponseBody + private JSONObject stop(String serial , + @RequestParam(required = false)Integer channel , + @RequestParam(required = false)String code, + @RequestParam(required = false)String check_outputs + + ){ + + + Device device = deviceService.getDeviceByDeviceId(serial); + if (device == null) { + JSONObject result = new JSONObject(); + result.put("error","未找到设备"); + return result; + } + DeviceChannel deviceChannel = deviceChannelService.getOne(serial, code); + if (deviceChannel == null) { + JSONObject result = new JSONObject(); + result.put("error","未找到通道"); + return result; + } + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceChannel.getId()); + if (inviteInfo == null) { + JSONObject result = new JSONObject(); + result.put("error","未找到流信息"); + return result; + } + + try { + cmder.streamByeCmd(device, code, "rtp", inviteInfo.getStream(), null, null); + } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { + JSONObject result = new JSONObject(); + result.put("error","发送BYE失败:" + e.getMessage()); + return result; + } + inviteStreamService.removeInviteInfo(inviteInfo); + deviceChannelService.stopPlay(inviteInfo.getChannelId()); + return null; + } + + /** + * 实时直播 - 直播流保活 + * @param serial 设备编号 + * @param channel 通道序号 + * @param code 通道国标编号 + * @return + */ + @GetMapping("/touch") + @ResponseBody + private JSONObject touch(String serial ,String t, + @RequestParam(required = false)Integer channel , + @RequestParam(required = false)String code, + @RequestParam(required = false)String autorestart, + @RequestParam(required = false)String audio, + @RequestParam(required = false)String cdn + ){ + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/gb28181/AuthController.java b/src/main/java/com/genersoft/iot/vmp/web/gb28181/AuthController.java new file mode 100644 index 0000000..caed2ad --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/gb28181/AuthController.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.web.gb28181; + +import com.genersoft.iot.vmp.service.IUserService; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import io.swagger.v3.oas.annotations.Hidden; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequestMapping(value = "/auth") +@Hidden +public class AuthController { + + @Autowired + private IUserService userService; + + @GetMapping("/login") + public String devices(String name, String passwd){ + User user = userService.getUser(name, passwd); + if (user != null) { + return "success"; + }else { + return "fail"; + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/gb28181/dto/DeviceChannelExtend.java b/src/main/java/com/genersoft/iot/vmp/web/gb28181/dto/DeviceChannelExtend.java new file mode 100644 index 0000000..e963075 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/gb28181/dto/DeviceChannelExtend.java @@ -0,0 +1,235 @@ +package com.genersoft.iot.vmp.web.gb28181.dto; + +import lombok.Data; + +@Data +public class DeviceChannelExtend { + + + /** + * 数据库自增ID + */ + private int id; + + /** + * 通道id + */ + private String channelId; + + /** + * 设备id + */ + private String deviceId; + + /** + * 通道名 + */ + private String name; + + private String deviceName; + + private boolean deviceOnline; + + /** + * 生产厂商 + */ + private String manufacture; + + /** + * 型号 + */ + private String model; + + /** + * 设备归属 + */ + private String owner; + + /** + * 行政区域 + */ + private String civilCode; + + /** + * 警区 + */ + private String block; + + /** + * 安装地址 + */ + private String address; + + /** + * 是否有子设备 1有, 0没有 + */ + private int parental; + + /** + * 父级id + */ + private String parentId; + + /** + * 信令安全模式 缺省为0; 0:不采用; 2: S/MIME签名方式; 3: S/ MIME加密签名同时采用方式; 4:数字摘要方式 + */ + private int safetyWay; + + /** + * 注册方式 缺省为1;1:符合IETFRFC3261标准的认证注册模 式; 2:基于口令的双向认证注册模式; 3:基于数字证书的双向认证注册模式 + */ + private int registerWay; + + /** + * 证书序列号 + */ + private String certNum; + + /** + * 证书有效标识 缺省为0;证书有效标识:0:无效1: 有效 + */ + private int certifiable; + + /** + * 证书无效原因码 + */ + private int errCode; + + /** + * 证书终止有效期 + */ + private String endTime; + + /** + * 保密属性 缺省为0; 0:不涉密, 1:涉密 + */ + private String secrecy; + + /** + * IP地址 + */ + private String ipAddress; + + /** + * 端口号 + */ + private int port; + + /** + * 密码 + */ + private String password; + + /** + * 云台类型 + */ + private int PTZType; + + /** + * 云台类型描述字符串 + */ + private String PTZTypeText; + + /** + * 创建时间 + */ + private String createTime; + + /** + * 更新时间 + */ + private String updateTime; + + /** + * 在线/离线 + * 1在线,0离线 + * 默认在线 + * 信令: + * ON + * OFF + * 遇到过NVR下的IPC下发信令可以推流, 但是 Status 响应 OFF + */ + private String status; + + /** + * 经度 + */ + private double longitude; + + /** + * 纬度 + */ + private double latitude; + + /** + * 经度 GCJ02 + */ + private double longitudeGcj02; + + /** + * 纬度 GCJ02 + */ + private double latitudeGcj02; + + /** + * 经度 WGS84 + */ + private double longitudeWgs84; + + /** + * 纬度 WGS84 + */ + private double latitudeWgs84; + + /** + * 子设备数 + */ + private int subCount; + + /** + * 流唯一编号,存在表示正在直播 + */ + private String streamId; + + /** + * 是否含有音频 + */ + private boolean hasAudio; + + /** + * 标记通道的类型,0->国标通道 1->直播流通道 2->业务分组/虚拟组织/行政区划 + */ + private int channelType; + + /** + * 业务分组 + */ + private String businessGroupId; + + /** + * GPS的更新时间 + */ + private String gpsTime; + + + public void setPTZType(int PTZType) { + this.PTZType = PTZType; + switch (PTZType) { + case 0: + this.PTZTypeText = "未知"; + break; + case 1: + this.PTZTypeText = "球机"; + break; + case 2: + this.PTZTypeText = "半球"; + break; + case 3: + this.PTZTypeText = "固定枪机"; + break; + case 4: + this.PTZTypeText = "遥控枪机"; + break; + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..67213e8 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,5 @@ +spring: + application: + name: wvp + profiles: + active: 274-dev diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..7a75e1c --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,7 @@ + ___ __ ___ ___ ________ ________ ________ ________ +|\ \ |\ \|\ \ / /|\ __ \ |\ __ \|\ __ \|\ __ \ +\ \ \ \ \ \ \ \ / / | \ \|\ \ ____________\ \ \|\ \ \ \|\ \ \ \|\ \ + \ \ \ __\ \ \ \ \/ / / \ \ ____\\____________\ \ ____\ \ _ _\ \ \\\ \ + \ \ \|\__\_\ \ \ / / \ \ \___\|____________|\ \ \___|\ \ \\ \\ \ \\\ \ + \ \____________\ \__/ / \ \__\ \ \__\ \ \__\\ _\\ \_______\ + \|____________|\|__|/ \|__| \|__| \|__|\|__|\|_______| diff --git a/src/main/resources/civilCode.csv b/src/main/resources/civilCode.csv new file mode 100644 index 0000000..6c2284a --- /dev/null +++ b/src/main/resources/civilCode.csv @@ -0,0 +1,3224 @@ +编号,名称,上级 +11,北京市, +1101,市辖区,11 +110101,东城区,1101 +110102,西城区,1101 +110105,朝阳区,1101 +110106,丰台区,1101 +110107,石景山区,1101 +110108,海淀区,1101 +110109,门头沟区,1101 +110111,房山区,1101 +110112,通州区,1101 +110113,顺义区,1101 +110114,昌平区,1101 +110115,大兴区,1101 +110116,怀柔区,1101 +110117,平谷区,1101 +110118,密云区,1101 +110119,延庆区,1101 +12,天津市, +1201,市辖区,12 +120101,和平区,1201 +120102,河东区,1201 +120103,河西区,1201 +120104,南开区,1201 +120105,河北区,1201 +120106,红桥区,1201 +120110,东丽区,1201 +120111,西青区,1201 +120112,津南区,1201 +120113,北辰区,1201 +120114,武清区,1201 +120115,宝坻区,1201 +120116,滨海新区,1201 +120117,宁河区,1201 +120118,静海区,1201 +120119,蓟州区,1201 +13,河北省, +1301,石家庄市,13 +130102,长安区,1301 +130104,桥西区,1301 +130105,新华区,1301 +130107,井陉矿区,1301 +130108,裕华区,1301 +130109,藁城区,1301 +130110,鹿泉区,1301 +130111,栾城区,1301 +130121,井陉县,1301 +130123,正定县,1301 +130125,行唐县,1301 +130126,灵寿县,1301 +130127,高邑县,1301 +130128,深泽县,1301 +130129,赞皇县,1301 +130130,无极县,1301 +130131,平山县,1301 +130132,元氏县,1301 +130133,赵县,1301 +130181,辛集市,1301 +130183,晋州市,1301 +130184,新乐市,1301 +1302,唐山市,13 +130202,路南区,1302 +130203,路北区,1302 +130204,古冶区,1302 +130205,开平区,1302 +130207,丰南区,1302 +130208,丰润区,1302 +130209,曹妃甸区,1302 +130224,滦南县,1302 +130225,乐亭县,1302 +130227,迁西县,1302 +130229,玉田县,1302 +130281,遵化市,1302 +130283,迁安市,1302 +130284,滦州市,1302 +1303,秦皇岛市,13 +130302,海港区,1303 +130303,山海关区,1303 +130304,北戴河区,1303 +130306,抚宁区,1303 +130321,青龙满族自治县,1303 +130322,昌黎县,1303 +130324,卢龙县,1303 +1304,邯郸市,13 +130402,邯山区,1304 +130403,丛台区,1304 +130404,复兴区,1304 +130406,峰峰矿区,1304 +130407,肥乡区,1304 +130408,永年区,1304 +130423,临漳县,1304 +130424,成安县,1304 +130425,大名县,1304 +130426,涉县,1304 +130427,磁县,1304 +130430,邱县,1304 +130431,鸡泽县,1304 +130432,广平县,1304 +130433,馆陶县,1304 +130434,魏县,1304 +130435,曲周县,1304 +130481,武安市,1304 +1305,邢台市,13 +130502,桥东区,1305 +130503,桥西区,1305 +130521,邢台县,1305 +130522,临城县,1305 +130523,内丘县,1305 +130524,柏乡县,1305 +130525,隆尧县,1305 +130526,任县,1305 +130527,南和县,1305 +130528,宁晋县,1305 +130529,巨鹿县,1305 +130530,新河县,1305 +130531,广宗县,1305 +130532,平乡县,1305 +130533,威县,1305 +130534,清河县,1305 +130535,临西县,1305 +130581,南宫市,1305 +130582,沙河市,1305 +1306,保定市,13 +130602,竞秀区,1306 +130606,莲池区,1306 +130607,满城区,1306 +130608,清苑区,1306 +130609,徐水区,1306 +130623,涞水县,1306 +130624,阜平县,1306 +130626,定兴县,1306 +130627,唐县,1306 +130628,高阳县,1306 +130629,容城县,1306 +130630,涞源县,1306 +130631,望都县,1306 +130632,安新县,1306 +130633,易县,1306 +130634,曲阳县,1306 +130635,蠡县,1306 +130636,顺平县,1306 +130637,博野县,1306 +130638,雄县,1306 +130681,涿州市,1306 +130682,定州市,1306 +130683,安国市,1306 +130684,高碑店市,1306 +1307,张家口市,13 +130702,桥东区,1307 +130703,桥西区,1307 +130705,宣化区,1307 +130706,下花园区,1307 +130708,万全区,1307 +130709,崇礼区,1307 +130722,张北县,1307 +130723,康保县,1307 +130724,沽源县,1307 +130725,尚义县,1307 +130726,蔚县,1307 +130727,阳原县,1307 +130728,怀安县,1307 +130730,怀来县,1307 +130731,涿鹿县,1307 +130732,赤城县,1307 +1308,承德市,13 +130802,双桥区,1308 +130803,双滦区,1308 +130804,鹰手营子矿区,1308 +130821,承德县,1308 +130822,兴隆县,1308 +130824,滦平县,1308 +130825,隆化县,1308 +130826,丰宁满族自治县,1308 +130827,宽城满族自治县,1308 +130828,围场满族蒙古族自治县,1308 +130881,平泉市,1308 +1309,沧州市,13 +130902,新华区,1309 +130903,运河区,1309 +130921,沧县,1309 +130922,青县,1309 +130923,东光县,1309 +130924,海兴县,1309 +130925,盐山县,1309 +130926,肃宁县,1309 +130927,南皮县,1309 +130928,吴桥县,1309 +130929,献县,1309 +130930,孟村回族自治县,1309 +130981,泊头市,1309 +130982,任丘市,1309 +130983,黄骅市,1309 +130984,河间市,1309 +1310,廊坊市,13 +131002,安次区,1310 +131003,广阳区,1310 +131022,固安县,1310 +131023,永清县,1310 +131024,香河县,1310 +131025,大城县,1310 +131026,文安县,1310 +131028,大厂回族自治县,1310 +131081,霸州市,1310 +131082,三河市,1310 +1311,衡水市,13 +131102,桃城区,1311 +131103,冀州区,1311 +131121,枣强县,1311 +131122,武邑县,1311 +131123,武强县,1311 +131124,饶阳县,1311 +131125,安平县,1311 +131126,故城县,1311 +131127,景县,1311 +131128,阜城县,1311 +131182,深州市,1311 +14,山西省, +1401,太原市,14 +140105,小店区,1401 +140106,迎泽区,1401 +140107,杏花岭区,1401 +140108,尖草坪区,1401 +140109,万柏林区,1401 +140110,晋源区,1401 +140121,清徐县,1401 +140122,阳曲县,1401 +140123,娄烦县,1401 +140181,古交市,1401 +1402,大同市,14 +140212,新荣区,1402 +140213,平城区,1402 +140214,云冈区,1402 +140215,云州区,1402 +140221,阳高县,1402 +140222,天镇县,1402 +140223,广灵县,1402 +140224,灵丘县,1402 +140225,浑源县,1402 +140226,左云县,1402 +1403,阳泉市,14 +140302,城区,1403 +140303,矿区,1403 +140311,郊区,1403 +140321,平定县,1403 +140322,盂县,1403 +1404,长治市,14 +140403,潞州区,1404 +140404,上党区,1404 +140405,屯留区,1404 +140406,潞城区,1404 +140423,襄垣县,1404 +140425,平顺县,1404 +140426,黎城县,1404 +140427,壶关县,1404 +140428,长子县,1404 +140429,武乡县,1404 +140430,沁县,1404 +140431,沁源县,1404 +1405,晋城市,14 +140502,城区,1405 +140521,沁水县,1405 +140522,阳城县,1405 +140524,陵川县,1405 +140525,泽州县,1405 +140581,高平市,1405 +1406,朔州市,14 +140602,朔城区,1406 +140603,平鲁区,1406 +140621,山阴县,1406 +140622,应县,1406 +140623,右玉县,1406 +140681,怀仁市,1406 +1407,晋中市,14 +140702,榆次区,1407 +140721,榆社县,1407 +140722,左权县,1407 +140723,和顺县,1407 +140724,昔阳县,1407 +140725,寿阳县,1407 +140726,太谷县,1407 +140727,祁县,1407 +140728,平遥县,1407 +140729,灵石县,1407 +140781,介休市,1407 +1408,运城市,14 +140802,盐湖区,1408 +140821,临猗县,1408 +140822,万荣县,1408 +140823,闻喜县,1408 +140824,稷山县,1408 +140825,新绛县,1408 +140826,绛县,1408 +140827,垣曲县,1408 +140828,夏县,1408 +140829,平陆县,1408 +140830,芮城县,1408 +140881,永济市,1408 +140882,河津市,1408 +1409,忻州市,14 +140902,忻府区,1409 +140921,定襄县,1409 +140922,五台县,1409 +140923,代县,1409 +140924,繁峙县,1409 +140925,宁武县,1409 +140926,静乐县,1409 +140927,神池县,1409 +140928,五寨县,1409 +140929,岢岚县,1409 +140930,河曲县,1409 +140931,保德县,1409 +140932,偏关县,1409 +140981,原平市,1409 +1410,临汾市,14 +141002,尧都区,1410 +141021,曲沃县,1410 +141022,翼城县,1410 +141023,襄汾县,1410 +141024,洪洞县,1410 +141025,古县,1410 +141026,安泽县,1410 +141027,浮山县,1410 +141028,吉县,1410 +141029,乡宁县,1410 +141030,大宁县,1410 +141031,隰县,1410 +141032,永和县,1410 +141033,蒲县,1410 +141034,汾西县,1410 +141081,侯马市,1410 +141082,霍州市,1410 +1411,吕梁市,14 +141102,离石区,1411 +141121,文水县,1411 +141122,交城县,1411 +141123,兴县,1411 +141124,临县,1411 +141125,柳林县,1411 +141126,石楼县,1411 +141127,岚县,1411 +141128,方山县,1411 +141129,中阳县,1411 +141130,交口县,1411 +141181,孝义市,1411 +141182,汾阳市,1411 +15,内蒙古自治区, +1501,呼和浩特市,15 +150102,新城区,1501 +150103,回民区,1501 +150104,玉泉区,1501 +150105,赛罕区,1501 +150121,土默特左旗,1501 +150122,托克托县,1501 +150123,和林格尔县,1501 +150124,清水河县,1501 +150125,武川县,1501 +1502,包头市,15 +150202,东河区,1502 +150203,昆都仑区,1502 +150204,青山区,1502 +150205,石拐区,1502 +150206,白云鄂博矿区,1502 +150207,九原区,1502 +150221,土默特右旗,1502 +150222,固阳县,1502 +150223,达尔罕茂明安联合旗,1502 +1503,乌海市,15 +150302,海勃湾区,1503 +150303,海南区,1503 +150304,乌达区,1503 +1504,赤峰市,15 +150402,红山区,1504 +150403,元宝山区,1504 +150404,松山区,1504 +150421,阿鲁科尔沁旗,1504 +150422,巴林左旗,1504 +150423,巴林右旗,1504 +150424,林西县,1504 +150425,克什克腾旗,1504 +150426,翁牛特旗,1504 +150428,喀喇沁旗,1504 +150429,宁城县,1504 +150430,敖汉旗,1504 +1505,通辽市,15 +150502,科尔沁区,1505 +150521,科尔沁左翼中旗,1505 +150522,科尔沁左翼后旗,1505 +150523,开鲁县,1505 +150524,库伦旗,1505 +150525,奈曼旗,1505 +150526,扎鲁特旗,1505 +150581,霍林郭勒市,1505 +1506,鄂尔多斯市,15 +150602,东胜区,1506 +150603,康巴什区,1506 +150621,达拉特旗,1506 +150622,准格尔旗,1506 +150623,鄂托克前旗,1506 +150624,鄂托克旗,1506 +150625,杭锦旗,1506 +150626,乌审旗,1506 +150627,伊金霍洛旗,1506 +1507,呼伦贝尔市,15 +150702,海拉尔区,1507 +150703,扎赉诺尔区,1507 +150721,阿荣旗,1507 +150722,莫力达瓦达斡尔族自治旗,1507 +150723,鄂伦春自治旗,1507 +150724,鄂温克族自治旗,1507 +150725,陈巴尔虎旗,1507 +150726,新巴尔虎左旗,1507 +150727,新巴尔虎右旗,1507 +150781,满洲里市,1507 +150782,牙克石市,1507 +150783,扎兰屯市,1507 +150784,额尔古纳市,1507 +150785,根河市,1507 +1508,巴彦淖尔市,15 +150802,临河区,1508 +150821,五原县,1508 +150822,磴口县,1508 +150823,乌拉特前旗,1508 +150824,乌拉特中旗,1508 +150825,乌拉特后旗,1508 +150826,杭锦后旗,1508 +1509,乌兰察布市,15 +150902,集宁区,1509 +150921,卓资县,1509 +150922,化德县,1509 +150923,商都县,1509 +150924,兴和县,1509 +150925,凉城县,1509 +150926,察哈尔右翼前旗,1509 +150927,察哈尔右翼中旗,1509 +150928,察哈尔右翼后旗,1509 +150929,四子王旗,1509 +150981,丰镇市,1509 +1522,兴安盟,15 +152201,乌兰浩特市,1522 +152202,阿尔山市,1522 +152221,科尔沁右翼前旗,1522 +152222,科尔沁右翼中旗,1522 +152223,扎赉特旗,1522 +152224,突泉县,1522 +1525,锡林郭勒盟,15 +152501,二连浩特市,1525 +152502,锡林浩特市,1525 +152522,阿巴嘎旗,1525 +152523,苏尼特左旗,1525 +152524,苏尼特右旗,1525 +152525,东乌珠穆沁旗,1525 +152526,西乌珠穆沁旗,1525 +152527,太仆寺旗,1525 +152528,镶黄旗,1525 +152529,正镶白旗,1525 +152530,正蓝旗,1525 +152531,多伦县,1525 +1529,阿拉善盟,15 +152921,阿拉善左旗,1529 +152922,阿拉善右旗,1529 +152923,额济纳旗,1529 +21,辽宁省, +2101,沈阳市,21 +210102,和平区,2101 +210103,沈河区,2101 +210104,大东区,2101 +210105,皇姑区,2101 +210106,铁西区,2101 +210111,苏家屯区,2101 +210112,浑南区,2101 +210113,沈北新区,2101 +210114,于洪区,2101 +210115,辽中区,2101 +210123,康平县,2101 +210124,法库县,2101 +210181,新民市,2101 +2102,大连市,21 +210202,中山区,2102 +210203,西岗区,2102 +210204,沙河口区,2102 +210211,甘井子区,2102 +210212,旅顺口区,2102 +210213,金州区,2102 +210214,普兰店区,2102 +210224,长海县,2102 +210281,瓦房店市,2102 +210283,庄河市,2102 +2103,鞍山市,21 +210302,铁东区,2103 +210303,铁西区,2103 +210304,立山区,2103 +210311,千山区,2103 +210321,台安县,2103 +210323,岫岩满族自治县,2103 +210381,海城市,2103 +2104,抚顺市,21 +210402,新抚区,2104 +210403,东洲区,2104 +210404,望花区,2104 +210411,顺城区,2104 +210421,抚顺县,2104 +210422,新宾满族自治县,2104 +210423,清原满族自治县,2104 +2105,本溪市,21 +210502,平山区,2105 +210503,溪湖区,2105 +210504,明山区,2105 +210505,南芬区,2105 +210521,本溪满族自治县,2105 +210522,桓仁满族自治县,2105 +2106,丹东市,21 +210602,元宝区,2106 +210603,振兴区,2106 +210604,振安区,2106 +210624,宽甸满族自治县,2106 +210681,东港市,2106 +210682,凤城市,2106 +2107,锦州市,21 +210702,古塔区,2107 +210703,凌河区,2107 +210711,太和区,2107 +210726,黑山县,2107 +210727,义县,2107 +210781,凌海市,2107 +210782,北镇市,2107 +2108,营口市,21 +210802,站前区,2108 +210803,西市区,2108 +210804,鲅鱼圈区,2108 +210811,老边区,2108 +210881,盖州市,2108 +210882,大石桥市,2108 +2109,阜新市,21 +210902,海州区,2109 +210903,新邱区,2109 +210904,太平区,2109 +210905,清河门区,2109 +210911,细河区,2109 +210921,阜新蒙古族自治县,2109 +210922,彰武县,2109 +2110,辽阳市,21 +211002,白塔区,2110 +211003,文圣区,2110 +211004,宏伟区,2110 +211005,弓长岭区,2110 +211011,太子河区,2110 +211021,辽阳县,2110 +211081,灯塔市,2110 +2111,盘锦市,21 +211102,双台子区,2111 +211103,兴隆台区,2111 +211104,大洼区,2111 +211122,盘山县,2111 +2112,铁岭市,21 +211202,银州区,2112 +211204,清河区,2112 +211221,铁岭县,2112 +211223,西丰县,2112 +211224,昌图县,2112 +211281,调兵山市,2112 +211282,开原市,2112 +2113,朝阳市,21 +211302,双塔区,2113 +211303,龙城区,2113 +211321,朝阳县,2113 +211322,建平县,2113 +211324,喀喇沁左翼蒙古族自治县,2113 +211381,北票市,2113 +211382,凌源市,2113 +2114,葫芦岛市,21 +211402,连山区,2114 +211403,龙港区,2114 +211404,南票区,2114 +211421,绥中县,2114 +211422,建昌县,2114 +211481,兴城市,2114 +22,吉林省, +2201,长春市,22 +220102,南关区,2201 +220103,宽城区,2201 +220104,朝阳区,2201 +220105,二道区,2201 +220106,绿园区,2201 +220112,双阳区,2201 +220113,九台区,2201 +220122,农安县,2201 +220182,榆树市,2201 +220183,德惠市,2201 +2202,吉林市,22 +220202,昌邑区,2202 +220203,龙潭区,2202 +220204,船营区,2202 +220211,丰满区,2202 +220221,永吉县,2202 +220281,蛟河市,2202 +220282,桦甸市,2202 +220283,舒兰市,2202 +220284,磐石市,2202 +2203,四平市,22 +220302,铁西区,2203 +220303,铁东区,2203 +220322,梨树县,2203 +220323,伊通满族自治县,2203 +220381,公主岭市,2203 +220382,双辽市,2203 +2204,辽源市,22 +220402,龙山区,2204 +220403,西安区,2204 +220421,东丰县,2204 +220422,东辽县,2204 +2205,通化市,22 +220502,东昌区,2205 +220503,二道江区,2205 +220521,通化县,2205 +220523,辉南县,2205 +220524,柳河县,2205 +220581,梅河口市,2205 +220582,集安市,2205 +2206,白山市,22 +220602,浑江区,2206 +220605,江源区,2206 +220621,抚松县,2206 +220622,靖宇县,2206 +220623,长白朝鲜族自治县,2206 +220681,临江市,2206 +2207,松原市,22 +220702,宁江区,2207 +220721,前郭尔罗斯蒙古族自治县,2207 +220722,长岭县,2207 +220723,乾安县,2207 +220781,扶余市,2207 +2208,白城市,22 +220802,洮北区,2208 +220821,镇赉县,2208 +220822,通榆县,2208 +220881,洮南市,2208 +220882,大安市,2208 +2224,延边朝鲜族自治州,22 +222401,延吉市,2224 +222402,图们市,2224 +222403,敦化市,2224 +222404,珲春市,2224 +222405,龙井市,2224 +222406,和龙市,2224 +222424,汪清县,2224 +222426,安图县,2224 +23,黑龙江省, +2301,哈尔滨市,23 +230102,道里区,2301 +230103,南岗区,2301 +230104,道外区,2301 +230108,平房区,2301 +230109,松北区,2301 +230110,香坊区,2301 +230111,呼兰区,2301 +230112,阿城区,2301 +230113,双城区,2301 +230123,依兰县,2301 +230124,方正县,2301 +230125,宾县,2301 +230126,巴彦县,2301 +230127,木兰县,2301 +230128,通河县,2301 +230129,延寿县,2301 +230183,尚志市,2301 +230184,五常市,2301 +2302,齐齐哈尔市,23 +230202,龙沙区,2302 +230203,建华区,2302 +230204,铁锋区,2302 +230205,昂昂溪区,2302 +230206,富拉尔基区,2302 +230207,碾子山区,2302 +230208,梅里斯达斡尔族区,2302 +230221,龙江县,2302 +230223,依安县,2302 +230224,泰来县,2302 +230225,甘南县,2302 +230227,富裕县,2302 +230229,克山县,2302 +230230,克东县,2302 +230231,拜泉县,2302 +230281,讷河市,2302 +2303,鸡西市,23 +230302,鸡冠区,2303 +230303,恒山区,2303 +230304,滴道区,2303 +230305,梨树区,2303 +230306,城子河区,2303 +230307,麻山区,2303 +230321,鸡东县,2303 +230381,虎林市,2303 +230382,密山市,2303 +2304,鹤岗市,23 +230402,向阳区,2304 +230403,工农区,2304 +230404,南山区,2304 +230405,兴安区,2304 +230406,东山区,2304 +230407,兴山区,2304 +230421,萝北县,2304 +230422,绥滨县,2304 +2305,双鸭山市,23 +230502,尖山区,2305 +230503,岭东区,2305 +230505,四方台区,2305 +230506,宝山区,2305 +230521,集贤县,2305 +230522,友谊县,2305 +230523,宝清县,2305 +230524,饶河县,2305 +2306,大庆市,23 +230602,萨尔图区,2306 +230603,龙凤区,2306 +230604,让胡路区,2306 +230605,红岗区,2306 +230606,大同区,2306 +230621,肇州县,2306 +230622,肇源县,2306 +230623,林甸县,2306 +230624,杜尔伯特蒙古族自治县,2306 +2307,伊春市,23 +230702,伊春区,2307 +230703,南岔区,2307 +230704,友好区,2307 +230705,西林区,2307 +230706,翠峦区,2307 +230707,新青区,2307 +230708,美溪区,2307 +230709,金山屯区,2307 +230710,五营区,2307 +230711,乌马河区,2307 +230712,汤旺河区,2307 +230713,带岭区,2307 +230714,乌伊岭区,2307 +230715,红星区,2307 +230716,上甘岭区,2307 +230722,嘉荫县,2307 +230781,铁力市,2307 +2308,佳木斯市,23 +230803,向阳区,2308 +230804,前进区,2308 +230805,东风区,2308 +230811,郊区,2308 +230822,桦南县,2308 +230826,桦川县,2308 +230828,汤原县,2308 +230881,同江市,2308 +230882,富锦市,2308 +230883,抚远市,2308 +2309,七台河市,23 +230902,新兴区,2309 +230903,桃山区,2309 +230904,茄子河区,2309 +230921,勃利县,2309 +2310,牡丹江市,23 +231002,东安区,2310 +231003,阳明区,2310 +231004,爱民区,2310 +231005,西安区,2310 +231025,林口县,2310 +231081,绥芬河市,2310 +231083,海林市,2310 +231084,宁安市,2310 +231085,穆棱市,2310 +231086,东宁市,2310 +2311,黑河市,23 +231102,爱辉区,2311 +231121,嫩江县,2311 +231123,逊克县,2311 +231124,孙吴县,2311 +231181,北安市,2311 +231182,五大连池市,2311 +2312,绥化市,23 +231202,北林区,2312 +231221,望奎县,2312 +231222,兰西县,2312 +231223,青冈县,2312 +231224,庆安县,2312 +231225,明水县,2312 +231226,绥棱县,2312 +231281,安达市,2312 +231282,肇东市,2312 +231283,海伦市,2312 +2327,大兴安岭地区,23 +232701,漠河市,2327 +232721,呼玛县,2327 +232722,塔河县,2327 +31,上海市, +3101,市辖区,31 +310101,黄浦区,3101 +310104,徐汇区,3101 +310105,长宁区,3101 +310106,静安区,3101 +310107,普陀区,3101 +310109,虹口区,3101 +310110,杨浦区,3101 +310112,闵行区,3101 +310113,宝山区,3101 +310114,嘉定区,3101 +310115,浦东新区,3101 +310116,金山区,3101 +310117,松江区,3101 +310118,青浦区,3101 +310120,奉贤区,3101 +310151,崇明区,3101 +32,江苏省, +3201,南京市,32 +320102,玄武区,3201 +320104,秦淮区,3201 +320105,建邺区,3201 +320106,鼓楼区,3201 +320111,浦口区,3201 +320113,栖霞区,3201 +320114,雨花台区,3201 +320115,江宁区,3201 +320116,六合区,3201 +320117,溧水区,3201 +320118,高淳区,3201 +3202,无锡市,32 +320205,锡山区,3202 +320206,惠山区,3202 +320211,滨湖区,3202 +320213,梁溪区,3202 +320214,新吴区,3202 +320281,江阴市,3202 +320282,宜兴市,3202 +3203,徐州市,32 +320302,鼓楼区,3203 +320303,云龙区,3203 +320305,贾汪区,3203 +320311,泉山区,3203 +320312,铜山区,3203 +320321,丰县,3203 +320322,沛县,3203 +320324,睢宁县,3203 +320381,新沂市,3203 +320382,邳州市,3203 +3204,常州市,32 +320402,天宁区,3204 +320404,钟楼区,3204 +320411,新北区,3204 +320412,武进区,3204 +320413,金坛区,3204 +320481,溧阳市,3204 +3205,苏州市,32 +320505,虎丘区,3205 +320506,吴中区,3205 +320507,相城区,3205 +320508,姑苏区,3205 +320509,吴江区,3205 +320581,常熟市,3205 +320582,张家港市,3205 +320583,昆山市,3205 +320585,太仓市,3205 +3206,南通市,32 +320602,崇川区,3206 +320611,港闸区,3206 +320612,通州区,3206 +320623,如东县,3206 +320681,启东市,3206 +320682,如皋市,3206 +320684,海门市,3206 +320685,海安市,3206 +3207,连云港市,32 +320703,连云区,3207 +320706,海州区,3207 +320707,赣榆区,3207 +320722,东海县,3207 +320723,灌云县,3207 +320724,灌南县,3207 +3208,淮安市,32 +320803,淮安区,3208 +320804,淮阴区,3208 +320812,清江浦区,3208 +320813,洪泽区,3208 +320826,涟水县,3208 +320830,盱眙县,3208 +320831,金湖县,3208 +3209,盐城市,32 +320902,亭湖区,3209 +320903,盐都区,3209 +320904,大丰区,3209 +320921,响水县,3209 +320922,滨海县,3209 +320923,阜宁县,3209 +320924,射阳县,3209 +320925,建湖县,3209 +320981,东台市,3209 +3210,扬州市,32 +321002,广陵区,3210 +321003,邗江区,3210 +321012,江都区,3210 +321023,宝应县,3210 +321081,仪征市,3210 +321084,高邮市,3210 +3211,镇江市,32 +321102,京口区,3211 +321111,润州区,3211 +321112,丹徒区,3211 +321181,丹阳市,3211 +321182,扬中市,3211 +321183,句容市,3211 +3212,泰州市,32 +321202,海陵区,3212 +321203,高港区,3212 +321204,姜堰区,3212 +321281,兴化市,3212 +321282,靖江市,3212 +321283,泰兴市,3212 +3213,宿迁市,32 +321302,宿城区,3213 +321311,宿豫区,3213 +321322,沭阳县,3213 +321323,泗阳县,3213 +321324,泗洪县,3213 +33,浙江省, +3301,杭州市,33 +330102,上城区,3301 +330103,下城区,3301 +330104,江干区,3301 +330105,拱墅区,3301 +330106,西湖区,3301 +330108,滨江区,3301 +330109,萧山区,3301 +330110,余杭区,3301 +330111,富阳区,3301 +330112,临安区,3301 +330122,桐庐县,3301 +330127,淳安县,3301 +330182,建德市,3301 +3302,宁波市,33 +330203,海曙区,3302 +330205,江北区,3302 +330206,北仑区,3302 +330211,镇海区,3302 +330212,鄞州区,3302 +330213,奉化区,3302 +330225,象山县,3302 +330226,宁海县,3302 +330281,余姚市,3302 +330282,慈溪市,3302 +3303,温州市,33 +330302,鹿城区,3303 +330303,龙湾区,3303 +330304,瓯海区,3303 +330305,洞头区,3303 +330324,永嘉县,3303 +330326,平阳县,3303 +330327,苍南县,3303 +330328,文成县,3303 +330329,泰顺县,3303 +330381,瑞安市,3303 +330382,乐清市,3303 +3304,嘉兴市,33 +330402,南湖区,3304 +330411,秀洲区,3304 +330421,嘉善县,3304 +330424,海盐县,3304 +330481,海宁市,3304 +330482,平湖市,3304 +330483,桐乡市,3304 +3305,湖州市,33 +330502,吴兴区,3305 +330503,南浔区,3305 +330521,德清县,3305 +330522,长兴县,3305 +330523,安吉县,3305 +3306,绍兴市,33 +330602,越城区,3306 +330603,柯桥区,3306 +330604,上虞区,3306 +330624,新昌县,3306 +330681,诸暨市,3306 +330683,嵊州市,3306 +3307,金华市,33 +330702,婺城区,3307 +330703,金东区,3307 +330723,武义县,3307 +330726,浦江县,3307 +330727,磐安县,3307 +330781,兰溪市,3307 +330782,义乌市,3307 +330783,东阳市,3307 +330784,永康市,3307 +3308,衢州市,33 +330802,柯城区,3308 +330803,衢江区,3308 +330822,常山县,3308 +330824,开化县,3308 +330825,龙游县,3308 +330881,江山市,3308 +3309,舟山市,33 +330902,定海区,3309 +330903,普陀区,3309 +330921,岱山县,3309 +330922,嵊泗县,3309 +3310,台州市,33 +331002,椒江区,3310 +331003,黄岩区,3310 +331004,路桥区,3310 +331022,三门县,3310 +331023,天台县,3310 +331024,仙居县,3310 +331081,温岭市,3310 +331082,临海市,3310 +331083,玉环市,3310 +3311,丽水市,33 +331102,莲都区,3311 +331121,青田县,3311 +331122,缙云县,3311 +331123,遂昌县,3311 +331124,松阳县,3311 +331125,云和县,3311 +331126,庆元县,3311 +331127,景宁畲族自治县,3311 +331181,龙泉市,3311 +34,安徽省, +3401,合肥市,34 +340102,瑶海区,3401 +340103,庐阳区,3401 +340104,蜀山区,3401 +340111,包河区,3401 +340121,长丰县,3401 +340122,肥东县,3401 +340123,肥西县,3401 +340124,庐江县,3401 +340181,巢湖市,3401 +3402,芜湖市,34 +340202,镜湖区,3402 +340203,弋江区,3402 +340207,鸠江区,3402 +340208,三山区,3402 +340221,芜湖县,3402 +340222,繁昌县,3402 +340223,南陵县,3402 +340225,无为县,3402 +3403,蚌埠市,34 +340302,龙子湖区,3403 +340303,蚌山区,3403 +340304,禹会区,3403 +340311,淮上区,3403 +340321,怀远县,3403 +340322,五河县,3403 +340323,固镇县,3403 +3404,淮南市,34 +340402,大通区,3404 +340403,田家庵区,3404 +340404,谢家集区,3404 +340405,八公山区,3404 +340406,潘集区,3404 +340421,凤台县,3404 +340422,寿县,3404 +3405,马鞍山市,34 +340503,花山区,3405 +340504,雨山区,3405 +340506,博望区,3405 +340521,当涂县,3405 +340522,含山县,3405 +340523,和县,3405 +3406,淮北市,34 +340602,杜集区,3406 +340603,相山区,3406 +340604,烈山区,3406 +340621,濉溪县,3406 +3407,铜陵市,34 +340705,铜官区,3407 +340706,义安区,3407 +340711,郊区,3407 +340722,枞阳县,3407 +3408,安庆市,34 +340802,迎江区,3408 +340803,大观区,3408 +340811,宜秀区,3408 +340822,怀宁县,3408 +340825,太湖县,3408 +340826,宿松县,3408 +340827,望江县,3408 +340828,岳西县,3408 +340881,桐城市,3408 +340882,潜山市,3408 +3410,黄山市,34 +341002,屯溪区,3410 +341003,黄山区,3410 +341004,徽州区,3410 +341021,歙县,3410 +341022,休宁县,3410 +341023,黟县,3410 +341024,祁门县,3410 +3411,滁州市,34 +341102,琅琊区,3411 +341103,南谯区,3411 +341122,来安县,3411 +341124,全椒县,3411 +341125,定远县,3411 +341126,凤阳县,3411 +341181,天长市,3411 +341182,明光市,3411 +3412,阜阳市,34 +341202,颍州区,3412 +341203,颍东区,3412 +341204,颍泉区,3412 +341221,临泉县,3412 +341222,太和县,3412 +341225,阜南县,3412 +341226,颍上县,3412 +341282,界首市,3412 +3413,宿州市,34 +341302,埇桥区,3413 +341321,砀山县,3413 +341322,萧县,3413 +341323,灵璧县,3413 +341324,泗县,3413 +3415,六安市,34 +341502,金安区,3415 +341503,裕安区,3415 +341504,叶集区,3415 +341522,霍邱县,3415 +341523,舒城县,3415 +341524,金寨县,3415 +341525,霍山县,3415 +3416,亳州市,34 +341602,谯城区,3416 +341621,涡阳县,3416 +341622,蒙城县,3416 +341623,利辛县,3416 +3417,池州市,34 +341702,贵池区,3417 +341721,东至县,3417 +341722,石台县,3417 +341723,青阳县,3417 +3418,宣城市,34 +341802,宣州区,3418 +341821,郎溪县,3418 +341822,广德县,3418 +341823,泾县,3418 +341824,绩溪县,3418 +341825,旌德县,3418 +341881,宁国市,3418 +35,福建省, +3501,福州市,35 +350102,鼓楼区,3501 +350103,台江区,3501 +350104,仓山区,3501 +350105,马尾区,3501 +350111,晋安区,3501 +350112,长乐区,3501 +350121,闽侯县,3501 +350122,连江县,3501 +350123,罗源县,3501 +350124,闽清县,3501 +350125,永泰县,3501 +350128,平潭县,3501 +350181,福清市,3501 +3502,厦门市,35 +350203,思明区,3502 +350205,海沧区,3502 +350206,湖里区,3502 +350211,集美区,3502 +350212,同安区,3502 +350213,翔安区,3502 +3503,莆田市,35 +350302,城厢区,3503 +350303,涵江区,3503 +350304,荔城区,3503 +350305,秀屿区,3503 +350322,仙游县,3503 +3504,三明市,35 +350402,梅列区,3504 +350403,三元区,3504 +350421,明溪县,3504 +350423,清流县,3504 +350424,宁化县,3504 +350425,大田县,3504 +350426,尤溪县,3504 +350427,沙县,3504 +350428,将乐县,3504 +350429,泰宁县,3504 +350430,建宁县,3504 +350481,永安市,3504 +3505,泉州市,35 +350502,鲤城区,3505 +350503,丰泽区,3505 +350504,洛江区,3505 +350505,泉港区,3505 +350521,惠安县,3505 +350524,安溪县,3505 +350525,永春县,3505 +350526,德化县,3505 +350527,金门县,3505 +350581,石狮市,3505 +350582,晋江市,3505 +350583,南安市,3505 +3506,漳州市,35 +350602,芗城区,3506 +350603,龙文区,3506 +350622,云霄县,3506 +350623,漳浦县,3506 +350624,诏安县,3506 +350625,长泰县,3506 +350626,东山县,3506 +350627,南靖县,3506 +350628,平和县,3506 +350629,华安县,3506 +350681,龙海市,3506 +3507,南平市,35 +350702,延平区,3507 +350703,建阳区,3507 +350721,顺昌县,3507 +350722,浦城县,3507 +350723,光泽县,3507 +350724,松溪县,3507 +350725,政和县,3507 +350781,邵武市,3507 +350782,武夷山市,3507 +350783,建瓯市,3507 +3508,龙岩市,35 +350802,新罗区,3508 +350803,永定区,3508 +350821,长汀县,3508 +350823,上杭县,3508 +350824,武平县,3508 +350825,连城县,3508 +350881,漳平市,3508 +3509,宁德市,35 +350902,蕉城区,3509 +350921,霞浦县,3509 +350922,古田县,3509 +350923,屏南县,3509 +350924,寿宁县,3509 +350925,周宁县,3509 +350926,柘荣县,3509 +350981,福安市,3509 +350982,福鼎市,3509 +36,江西省, +3601,南昌市,36 +360102,东湖区,3601 +360103,西湖区,3601 +360104,青云谱区,3601 +360105,湾里区,3601 +360111,青山湖区,3601 +360112,新建区,3601 +360121,南昌县,3601 +360123,安义县,3601 +360124,进贤县,3601 +3602,景德镇市,36 +360202,昌江区,3602 +360203,珠山区,3602 +360222,浮梁县,3602 +360281,乐平市,3602 +3603,萍乡市,36 +360302,安源区,3603 +360313,湘东区,3603 +360321,莲花县,3603 +360322,上栗县,3603 +360323,芦溪县,3603 +3604,九江市,36 +360402,濂溪区,3604 +360403,浔阳区,3604 +360404,柴桑区,3604 +360423,武宁县,3604 +360424,修水县,3604 +360425,永修县,3604 +360426,德安县,3604 +360428,都昌县,3604 +360429,湖口县,3604 +360430,彭泽县,3604 +360481,瑞昌市,3604 +360482,共青城市,3604 +360483,庐山市,3604 +3605,新余市,36 +360502,渝水区,3605 +360521,分宜县,3605 +3606,鹰潭市,36 +360602,月湖区,3606 +360603,余江区,3606 +360681,贵溪市,3606 +3607,赣州市,36 +360702,章贡区,3607 +360703,南康区,3607 +360704,赣县区,3607 +360722,信丰县,3607 +360723,大余县,3607 +360724,上犹县,3607 +360725,崇义县,3607 +360726,安远县,3607 +360727,龙南县,3607 +360728,定南县,3607 +360729,全南县,3607 +360730,宁都县,3607 +360731,于都县,3607 +360732,兴国县,3607 +360733,会昌县,3607 +360734,寻乌县,3607 +360735,石城县,3607 +360781,瑞金市,3607 +3608,吉安市,36 +360802,吉州区,3608 +360803,青原区,3608 +360821,吉安县,3608 +360822,吉水县,3608 +360823,峡江县,3608 +360824,新干县,3608 +360825,永丰县,3608 +360826,泰和县,3608 +360827,遂川县,3608 +360828,万安县,3608 +360829,安福县,3608 +360830,永新县,3608 +360881,井冈山市,3608 +3609,宜春市,36 +360902,袁州区,3609 +360921,奉新县,3609 +360922,万载县,3609 +360923,上高县,3609 +360924,宜丰县,3609 +360925,靖安县,3609 +360926,铜鼓县,3609 +360981,丰城市,3609 +360982,樟树市,3609 +360983,高安市,3609 +3610,抚州市,36 +361002,临川区,3610 +361003,东乡区,3610 +361021,南城县,3610 +361022,黎川县,3610 +361023,南丰县,3610 +361024,崇仁县,3610 +361025,乐安县,3610 +361026,宜黄县,3610 +361027,金溪县,3610 +361028,资溪县,3610 +361030,广昌县,3610 +3611,上饶市,36 +361102,信州区,3611 +361103,广丰区,3611 +361121,上饶县,3611 +361123,玉山县,3611 +361124,铅山县,3611 +361125,横峰县,3611 +361126,弋阳县,3611 +361127,余干县,3611 +361128,鄱阳县,3611 +361129,万年县,3611 +361130,婺源县,3611 +361181,德兴市,3611 +37,山东省, +3701,济南市,37 +370102,历下区,3701 +370103,市中区,3701 +370104,槐荫区,3701 +370105,天桥区,3701 +370112,历城区,3701 +370113,长清区,3701 +370114,章丘区,3701 +370115,济阳区,3701 +370124,平阴县,3701 +370126,商河县,3701 +3702,青岛市,37 +370202,市南区,3702 +370203,市北区,3702 +370211,黄岛区,3702 +370212,崂山区,3702 +370213,李沧区,3702 +370214,城阳区,3702 +370215,即墨区,3702 +370281,胶州市,3702 +370283,平度市,3702 +370285,莱西市,3702 +3703,淄博市,37 +370302,淄川区,3703 +370303,张店区,3703 +370304,博山区,3703 +370305,临淄区,3703 +370306,周村区,3703 +370321,桓台县,3703 +370322,高青县,3703 +370323,沂源县,3703 +3704,枣庄市,37 +370402,市中区,3704 +370403,薛城区,3704 +370404,峄城区,3704 +370405,台儿庄区,3704 +370406,山亭区,3704 +370481,滕州市,3704 +3705,东营市,37 +370502,东营区,3705 +370503,河口区,3705 +370505,垦利区,3705 +370522,利津县,3705 +370523,广饶县,3705 +3706,烟台市,37 +370602,芝罘区,3706 +370611,福山区,3706 +370612,牟平区,3706 +370613,莱山区,3706 +370634,长岛县,3706 +370681,龙口市,3706 +370682,莱阳市,3706 +370683,莱州市,3706 +370684,蓬莱市,3706 +370685,招远市,3706 +370686,栖霞市,3706 +370687,海阳市,3706 +3707,潍坊市,37 +370702,潍城区,3707 +370703,寒亭区,3707 +370704,坊子区,3707 +370705,奎文区,3707 +370724,临朐县,3707 +370725,昌乐县,3707 +370781,青州市,3707 +370782,诸城市,3707 +370783,寿光市,3707 +370784,安丘市,3707 +370785,高密市,3707 +370786,昌邑市,3707 +3708,济宁市,37 +370811,任城区,3708 +370812,兖州区,3708 +370826,微山县,3708 +370827,鱼台县,3708 +370828,金乡县,3708 +370829,嘉祥县,3708 +370830,汶上县,3708 +370831,泗水县,3708 +370832,梁山县,3708 +370881,曲阜市,3708 +370883,邹城市,3708 +3709,泰安市,37 +370902,泰山区,3709 +370911,岱岳区,3709 +370921,宁阳县,3709 +370923,东平县,3709 +370982,新泰市,3709 +370983,肥城市,3709 +3710,威海市,37 +371002,环翠区,3710 +371003,文登区,3710 +371082,荣成市,3710 +371083,乳山市,3710 +3711,日照市,37 +371102,东港区,3711 +371103,岚山区,3711 +371121,五莲县,3711 +371122,莒县,3711 +3712,莱芜市,37 +371202,莱城区,3712 +371203,钢城区,3712 +3713,临沂市,37 +371302,兰山区,3713 +371311,罗庄区,3713 +371312,河东区,3713 +371321,沂南县,3713 +371322,郯城县,3713 +371323,沂水县,3713 +371324,兰陵县,3713 +371325,费县,3713 +371326,平邑县,3713 +371327,莒南县,3713 +371328,蒙阴县,3713 +371329,临沭县,3713 +3714,德州市,37 +371402,德城区,3714 +371403,陵城区,3714 +371422,宁津县,3714 +371423,庆云县,3714 +371424,临邑县,3714 +371425,齐河县,3714 +371426,平原县,3714 +371427,夏津县,3714 +371428,武城县,3714 +371481,乐陵市,3714 +371482,禹城市,3714 +3715,聊城市,37 +371502,东昌府区,3715 +371521,阳谷县,3715 +371522,莘县,3715 +371523,茌平县,3715 +371524,东阿县,3715 +371525,冠县,3715 +371526,高唐县,3715 +371581,临清市,3715 +3716,滨州市,37 +371602,滨城区,3716 +371603,沾化区,3716 +371621,惠民县,3716 +371622,阳信县,3716 +371623,无棣县,3716 +371625,博兴县,3716 +371681,邹平市,3716 +3717,菏泽市,37 +371702,牡丹区,3717 +371703,定陶区,3717 +371721,曹县,3717 +371722,单县,3717 +371723,成武县,3717 +371724,巨野县,3717 +371725,郓城县,3717 +371726,鄄城县,3717 +371728,东明县,3717 +41,河南省, +4101,郑州市,41 +410102,中原区,4101 +410103,二七区,4101 +410104,管城回族区,4101 +410105,金水区,4101 +410106,上街区,4101 +410108,惠济区,4101 +410122,中牟县,4101 +410181,巩义市,4101 +410182,荥阳市,4101 +410183,新密市,4101 +410184,新郑市,4101 +410185,登封市,4101 +4102,开封市,41 +410202,龙亭区,4102 +410203,顺河回族区,4102 +410204,鼓楼区,4102 +410205,禹王台区,4102 +410212,祥符区,4102 +410221,杞县,4102 +410222,通许县,4102 +410223,尉氏县,4102 +410225,兰考县,4102 +4103,洛阳市,41 +410302,老城区,4103 +410303,西工区,4103 +410304,瀍河回族区,4103 +410305,涧西区,4103 +410306,吉利区,4103 +410311,洛龙区,4103 +410322,孟津县,4103 +410323,新安县,4103 +410324,栾川县,4103 +410325,嵩县,4103 +410326,汝阳县,4103 +410327,宜阳县,4103 +410328,洛宁县,4103 +410329,伊川县,4103 +410381,偃师市,4103 +4104,平顶山市,41 +410402,新华区,4104 +410403,卫东区,4104 +410404,石龙区,4104 +410411,湛河区,4104 +410421,宝丰县,4104 +410422,叶县,4104 +410423,鲁山县,4104 +410425,郏县,4104 +410481,舞钢市,4104 +410482,汝州市,4104 +4105,安阳市,41 +410502,文峰区,4105 +410503,北关区,4105 +410505,殷都区,4105 +410506,龙安区,4105 +410522,安阳县,4105 +410523,汤阴县,4105 +410526,滑县,4105 +410527,内黄县,4105 +410581,林州市,4105 +4106,鹤壁市,41 +410602,鹤山区,4106 +410603,山城区,4106 +410611,淇滨区,4106 +410621,浚县,4106 +410622,淇县,4106 +4107,新乡市,41 +410702,红旗区,4107 +410703,卫滨区,4107 +410704,凤泉区,4107 +410711,牧野区,4107 +410721,新乡县,4107 +410724,获嘉县,4107 +410725,原阳县,4107 +410726,延津县,4107 +410727,封丘县,4107 +410728,长垣县,4107 +410781,卫辉市,4107 +410782,辉县市,4107 +4108,焦作市,41 +410802,解放区,4108 +410803,中站区,4108 +410804,马村区,4108 +410811,山阳区,4108 +410821,修武县,4108 +410822,博爱县,4108 +410823,武陟县,4108 +410825,温县,4108 +410882,沁阳市,4108 +410883,孟州市,4108 +4109,濮阳市,41 +410902,华龙区,4109 +410922,清丰县,4109 +410923,南乐县,4109 +410926,范县,4109 +410927,台前县,4109 +410928,濮阳县,4109 +4110,许昌市,41 +411002,魏都区,4110 +411003,建安区,4110 +411024,鄢陵县,4110 +411025,襄城县,4110 +411081,禹州市,4110 +411082,长葛市,4110 +4111,漯河市,41 +411102,源汇区,4111 +411103,郾城区,4111 +411104,召陵区,4111 +411121,舞阳县,4111 +411122,临颍县,4111 +4112,三门峡市,41 +411202,湖滨区,4112 +411203,陕州区,4112 +411221,渑池县,4112 +411224,卢氏县,4112 +411281,义马市,4112 +411282,灵宝市,4112 +4113,南阳市,41 +411302,宛城区,4113 +411303,卧龙区,4113 +411321,南召县,4113 +411322,方城县,4113 +411323,西峡县,4113 +411324,镇平县,4113 +411325,内乡县,4113 +411326,淅川县,4113 +411327,社旗县,4113 +411328,唐河县,4113 +411329,新野县,4113 +411330,桐柏县,4113 +411381,邓州市,4113 +4114,商丘市,41 +411402,梁园区,4114 +411403,睢阳区,4114 +411421,民权县,4114 +411422,睢县,4114 +411423,宁陵县,4114 +411424,柘城县,4114 +411425,虞城县,4114 +411426,夏邑县,4114 +411481,永城市,4114 +4115,信阳市,41 +411502,浉河区,4115 +411503,平桥区,4115 +411521,罗山县,4115 +411522,光山县,4115 +411523,新县,4115 +411524,商城县,4115 +411525,固始县,4115 +411526,潢川县,4115 +411527,淮滨县,4115 +411528,息县,4115 +4116,周口市,41 +411602,川汇区,4116 +411621,扶沟县,4116 +411622,西华县,4116 +411623,商水县,4116 +411624,沈丘县,4116 +411625,郸城县,4116 +411626,淮阳县,4116 +411627,太康县,4116 +411628,鹿邑县,4116 +411681,项城市,4116 +4117,驻马店市,41 +411702,驿城区,4117 +411721,西平县,4117 +411722,上蔡县,4117 +411723,平舆县,4117 +411724,正阳县,4117 +411725,确山县,4117 +411726,泌阳县,4117 +411727,汝南县,4117 +411728,遂平县,4117 +411729,新蔡县,4117 +419001,济源市,41 +42,湖北省, +4201,武汉市,42 +420102,江岸区,4201 +420103,江汉区,4201 +420104,硚口区,4201 +420105,汉阳区,4201 +420106,武昌区,4201 +420107,青山区,4201 +420111,洪山区,4201 +420112,东西湖区,4201 +420113,汉南区,4201 +420114,蔡甸区,4201 +420115,江夏区,4201 +420116,黄陂区,4201 +420117,新洲区,4201 +4202,黄石市,42 +420202,黄石港区,4202 +420203,西塞山区,4202 +420204,下陆区,4202 +420205,铁山区,4202 +420222,阳新县,4202 +420281,大冶市,4202 +4203,十堰市,42 +420302,茅箭区,4203 +420303,张湾区,4203 +420304,郧阳区,4203 +420322,郧西县,4203 +420323,竹山县,4203 +420324,竹溪县,4203 +420325,房县,4203 +420381,丹江口市,4203 +4205,宜昌市,42 +420502,西陵区,4205 +420503,伍家岗区,4205 +420504,点军区,4205 +420505,猇亭区,4205 +420506,夷陵区,4205 +420525,远安县,4205 +420526,兴山县,4205 +420527,秭归县,4205 +420528,长阳土家族自治县,4205 +420529,五峰土家族自治县,4205 +420581,宜都市,4205 +420582,当阳市,4205 +420583,枝江市,4205 +4206,襄阳市,42 +420602,襄城区,4206 +420606,樊城区,4206 +420607,襄州区,4206 +420624,南漳县,4206 +420625,谷城县,4206 +420626,保康县,4206 +420682,老河口市,4206 +420683,枣阳市,4206 +420684,宜城市,4206 +4207,鄂州市,42 +420702,梁子湖区,4207 +420703,华容区,4207 +420704,鄂城区,4207 +4208,荆门市,42 +420802,东宝区,4208 +420804,掇刀区,4208 +420822,沙洋县,4208 +420881,钟祥市,4208 +420882,京山市,4208 +4209,孝感市,42 +420902,孝南区,4209 +420921,孝昌县,4209 +420922,大悟县,4209 +420923,云梦县,4209 +420981,应城市,4209 +420982,安陆市,4209 +420984,汉川市,4209 +4210,荆州市,42 +421002,沙市区,4210 +421003,荆州区,4210 +421022,公安县,4210 +421023,监利县,4210 +421024,江陵县,4210 +421081,石首市,4210 +421083,洪湖市,4210 +421087,松滋市,4210 +4211,黄冈市,42 +421102,黄州区,4211 +421121,团风县,4211 +421122,红安县,4211 +421123,罗田县,4211 +421124,英山县,4211 +421125,浠水县,4211 +421126,蕲春县,4211 +421127,黄梅县,4211 +421181,麻城市,4211 +421182,武穴市,4211 +4212,咸宁市,42 +421202,咸安区,4212 +421221,嘉鱼县,4212 +421222,通城县,4212 +421223,崇阳县,4212 +421224,通山县,4212 +421281,赤壁市,4212 +4213,随州市,42 +421303,曾都区,4213 +421321,随县,4213 +421381,广水市,4213 +4228,恩施土家族苗族自治州,42 +422801,恩施市,4228 +422802,利川市,4228 +422822,建始县,4228 +422823,巴东县,4228 +422825,宣恩县,4228 +422826,咸丰县,4228 +422827,来凤县,4228 +422828,鹤峰县,4228 +429004,仙桃市,42 +429005,潜江市,42 +429006,天门市,42 +429021,神农架林区,42 +43,湖南省, +4301,长沙市,43 +430102,芙蓉区,4301 +430103,天心区,4301 +430104,岳麓区,4301 +430105,开福区,4301 +430111,雨花区,4301 +430112,望城区,4301 +430121,长沙县,4301 +430181,浏阳市,4301 +430182,宁乡市,4301 +4302,株洲市,43 +430202,荷塘区,4302 +430203,芦淞区,4302 +430204,石峰区,4302 +430211,天元区,4302 +430212,渌口区,4302 +430223,攸县,4302 +430224,茶陵县,4302 +430225,炎陵县,4302 +430281,醴陵市,4302 +4303,湘潭市,43 +430302,雨湖区,4303 +430304,岳塘区,4303 +430321,湘潭县,4303 +430381,湘乡市,4303 +430382,韶山市,4303 +4304,衡阳市,43 +430405,珠晖区,4304 +430406,雁峰区,4304 +430407,石鼓区,4304 +430408,蒸湘区,4304 +430412,南岳区,4304 +430421,衡阳县,4304 +430422,衡南县,4304 +430423,衡山县,4304 +430424,衡东县,4304 +430426,祁东县,4304 +430481,耒阳市,4304 +430482,常宁市,4304 +4305,邵阳市,43 +430502,双清区,4305 +430503,大祥区,4305 +430511,北塔区,4305 +430521,邵东县,4305 +430522,新邵县,4305 +430523,邵阳县,4305 +430524,隆回县,4305 +430525,洞口县,4305 +430527,绥宁县,4305 +430528,新宁县,4305 +430529,城步苗族自治县,4305 +430581,武冈市,4305 +4306,岳阳市,43 +430602,岳阳楼区,4306 +430603,云溪区,4306 +430611,君山区,4306 +430621,岳阳县,4306 +430623,华容县,4306 +430624,湘阴县,4306 +430626,平江县,4306 +430681,汨罗市,4306 +430682,临湘市,4306 +4307,常德市,43 +430702,武陵区,4307 +430703,鼎城区,4307 +430721,安乡县,4307 +430722,汉寿县,4307 +430723,澧县,4307 +430724,临澧县,4307 +430725,桃源县,4307 +430726,石门县,4307 +430781,津市市,4307 +4308,张家界市,43 +430802,永定区,4308 +430811,武陵源区,4308 +430821,慈利县,4308 +430822,桑植县,4308 +4309,益阳市,43 +430902,资阳区,4309 +430903,赫山区,4309 +430921,南县,4309 +430922,桃江县,4309 +430923,安化县,4309 +430981,沅江市,4309 +4310,郴州市,43 +431002,北湖区,4310 +431003,苏仙区,4310 +431021,桂阳县,4310 +431022,宜章县,4310 +431023,永兴县,4310 +431024,嘉禾县,4310 +431025,临武县,4310 +431026,汝城县,4310 +431027,桂东县,4310 +431028,安仁县,4310 +431081,资兴市,4310 +4311,永州市,43 +431102,零陵区,4311 +431103,冷水滩区,4311 +431121,祁阳县,4311 +431122,东安县,4311 +431123,双牌县,4311 +431124,道县,4311 +431125,江永县,4311 +431126,宁远县,4311 +431127,蓝山县,4311 +431128,新田县,4311 +431129,江华瑶族自治县,4311 +4312,怀化市,43 +431202,鹤城区,4312 +431221,中方县,4312 +431222,沅陵县,4312 +431223,辰溪县,4312 +431224,溆浦县,4312 +431225,会同县,4312 +431226,麻阳苗族自治县,4312 +431227,新晃侗族自治县,4312 +431228,芷江侗族自治县,4312 +431229,靖州苗族侗族自治县,4312 +431230,通道侗族自治县,4312 +431281,洪江市,4312 +4313,娄底市,43 +431302,娄星区,4313 +431321,双峰县,4313 +431322,新化县,4313 +431381,冷水江市,4313 +431382,涟源市,4313 +4331,湘西土家族苗族自治州,43 +433101,吉首市,4331 +433122,泸溪县,4331 +433123,凤凰县,4331 +433124,花垣县,4331 +433125,保靖县,4331 +433126,古丈县,4331 +433127,永顺县,4331 +433130,龙山县,4331 +44,广东省, +4401,广州市,44 +440103,荔湾区,4401 +440104,越秀区,4401 +440105,海珠区,4401 +440106,天河区,4401 +440111,白云区,4401 +440112,黄埔区,4401 +440113,番禺区,4401 +440114,花都区,4401 +440115,南沙区,4401 +440117,从化区,4401 +440118,增城区,4401 +4402,韶关市,44 +440203,武江区,4402 +440204,浈江区,4402 +440205,曲江区,4402 +440222,始兴县,4402 +440224,仁化县,4402 +440229,翁源县,4402 +440232,乳源瑶族自治县,4402 +440233,新丰县,4402 +440281,乐昌市,4402 +440282,南雄市,4402 +4403,深圳市,44 +440303,罗湖区,4403 +440304,福田区,4403 +440305,南山区,4403 +440306,宝安区,4403 +440307,龙岗区,4403 +440308,盐田区,4403 +440309,龙华区,4403 +440310,坪山区,4403 +440311,光明区,4403 +4404,珠海市,44 +440402,香洲区,4404 +440403,斗门区,4404 +440404,金湾区,4404 +4405,汕头市,44 +440507,龙湖区,4405 +440511,金平区,4405 +440512,濠江区,4405 +440513,潮阳区,4405 +440514,潮南区,4405 +440515,澄海区,4405 +440523,南澳县,4405 +4406,佛山市,44 +440604,禅城区,4406 +440605,南海区,4406 +440606,顺德区,4406 +440607,三水区,4406 +440608,高明区,4406 +4407,江门市,44 +440703,蓬江区,4407 +440704,江海区,4407 +440705,新会区,4407 +440781,台山市,4407 +440783,开平市,4407 +440784,鹤山市,4407 +440785,恩平市,4407 +4408,湛江市,44 +440802,赤坎区,4408 +440803,霞山区,4408 +440804,坡头区,4408 +440811,麻章区,4408 +440823,遂溪县,4408 +440825,徐闻县,4408 +440881,廉江市,4408 +440882,雷州市,4408 +440883,吴川市,4408 +4409,茂名市,44 +440902,茂南区,4409 +440904,电白区,4409 +440981,高州市,4409 +440982,化州市,4409 +440983,信宜市,4409 +4412,肇庆市,44 +441202,端州区,4412 +441203,鼎湖区,4412 +441204,高要区,4412 +441223,广宁县,4412 +441224,怀集县,4412 +441225,封开县,4412 +441226,德庆县,4412 +441284,四会市,4412 +4413,惠州市,44 +441302,惠城区,4413 +441303,惠阳区,4413 +441322,博罗县,4413 +441323,惠东县,4413 +441324,龙门县,4413 +4414,梅州市,44 +441402,梅江区,4414 +441403,梅县区,4414 +441422,大埔县,4414 +441423,丰顺县,4414 +441424,五华县,4414 +441426,平远县,4414 +441427,蕉岭县,4414 +441481,兴宁市,4414 +4415,汕尾市,44 +441502,城区,4415 +441521,海丰县,4415 +441523,陆河县,4415 +441581,陆丰市,4415 +4416,河源市,44 +441602,源城区,4416 +441621,紫金县,4416 +441622,龙川县,4416 +441623,连平县,4416 +441624,和平县,4416 +441625,东源县,4416 +4417,阳江市,44 +441702,江城区,4417 +441704,阳东区,4417 +441721,阳西县,4417 +441781,阳春市,4417 +4418,清远市,44 +441802,清城区,4418 +441803,清新区,4418 +441821,佛冈县,4418 +441823,阳山县,4418 +441825,连山壮族瑶族自治县,4418 +441826,连南瑶族自治县,4418 +441881,英德市,4418 +441882,连州市,4418 +4419,东莞市,44 +4420,中山市,44 +4451,潮州市,44 +445102,湘桥区,4451 +445103,潮安区,4451 +445122,饶平县,4451 +4452,揭阳市,44 +445202,榕城区,4452 +445203,揭东区,4452 +445222,揭西县,4452 +445224,惠来县,4452 +445281,普宁市,4452 +4453,云浮市,44 +445302,云城区,4453 +445303,云安区,4453 +445321,新兴县,4453 +445322,郁南县,4453 +445381,罗定市,4453 +45,广西壮族自治区, +4501,南宁市,45 +450102,兴宁区,4501 +450103,青秀区,4501 +450105,江南区,4501 +450107,西乡塘区,4501 +450108,良庆区,4501 +450109,邕宁区,4501 +450110,武鸣区,4501 +450123,隆安县,4501 +450124,马山县,4501 +450125,上林县,4501 +450126,宾阳县,4501 +450127,横县,4501 +4502,柳州市,45 +450202,城中区,4502 +450203,鱼峰区,4502 +450204,柳南区,4502 +450205,柳北区,4502 +450206,柳江区,4502 +450222,柳城县,4502 +450223,鹿寨县,4502 +450224,融安县,4502 +450225,融水苗族自治县,4502 +450226,三江侗族自治县,4502 +4503,桂林市,45 +450302,秀峰区,4503 +450303,叠彩区,4503 +450304,象山区,4503 +450305,七星区,4503 +450311,雁山区,4503 +450312,临桂区,4503 +450321,阳朔县,4503 +450323,灵川县,4503 +450324,全州县,4503 +450325,兴安县,4503 +450326,永福县,4503 +450327,灌阳县,4503 +450328,龙胜各族自治县,4503 +450329,资源县,4503 +450330,平乐县,4503 +450332,恭城瑶族自治县,4503 +450381,荔浦市,4503 +4504,梧州市,45 +450403,万秀区,4504 +450405,长洲区,4504 +450406,龙圩区,4504 +450421,苍梧县,4504 +450422,藤县,4504 +450423,蒙山县,4504 +450481,岑溪市,4504 +4505,北海市,45 +450502,海城区,4505 +450503,银海区,4505 +450512,铁山港区,4505 +450521,合浦县,4505 +4506,防城港市,45 +450602,港口区,4506 +450603,防城区,4506 +450621,上思县,4506 +450681,东兴市,4506 +4507,钦州市,45 +450702,钦南区,4507 +450703,钦北区,4507 +450721,灵山县,4507 +450722,浦北县,4507 +4508,贵港市,45 +450802,港北区,4508 +450803,港南区,4508 +450804,覃塘区,4508 +450821,平南县,4508 +450881,桂平市,4508 +4509,玉林市,45 +450902,玉州区,4509 +450903,福绵区,4509 +450921,容县,4509 +450922,陆川县,4509 +450923,博白县,4509 +450924,兴业县,4509 +450981,北流市,4509 +4510,百色市,45 +451002,右江区,4510 +451021,田阳县,4510 +451022,田东县,4510 +451023,平果县,4510 +451024,德保县,4510 +451026,那坡县,4510 +451027,凌云县,4510 +451028,乐业县,4510 +451029,田林县,4510 +451030,西林县,4510 +451031,隆林各族自治县,4510 +451081,靖西市,4510 +4511,贺州市,45 +451102,八步区,4511 +451103,平桂区,4511 +451121,昭平县,4511 +451122,钟山县,4511 +451123,富川瑶族自治县,4511 +4512,河池市,45 +451202,金城江区,4512 +451203,宜州区,4512 +451221,南丹县,4512 +451222,天峨县,4512 +451223,凤山县,4512 +451224,东兰县,4512 +451225,罗城仫佬族自治县,4512 +451226,环江毛南族自治县,4512 +451227,巴马瑶族自治县,4512 +451228,都安瑶族自治县,4512 +451229,大化瑶族自治县,4512 +4513,来宾市,45 +451302,兴宾区,4513 +451321,忻城县,4513 +451322,象州县,4513 +451323,武宣县,4513 +451324,金秀瑶族自治县,4513 +451381,合山市,4513 +4514,崇左市,45 +451402,江州区,4514 +451421,扶绥县,4514 +451422,宁明县,4514 +451423,龙州县,4514 +451424,大新县,4514 +451425,天等县,4514 +451481,凭祥市,4514 +46,海南省, +4601,海口市,46 +460105,秀英区,4601 +460106,龙华区,4601 +460107,琼山区,4601 +460108,美兰区,4601 +4602,三亚市,46 +460202,海棠区,4602 +460203,吉阳区,4602 +460204,天涯区,4602 +460205,崖州区,4602 +4603,三沙市,46 +4604,儋州市,46 +469001,五指山市,46 +469002,琼海市,46 +469005,文昌市,46 +469006,万宁市,46 +469007,东方市,46 +469021,定安县,46 +469022,屯昌县,46 +469023,澄迈县,46 +469024,临高县,46 +469025,白沙黎族自治县,46 +469026,昌江黎族自治县,46 +469027,乐东黎族自治县,46 +469028,陵水黎族自治县,46 +469029,保亭黎族苗族自治县,46 +469030,琼中黎族苗族自治县,46 +50,重庆市, +5001,市辖区,50 +500101,万州区,5001 +500102,涪陵区,5001 +500103,渝中区,5001 +500104,大渡口区,5001 +500105,江北区,5001 +500106,沙坪坝区,5001 +500107,九龙坡区,5001 +500108,南岸区,5001 +500109,北碚区,5001 +500110,綦江区,5001 +500111,大足区,5001 +500112,渝北区,5001 +500113,巴南区,5001 +500114,黔江区,5001 +500115,长寿区,5001 +500116,江津区,5001 +500117,合川区,5001 +500118,永川区,5001 +500119,南川区,5001 +500120,璧山区,5001 +500151,铜梁区,5001 +500152,潼南区,5001 +500153,荣昌区,5001 +500154,开州区,5001 +500155,梁平区,5001 +500156,武隆区,5001 +500229,城口县,5001 +500230,丰都县,5001 +500231,垫江县,5001 +500233,忠县,5001 +500235,云阳县,5001 +500236,奉节县,5001 +500237,巫山县,5001 +500238,巫溪县,5001 +500240,石柱土家族自治县,5001 +500241,秀山土家族苗族自治县,5001 +500242,酉阳土家族苗族自治县,5001 +500243,彭水苗族土家族自治县,5001 +51,四川省, +5101,成都市,51 +510104,锦江区,5101 +510105,青羊区,5101 +510106,金牛区,5101 +510107,武侯区,5101 +510108,成华区,5101 +510112,龙泉驿区,5101 +510113,青白江区,5101 +510114,新都区,5101 +510115,温江区,5101 +510116,双流区,5101 +510117,郫都区,5101 +510121,金堂县,5101 +510129,大邑县,5101 +510131,蒲江县,5101 +510132,新津县,5101 +510181,都江堰市,5101 +510182,彭州市,5101 +510183,邛崃市,5101 +510184,崇州市,5101 +510185,简阳市,5101 +5103,自贡市,51 +510302,自流井区,5103 +510303,贡井区,5103 +510304,大安区,5103 +510311,沿滩区,5103 +510321,荣县,5103 +510322,富顺县,5103 +5104,攀枝花市,51 +510402,东区,5104 +510403,西区,5104 +510411,仁和区,5104 +510421,米易县,5104 +510422,盐边县,5104 +5105,泸州市,51 +510502,江阳区,5105 +510503,纳溪区,5105 +510504,龙马潭区,5105 +510521,泸县,5105 +510522,合江县,5105 +510524,叙永县,5105 +510525,古蔺县,5105 +5106,德阳市,51 +510603,旌阳区,5106 +510604,罗江区,5106 +510623,中江县,5106 +510681,广汉市,5106 +510682,什邡市,5106 +510683,绵竹市,5106 +5107,绵阳市,51 +510703,涪城区,5107 +510704,游仙区,5107 +510705,安州区,5107 +510722,三台县,5107 +510723,盐亭县,5107 +510725,梓潼县,5107 +510726,北川羌族自治县,5107 +510727,平武县,5107 +510781,江油市,5107 +5108,广元市,51 +510802,利州区,5108 +510811,昭化区,5108 +510812,朝天区,5108 +510821,旺苍县,5108 +510822,青川县,5108 +510823,剑阁县,5108 +510824,苍溪县,5108 +5109,遂宁市,51 +510903,船山区,5109 +510904,安居区,5109 +510921,蓬溪县,5109 +510922,射洪县,5109 +510923,大英县,5109 +5110,内江市,51 +511002,市中区,5110 +511011,东兴区,5110 +511024,威远县,5110 +511025,资中县,5110 +511083,隆昌市,5110 +5111,乐山市,51 +511102,市中区,5111 +511111,沙湾区,5111 +511112,五通桥区,5111 +511113,金口河区,5111 +511123,犍为县,5111 +511124,井研县,5111 +511126,夹江县,5111 +511129,沐川县,5111 +511132,峨边彝族自治县,5111 +511133,马边彝族自治县,5111 +511181,峨眉山市,5111 +5113,南充市,51 +511302,顺庆区,5113 +511303,高坪区,5113 +511304,嘉陵区,5113 +511321,南部县,5113 +511322,营山县,5113 +511323,蓬安县,5113 +511324,仪陇县,5113 +511325,西充县,5113 +511381,阆中市,5113 +5114,眉山市,51 +511402,东坡区,5114 +511403,彭山区,5114 +511421,仁寿县,5114 +511423,洪雅县,5114 +511424,丹棱县,5114 +511425,青神县,5114 +5115,宜宾市,51 +511502,翠屏区,5115 +511503,南溪区,5115 +511504,叙州区,5115 +511523,江安县,5115 +511524,长宁县,5115 +511525,高县,5115 +511526,珙县,5115 +511527,筠连县,5115 +511528,兴文县,5115 +511529,屏山县,5115 +5116,广安市,51 +511602,广安区,5116 +511603,前锋区,5116 +511621,岳池县,5116 +511622,武胜县,5116 +511623,邻水县,5116 +511681,华蓥市,5116 +5117,达州市,51 +511702,通川区,5117 +511703,达川区,5117 +511722,宣汉县,5117 +511723,开江县,5117 +511724,大竹县,5117 +511725,渠县,5117 +511781,万源市,5117 +5118,雅安市,51 +511802,雨城区,5118 +511803,名山区,5118 +511822,荥经县,5118 +511823,汉源县,5118 +511824,石棉县,5118 +511825,天全县,5118 +511826,芦山县,5118 +511827,宝兴县,5118 +5119,巴中市,51 +511902,巴州区,5119 +511903,恩阳区,5119 +511921,通江县,5119 +511922,南江县,5119 +511923,平昌县,5119 +5120,资阳市,51 +512002,雁江区,5120 +512021,安岳县,5120 +512022,乐至县,5120 +5132,阿坝藏族羌族自治州,51 +513201,马尔康市,5132 +513221,汶川县,5132 +513222,理县,5132 +513223,茂县,5132 +513224,松潘县,5132 +513225,九寨沟县,5132 +513226,金川县,5132 +513227,小金县,5132 +513228,黑水县,5132 +513230,壤塘县,5132 +513231,阿坝县,5132 +513232,若尔盖县,5132 +513233,红原县,5132 +5133,甘孜藏族自治州,51 +513301,康定市,5133 +513322,泸定县,5133 +513323,丹巴县,5133 +513324,九龙县,5133 +513325,雅江县,5133 +513326,道孚县,5133 +513327,炉霍县,5133 +513328,甘孜县,5133 +513329,新龙县,5133 +513330,德格县,5133 +513331,白玉县,5133 +513332,石渠县,5133 +513333,色达县,5133 +513334,理塘县,5133 +513335,巴塘县,5133 +513336,乡城县,5133 +513337,稻城县,5133 +513338,得荣县,5133 +5134,凉山彝族自治州,51 +513401,西昌市,5134 +513422,木里藏族自治县,5134 +513423,盐源县,5134 +513424,德昌县,5134 +513425,会理县,5134 +513426,会东县,5134 +513427,宁南县,5134 +513428,普格县,5134 +513429,布拖县,5134 +513430,金阳县,5134 +513431,昭觉县,5134 +513432,喜德县,5134 +513433,冕宁县,5134 +513434,越西县,5134 +513435,甘洛县,5134 +513436,美姑县,5134 +513437,雷波县,5134 +52,贵州省, +5201,贵阳市,52 +520102,南明区,5201 +520103,云岩区,5201 +520111,花溪区,5201 +520112,乌当区,5201 +520113,白云区,5201 +520115,观山湖区,5201 +520121,开阳县,5201 +520122,息烽县,5201 +520123,修文县,5201 +520181,清镇市,5201 +5202,六盘水市,52 +520201,钟山区,5202 +520203,六枝特区,5202 +520221,水城县,5202 +520281,盘州市,5202 +5203,遵义市,52 +520302,红花岗区,5203 +520303,汇川区,5203 +520304,播州区,5203 +520322,桐梓县,5203 +520323,绥阳县,5203 +520324,正安县,5203 +520325,道真仡佬族苗族自治县,5203 +520326,务川仡佬族苗族自治县,5203 +520327,凤冈县,5203 +520328,湄潭县,5203 +520329,余庆县,5203 +520330,习水县,5203 +520381,赤水市,5203 +520382,仁怀市,5203 +5204,安顺市,52 +520402,西秀区,5204 +520403,平坝区,5204 +520422,普定县,5204 +520423,镇宁布依族苗族自治县,5204 +520424,关岭布依族苗族自治县,5204 +520425,紫云苗族布依族自治县,5204 +5205,毕节市,52 +520502,七星关区,5205 +520521,大方县,5205 +520522,黔西县,5205 +520523,金沙县,5205 +520524,织金县,5205 +520525,纳雍县,5205 +520526,威宁彝族回族苗族自治县,5205 +520527,赫章县,5205 +5206,铜仁市,52 +520602,碧江区,5206 +520603,万山区,5206 +520621,江口县,5206 +520622,玉屏侗族自治县,5206 +520623,石阡县,5206 +520624,思南县,5206 +520625,印江土家族苗族自治县,5206 +520626,德江县,5206 +520627,沿河土家族自治县,5206 +520628,松桃苗族自治县,5206 +5223,黔西南布依族苗族自治州,52 +522301,兴义市,5223 +522302,兴仁市,5223 +522323,普安县,5223 +522324,晴隆县,5223 +522325,贞丰县,5223 +522326,望谟县,5223 +522327,册亨县,5223 +522328,安龙县,5223 +5226,黔东南苗族侗族自治州,52 +522601,凯里市,5226 +522622,黄平县,5226 +522623,施秉县,5226 +522624,三穗县,5226 +522625,镇远县,5226 +522626,岑巩县,5226 +522627,天柱县,5226 +522628,锦屏县,5226 +522629,剑河县,5226 +522630,台江县,5226 +522631,黎平县,5226 +522632,榕江县,5226 +522633,从江县,5226 +522634,雷山县,5226 +522635,麻江县,5226 +522636,丹寨县,5226 +5227,黔南布依族苗族自治州,52 +522701,都匀市,5227 +522702,福泉市,5227 +522722,荔波县,5227 +522723,贵定县,5227 +522725,瓮安县,5227 +522726,独山县,5227 +522727,平塘县,5227 +522728,罗甸县,5227 +522729,长顺县,5227 +522730,龙里县,5227 +522731,惠水县,5227 +522732,三都水族自治县,5227 +53,云南省, +5301,昆明市,53 +530102,五华区,5301 +530103,盘龙区,5301 +530111,官渡区,5301 +530112,西山区,5301 +530113,东川区,5301 +530114,呈贡区,5301 +530115,晋宁区,5301 +530124,富民县,5301 +530125,宜良县,5301 +530126,石林彝族自治县,5301 +530127,嵩明县,5301 +530128,禄劝彝族苗族自治县,5301 +530129,寻甸回族彝族自治县,5301 +530181,安宁市,5301 +5303,曲靖市,53 +530302,麒麟区,5303 +530303,沾益区,5303 +530304,马龙区,5303 +530322,陆良县,5303 +530323,师宗县,5303 +530324,罗平县,5303 +530325,富源县,5303 +530326,会泽县,5303 +530381,宣威市,5303 +5304,玉溪市,53 +530402,红塔区,5304 +530403,江川区,5304 +530422,澄江县,5304 +530423,通海县,5304 +530424,华宁县,5304 +530425,易门县,5304 +530426,峨山彝族自治县,5304 +530427,新平彝族傣族自治县,5304 +530428,元江哈尼族彝族傣族自治县,5304 +5305,保山市,53 +530502,隆阳区,5305 +530521,施甸县,5305 +530523,龙陵县,5305 +530524,昌宁县,5305 +530581,腾冲市,5305 +5306,昭通市,53 +530602,昭阳区,5306 +530621,鲁甸县,5306 +530622,巧家县,5306 +530623,盐津县,5306 +530624,大关县,5306 +530625,永善县,5306 +530626,绥江县,5306 +530627,镇雄县,5306 +530628,彝良县,5306 +530629,威信县,5306 +530681,水富市,5306 +5307,丽江市,53 +530702,古城区,5307 +530721,玉龙纳西族自治县,5307 +530722,永胜县,5307 +530723,华坪县,5307 +530724,宁蒗彝族自治县,5307 +5308,普洱市,53 +530802,思茅区,5308 +530821,宁洱哈尼族彝族自治县,5308 +530822,墨江哈尼族自治县,5308 +530823,景东彝族自治县,5308 +530824,景谷傣族彝族自治县,5308 +530825,镇沅彝族哈尼族拉祜族自治县,5308 +530826,江城哈尼族彝族自治县,5308 +530827,孟连傣族拉祜族佤族自治县,5308 +530828,澜沧拉祜族自治县,5308 +530829,西盟佤族自治县,5308 +5309,临沧市,53 +530902,临翔区,5309 +530921,凤庆县,5309 +530922,云县,5309 +530923,永德县,5309 +530924,镇康县,5309 +530925,双江拉祜族佤族布朗族傣族自治县,5309 +530926,耿马傣族佤族自治县,5309 +530927,沧源佤族自治县,5309 +5323,楚雄彝族自治州,53 +532301,楚雄市,5323 +532322,双柏县,5323 +532323,牟定县,5323 +532324,南华县,5323 +532325,姚安县,5323 +532326,大姚县,5323 +532327,永仁县,5323 +532328,元谋县,5323 +532329,武定县,5323 +532331,禄丰县,5323 +5325,红河哈尼族彝族自治州,53 +532501,个旧市,5325 +532502,开远市,5325 +532503,蒙自市,5325 +532504,弥勒市,5325 +532523,屏边苗族自治县,5325 +532524,建水县,5325 +532525,石屏县,5325 +532527,泸西县,5325 +532528,元阳县,5325 +532529,红河县,5325 +532530,金平苗族瑶族傣族自治县,5325 +532531,绿春县,5325 +532532,河口瑶族自治县,5325 +5326,文山壮族苗族自治州,53 +532601,文山市,5326 +532622,砚山县,5326 +532623,西畴县,5326 +532624,麻栗坡县,5326 +532625,马关县,5326 +532626,丘北县,5326 +532627,广南县,5326 +532628,富宁县,5326 +5328,西双版纳傣族自治州,53 +532801,景洪市,5328 +532822,勐海县,5328 +532823,勐腊县,5328 +5329,大理白族自治州,53 +532901,大理市,5329 +532922,漾濞彝族自治县,5329 +532923,祥云县,5329 +532924,宾川县,5329 +532925,弥渡县,5329 +532926,南涧彝族自治县,5329 +532927,巍山彝族回族自治县,5329 +532928,永平县,5329 +532929,云龙县,5329 +532930,洱源县,5329 +532931,剑川县,5329 +532932,鹤庆县,5329 +5331,德宏傣族景颇族自治州,53 +533102,瑞丽市,5331 +533103,芒市,5331 +533122,梁河县,5331 +533123,盈江县,5331 +533124,陇川县,5331 +5333,怒江傈僳族自治州,53 +533301,泸水市,5333 +533323,福贡县,5333 +533324,贡山独龙族怒族自治县,5333 +533325,兰坪白族普米族自治县,5333 +5334,迪庆藏族自治州,53 +533401,香格里拉市,5334 +533422,德钦县,5334 +533423,维西傈僳族自治县,5334 +54,西藏自治区, +5401,拉萨市,54 +540102,城关区,5401 +540103,堆龙德庆区,5401 +540104,达孜区,5401 +540121,林周县,5401 +540122,当雄县,5401 +540123,尼木县,5401 +540124,曲水县,5401 +540127,墨竹工卡县,5401 +5402,日喀则市,54 +540202,桑珠孜区,5402 +540221,南木林县,5402 +540222,江孜县,5402 +540223,定日县,5402 +540224,萨迦县,5402 +540225,拉孜县,5402 +540226,昂仁县,5402 +540227,谢通门县,5402 +540228,白朗县,5402 +540229,仁布县,5402 +540230,康马县,5402 +540231,定结县,5402 +540232,仲巴县,5402 +540233,亚东县,5402 +540234,吉隆县,5402 +540235,聂拉木县,5402 +540236,萨嘎县,5402 +540237,岗巴县,5402 +5403,昌都市,54 +540302,卡若区,5403 +540321,江达县,5403 +540322,贡觉县,5403 +540323,类乌齐县,5403 +540324,丁青县,5403 +540325,察雅县,5403 +540326,八宿县,5403 +540327,左贡县,5403 +540328,芒康县,5403 +540329,洛隆县,5403 +540330,边坝县,5403 +5404,林芝市,54 +540402,巴宜区,5404 +540421,工布江达县,5404 +540422,米林县,5404 +540423,墨脱县,5404 +540424,波密县,5404 +540425,察隅县,5404 +540426,朗县,5404 +5405,山南市,54 +540502,乃东区,5405 +540521,扎囊县,5405 +540522,贡嘎县,5405 +540523,桑日县,5405 +540524,琼结县,5405 +540525,曲松县,5405 +540526,措美县,5405 +540527,洛扎县,5405 +540528,加查县,5405 +540529,隆子县,5405 +540530,错那县,5405 +540531,浪卡子县,5405 +5406,那曲市,54 +540602,色尼区,5406 +540621,嘉黎县,5406 +540622,比如县,5406 +540623,聂荣县,5406 +540624,安多县,5406 +540625,申扎县,5406 +540626,索县,5406 +540627,班戈县,5406 +540628,巴青县,5406 +540629,尼玛县,5406 +540630,双湖县,5406 +5425,阿里地区,54 +542521,普兰县,5425 +542522,札达县,5425 +542523,噶尔县,5425 +542524,日土县,5425 +542525,革吉县,5425 +542526,改则县,5425 +542527,措勤县,5425 +61,陕西省, +6101,西安市,61 +610102,新城区,6101 +610103,碑林区,6101 +610104,莲湖区,6101 +610111,灞桥区,6101 +610112,未央区,6101 +610113,雁塔区,6101 +610114,阎良区,6101 +610115,临潼区,6101 +610116,长安区,6101 +610117,高陵区,6101 +610118,鄠邑区,6101 +610122,蓝田县,6101 +610124,周至县,6101 +6102,铜川市,61 +610202,王益区,6102 +610203,印台区,6102 +610204,耀州区,6102 +610222,宜君县,6102 +6103,宝鸡市,61 +610302,渭滨区,6103 +610303,金台区,6103 +610304,陈仓区,6103 +610322,凤翔县,6103 +610323,岐山县,6103 +610324,扶风县,6103 +610326,眉县,6103 +610327,陇县,6103 +610328,千阳县,6103 +610329,麟游县,6103 +610330,凤县,6103 +610331,太白县,6103 +6104,咸阳市,61 +610402,秦都区,6104 +610403,杨陵区,6104 +610404,渭城区,6104 +610422,三原县,6104 +610423,泾阳县,6104 +610424,乾县,6104 +610425,礼泉县,6104 +610426,永寿县,6104 +610428,长武县,6104 +610429,旬邑县,6104 +610430,淳化县,6104 +610431,武功县,6104 +610481,兴平市,6104 +610482,彬州市,6104 +6105,渭南市,61 +610502,临渭区,6105 +610503,华州区,6105 +610522,潼关县,6105 +610523,大荔县,6105 +610524,合阳县,6105 +610525,澄城县,6105 +610526,蒲城县,6105 +610527,白水县,6105 +610528,富平县,6105 +610581,韩城市,6105 +610582,华阴市,6105 +6106,延安市,61 +610602,宝塔区,6106 +610603,安塞区,6106 +610621,延长县,6106 +610622,延川县,6106 +610623,子长县,6106 +610625,志丹县,6106 +610626,吴起县,6106 +610627,甘泉县,6106 +610628,富县,6106 +610629,洛川县,6106 +610630,宜川县,6106 +610631,黄龙县,6106 +610632,黄陵县,6106 +6107,汉中市,61 +610702,汉台区,6107 +610703,南郑区,6107 +610722,城固县,6107 +610723,洋县,6107 +610724,西乡县,6107 +610725,勉县,6107 +610726,宁强县,6107 +610727,略阳县,6107 +610728,镇巴县,6107 +610729,留坝县,6107 +610730,佛坪县,6107 +6108,榆林市,61 +610802,榆阳区,6108 +610803,横山区,6108 +610822,府谷县,6108 +610824,靖边县,6108 +610825,定边县,6108 +610826,绥德县,6108 +610827,米脂县,6108 +610828,佳县,6108 +610829,吴堡县,6108 +610830,清涧县,6108 +610831,子洲县,6108 +610881,神木市,6108 +6109,安康市,61 +610902,汉滨区,6109 +610921,汉阴县,6109 +610922,石泉县,6109 +610923,宁陕县,6109 +610924,紫阳县,6109 +610925,岚皋县,6109 +610926,平利县,6109 +610927,镇坪县,6109 +610928,旬阳县,6109 +610929,白河县,6109 +6110,商洛市,61 +611002,商州区,6110 +611021,洛南县,6110 +611022,丹凤县,6110 +611023,商南县,6110 +611024,山阳县,6110 +611025,镇安县,6110 +611026,柞水县,6110 +62,甘肃省, +6201,兰州市,62 +620102,城关区,6201 +620103,七里河区,6201 +620104,西固区,6201 +620105,安宁区,6201 +620111,红古区,6201 +620121,永登县,6201 +620122,皋兰县,6201 +620123,榆中县,6201 +6202,嘉峪关市,62 +6203,金昌市,62 +620302,金川区,6203 +620321,永昌县,6203 +6204,白银市,62 +620402,白银区,6204 +620403,平川区,6204 +620421,靖远县,6204 +620422,会宁县,6204 +620423,景泰县,6204 +6205,天水市,62 +620502,秦州区,6205 +620503,麦积区,6205 +620521,清水县,6205 +620522,秦安县,6205 +620523,甘谷县,6205 +620524,武山县,6205 +620525,张家川回族自治县,6205 +6206,武威市,62 +620602,凉州区,6206 +620621,民勤县,6206 +620622,古浪县,6206 +620623,天祝藏族自治县,6206 +6207,张掖市,62 +620702,甘州区,6207 +620721,肃南裕固族自治县,6207 +620722,民乐县,6207 +620723,临泽县,6207 +620724,高台县,6207 +620725,山丹县,6207 +6208,平凉市,62 +620802,崆峒区,6208 +620821,泾川县,6208 +620822,灵台县,6208 +620823,崇信县,6208 +620825,庄浪县,6208 +620826,静宁县,6208 +620881,华亭市,6208 +6209,酒泉市,62 +620902,肃州区,6209 +620921,金塔县,6209 +620922,瓜州县,6209 +620923,肃北蒙古族自治县,6209 +620924,阿克塞哈萨克族自治县,6209 +620981,玉门市,6209 +620982,敦煌市,6209 +6210,庆阳市,62 +621002,西峰区,6210 +621021,庆城县,6210 +621022,环县,6210 +621023,华池县,6210 +621024,合水县,6210 +621025,正宁县,6210 +621026,宁县,6210 +621027,镇原县,6210 +6211,定西市,62 +621102,安定区,6211 +621121,通渭县,6211 +621122,陇西县,6211 +621123,渭源县,6211 +621124,临洮县,6211 +621125,漳县,6211 +621126,岷县,6211 +6212,陇南市,62 +621202,武都区,6212 +621221,成县,6212 +621222,文县,6212 +621223,宕昌县,6212 +621224,康县,6212 +621225,西和县,6212 +621226,礼县,6212 +621227,徽县,6212 +621228,两当县,6212 +6229,临夏回族自治州,62 +622901,临夏市,6229 +622921,临夏县,6229 +622922,康乐县,6229 +622923,永靖县,6229 +622924,广河县,6229 +622925,和政县,6229 +622926,东乡族自治县,6229 +622927,积石山保安族东乡族撒拉族自治县,6229 +6230,甘南藏族自治州,62 +623001,合作市,6230 +623021,临潭县,6230 +623022,卓尼县,6230 +623023,舟曲县,6230 +623024,迭部县,6230 +623025,玛曲县,6230 +623026,碌曲县,6230 +623027,夏河县,6230 +63,青海省, +6301,西宁市,63 +630102,城东区,6301 +630103,城中区,6301 +630104,城西区,6301 +630105,城北区,6301 +630121,大通回族土族自治县,6301 +630122,湟中县,6301 +630123,湟源县,6301 +6302,海东市,63 +630202,乐都区,6302 +630203,平安区,6302 +630222,民和回族土族自治县,6302 +630223,互助土族自治县,6302 +630224,化隆回族自治县,6302 +630225,循化撒拉族自治县,6302 +6322,海北藏族自治州,63 +632221,门源回族自治县,6322 +632222,祁连县,6322 +632223,海晏县,6322 +632224,刚察县,6322 +6323,黄南藏族自治州,63 +632321,同仁县,6323 +632322,尖扎县,6323 +632323,泽库县,6323 +632324,河南蒙古族自治县,6323 +6325,海南藏族自治州,63 +632521,共和县,6325 +632522,同德县,6325 +632523,贵德县,6325 +632524,兴海县,6325 +632525,贵南县,6325 +6326,果洛藏族自治州,63 +632621,玛沁县,6326 +632622,班玛县,6326 +632623,甘德县,6326 +632624,达日县,6326 +632625,久治县,6326 +632626,玛多县,6326 +6327,玉树藏族自治州,63 +632701,玉树市,6327 +632722,杂多县,6327 +632723,称多县,6327 +632724,治多县,6327 +632725,囊谦县,6327 +632726,曲麻莱县,6327 +6328,海西蒙古族藏族自治州,63 +632801,格尔木市,6328 +632802,德令哈市,6328 +632803,茫崖市,6328 +632821,乌兰县,6328 +632822,都兰县,6328 +632823,天峻县,6328 +64,宁夏回族自治区, +6401,银川市,64 +640104,兴庆区,6401 +640105,西夏区,6401 +640106,金凤区,6401 +640121,永宁县,6401 +640122,贺兰县,6401 +640181,灵武市,6401 +6402,石嘴山市,64 +640202,大武口区,6402 +640205,惠农区,6402 +640221,平罗县,6402 +6403,吴忠市,64 +640302,利通区,6403 +640303,红寺堡区,6403 +640323,盐池县,6403 +640324,同心县,6403 +640381,青铜峡市,6403 +6404,固原市,64 +640402,原州区,6404 +640422,西吉县,6404 +640423,隆德县,6404 +640424,泾源县,6404 +640425,彭阳县,6404 +6405,中卫市,64 +640502,沙坡头区,6405 +640521,中宁县,6405 +640522,海原县,6405 +65,新疆维吾尔自治区, +6501,乌鲁木齐市,65 +650102,天山区,6501 +650103,沙依巴克区,6501 +650104,新市区,6501 +650105,水磨沟区,6501 +650106,头屯河区,6501 +650107,达坂城区,6501 +650109,米东区,6501 +650121,乌鲁木齐县,6501 +6502,克拉玛依市,65 +650202,独山子区,6502 +650203,克拉玛依区,6502 +650204,白碱滩区,6502 +650205,乌尔禾区,6502 +6504,吐鲁番市,65 +650402,高昌区,6504 +650421,鄯善县,6504 +650422,托克逊县,6504 +6505,哈密市,65 +650502,伊州区,6505 +650521,巴里坤哈萨克自治县,6505 +650522,伊吾县,6505 +6523,昌吉回族自治州,65 +652301,昌吉市,6523 +652302,阜康市,6523 +652323,呼图壁县,6523 +652324,玛纳斯县,6523 +652325,奇台县,6523 +652327,吉木萨尔县,6523 +652328,木垒哈萨克自治县,6523 +6527,博尔塔拉蒙古自治州,65 +652701,博乐市,6527 +652702,阿拉山口市,6527 +652722,精河县,6527 +652723,温泉县,6527 +6528,巴音郭楞蒙古自治州,65 +652801,库尔勒市,6528 +652822,轮台县,6528 +652823,尉犁县,6528 +652824,若羌县,6528 +652825,且末县,6528 +652826,焉耆回族自治县,6528 +652827,和静县,6528 +652828,和硕县,6528 +652829,博湖县,6528 +6529,阿克苏地区,65 +652901,阿克苏市,6529 +652922,温宿县,6529 +652923,库车县,6529 +652924,沙雅县,6529 +652925,新和县,6529 +652926,拜城县,6529 +652927,乌什县,6529 +652928,阿瓦提县,6529 +652929,柯坪县,6529 +6530,克孜勒苏柯尔克孜自治州,65 +653001,阿图什市,6530 +653022,阿克陶县,6530 +653023,阿合奇县,6530 +653024,乌恰县,6530 +6531,喀什地区,65 +653101,喀什市,6531 +653121,疏附县,6531 +653122,疏勒县,6531 +653123,英吉沙县,6531 +653124,泽普县,6531 +653125,莎车县,6531 +653126,叶城县,6531 +653127,麦盖提县,6531 +653128,岳普湖县,6531 +653129,伽师县,6531 +653130,巴楚县,6531 +653131,塔什库尔干塔吉克自治县,6531 +6532,和田地区,65 +653201,和田市,6532 +653221,和田县,6532 +653222,墨玉县,6532 +653223,皮山县,6532 +653224,洛浦县,6532 +653225,策勒县,6532 +653226,于田县,6532 +653227,民丰县,6532 +6540,伊犁哈萨克自治州,65 +654002,伊宁市,6540 +654003,奎屯市,6540 +654004,霍尔果斯市,6540 +654021,伊宁县,6540 +654022,察布查尔锡伯自治县,6540 +654023,霍城县,6540 +654024,巩留县,6540 +654025,新源县,6540 +654026,昭苏县,6540 +654027,特克斯县,6540 +654028,尼勒克县,6540 +6542,塔城地区,65 +654201,塔城市,6542 +654202,乌苏市,6542 +654221,额敏县,6542 +654223,沙湾县,6542 +654224,托里县,6542 +654225,裕民县,6542 +654226,和布克赛尔蒙古自治县,6542 +6543,阿勒泰地区,65 +654301,阿勒泰市,6543 +654321,布尔津县,6543 +654322,富蕴县,6543 +654323,福海县,6543 +654324,哈巴河县,6543 +654325,青河县,6543 +654326,吉木乃县,6543 +659001,石河子市,65 +659002,阿拉尔市,65 +659003,图木舒克市,65 +659004,五家渠市,65 +659005,北屯市,65 +659006,铁门关市,65 +659007,双河市,65 +659008,可克达拉市,65 +659009,昆玉市,65 +71,台湾省, +81,香港特别行政区, +82,澳门特别行政区, diff --git a/src/main/resources/index.html b/src/main/resources/index.html new file mode 100644 index 0000000..9d2fdca --- /dev/null +++ b/src/main/resources/index.html @@ -0,0 +1,10 @@ + + + + + Title + + +111 + + \ No newline at end of file diff --git a/src/main/resources/install.sh b/src/main/resources/install.sh new file mode 100755 index 0000000..3d76cf1 --- /dev/null +++ b/src/main/resources/install.sh @@ -0,0 +1,57 @@ +#! /bin/sh + +WORD_DIR=$(cd $(dirname $0); pwd) +SERVICE_NAME="wvp" + +# 检查是否为 root 用户 +if [ "$(id -u)" -ne 0 ]; then + echo "提示: 建议使用 root 用户执行此脚本,否则可能权限不足!" + read -p "继续?(y/n) " -n 1 -r + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi + echo +fi + +# 当前目录直接搜索(不含子目录) +jar_files=(*.jar) + +if [ ${#jar_files[@]} -eq 0 ]; then + echo "当前目录无 JAR 文件!" + exit 1 +fi + +# 遍历结果 +for jar in "${jar_files[@]}"; do + echo "找到 JAR 文件: $jar" +done + +# 写文件 +# 生成 Systemd 服务文件内容 +SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" +cat << EOF | sudo tee "$SERVICE_FILE" > /dev/null +[Unit] +Description=${SERVICE_NAME} +After=syslog.target + +[Service] +User=$USER +WorkingDirectory=${WORD_DIR} +ExecStart=java -jar ${jar_files} +SuccessExitStatus=143 +Restart=on-failure +RestartSec=10s +Environment=SPRING_PROFILES_ACTIVE=prod + +[Install] +WantedBy=multi-user.target +EOF + +# 重载 Systemd 并启动服务 +sudo systemctl daemon-reload +sudo systemctl enable "$SERVICE_NAME" +sudo systemctl start "$SERVICE_NAME" + +# 验证服务状态 +echo "服务已安装!执行以下命令查看状态:" +echo "sudo systemctl status $SERVICE_NAME" diff --git a/src/main/resources/jwk.json b/src/main/resources/jwk.json new file mode 100644 index 0000000..912b8f3 --- /dev/null +++ b/src/main/resources/jwk.json @@ -0,0 +1 @@ +{"keys":[{"kty":"RSA","kid":"3e79646c4dbc408383a9eed09f2b85ae","n":"rThRAlbMRceko3NkymeSoN2ICVaDlNBLWv3cyLUeixjWcmuhnPv2JpXmgoxezKZfhH_0sChBof--BaaqSUukl9wWMW1bWCyFyU5qNczhQk3ANlhaLiSgXsqD-NKI3ObJjB-26fnOZb9QskCqrPW1lEtwgb9-skMAfGlh5kaDOKjYKI64DPSMMXpSiJEDM-7DK-TFfm0QfPcoH-k-1C02NHlGWehVUn9FUJ0TAiDxpKj28qOmYh7s1M7OU_h-Sso7LM-5zbftpcO6SINe81Gw9JPd7rKPCRxkw8ROSCCq-JH_zshM80kTK2nWcseGvhQ_4vKQIBp9PrAgCrGJHM160w","e":"AQAB","d":"AwS2NKo6iQS_k7GREg3X-kGh-zest00h4wYFcOHnFFlsczX47PlfArEeASxdAofrpi1soB0zd5UzRHnxAbH1vkexg076hoDQG__nzeQyEKu2K7xCZgdxW_V_cziH9gF3hZ-P2mfl9tPsng6OatElRt5BqaEingyY15ImiJK1-qi_LTx4gfwRfquKLbUgqJR4Tf6eKlwOzEo41Ilo26gnojNzWryB_XHG7lj6SngPDBJp7ty32je4Fv3A3hXt7JHDwloww6-xiRtUflDpSec4A-o-PHgbfoYLyM7mM4BDt4PM54EHm4u8WzypG0wNKDTiq4KSapei5xDbiG3RpngvAQ","p":"5kUHkGxnZvZT762Ex-0De2nYodAbbZNVR-eIPx2ng2VZmEbAU3cp_DxigpXWyQ0FwJ2Me8GvxnlbxJ7k7d-4AV2X8q6Q-UqXajHdudRU_QX05kPEgZ3xtPk5ekI0-u1BEQT7pY_gxlZC2mzXAcVLd-LwbVPuQEba5S4JMsjcHUE","q":"wJNa06-qZ2tWncGl7cfJdO-SJ_H3taowMhh-RsJmeVefjjN3pfVjjE0wG_rIP-BjjCB9OhvSnI8LDjoNu8uIg090DYnA6IUfZpWo3zjgedeyqQyXFVjjVQkn98zgp5NFLpuitZsl9-EHhh7JaZDCwaJ527MN3VCoQxeI75ggjxM","dp":"HQTH_kBbC5OxYjwIxrUswinFnia-viFaFvSrq-CN0rY8Az-vTxVuWhY2B-TgK3gTqIFyScpP34A9u1qW2Q9fffSQiInNRU1MJZrhKWED0NsmULprkjYYVsktoCWlzZWGpKFvIR8voW8Pf71FnziA2TvlNrHkDX-gaE9T422Cp8E","dq":"owJYqMWS1dYLTKBlx0ANbHl6W2u7xb_Y6h7HjTfzLBWazvEL_6QW7uVLqvN-XGuheDTsK6rvfWyr7BACHgvsc1JnJyqK64f8C4b1mnZ3tUt7RROONBi43ftRJLX9GHxV3F0LvvQkkI2gI8ydq0lJQkU5J1qKiuNCewBJ_p3kOZc","qi":"hNAZV6aWEEWfB1HkrfdtO6sjq9ceEod55ez82I1ZNgoKle8gpRkh3vw2EIJ_5lcw57s5rw8G-sCQPG1AQSZ6u9aURwHkIXjpIhLAlv6gvKkCh0smPPvnSiltJKOJsuHkrD6rGkV1f-MlCS51lKlk9xShQzkRidkNd4BUh0a7ktA"}]} \ No newline at end of file diff --git a/src/main/resources/local.jks b/src/main/resources/local.jks new file mode 100644 index 0000000..529be6b Binary files /dev/null and b/src/main/resources/local.jks differ diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..308f148 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + ${log.pattern} + UTF-8 + + + + + DEBUG + + + + + + + ${log.pattern} + UTF-8 + + + + + + + + + + ${LOG_HOME}/wvp-%d{yyyy-MM-dd}.%i.log + + 30 + 20MB + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n + UTF-8 + + + + + DEBUG + + + + + + + + ${LOG_HOME}/sip-%d{yyyy-MM-dd}.%i.log + + 30 + 50MB + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n + UTF-8 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/配置详情.yml b/src/main/resources/配置详情.yml new file mode 100644 index 0000000..f9ae606 --- /dev/null +++ b/src/main/resources/配置详情.yml @@ -0,0 +1,285 @@ + + + +# 此配置文件只是用作展示所有配置项, 不可直接使用 + + +spring: + cache: + type: redis + thymeleaf: + cache: false + # 设置接口超时时间 + mvc: + async: + request-timeout: 20000 + # [可选]上传文件大小限制 + servlet: + multipart: + max-file-size: 10MB + max-request-size: 100MB + # REDIS数据库配置 + redis: + # [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1 + host: 127.0.0.1 + # [必须修改] 端口号 + port: 6379 + # [可选] 数据库 DB + database: 6 + # [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接 + password: + # [可选] 超时时间 + timeout: 10000 + # [可选] 一个pool最多可分配多少个jedis实例 + poolMaxTotal: 1000 + # [可选] 一个pool最多有多少个状态为idle(空闲)的jedis实例 + poolMaxIdle: 500 + # [可选] 最大的等待时间(秒) + poolMaxWait: 5 + # [必选] jdbc数据库配置 + datasource: + # kingbase配置 + # type: com.zaxxer.hikari.HikariDataSource + # driver-class-name: com.kingbase8.Driver + # url: jdbc:kingbase8://192.168.1.55:54321/wvp?useUnicode=true&characterEncoding=utf8 + # username: system + # password: system + # postgresql配置 + # type: com.zaxxer.hikari.HikariDataSource + # driver-class-name: org.postgresql.Driver + # url: jdbc:postgresql://192.168.1.242:3306/242wvp + # username: root + # password: SYceshizu1234 + # mysql配置 + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://127.0.0.1:3306/wvp2?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true + username: root + password: root123 + hikari: + connection-timeout: 20000 # 是客户端等待连接池连接的最大毫秒数 + initialSize: 50 # 连接池初始化连接数 + maximum-pool-size: 200 # 连接池最大连接数 + minimum-idle: 10 # 连接池最小空闲连接数 + idle-timeout: 300000 # 允许连接在连接池中空闲的最长时间(以毫秒为单位) + max-lifetime: 1200000 # 是池中连接关闭后的最长生命周期(以毫秒为单位) + + + +# 修改分页插件为 postgresql, 数据库类型为mysql不需要 +#pagehelper: +# helper-dialect: postgresql + +# [可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口 +server: + port: 18080 + # [可选] HTTPS配置, 默认不开启 + ssl: + # [可选] 是否开启HTTPS访问 + enabled: false + # [可选] 证书文件路径,放置在resource/目录下即可,修改xxx为文件名 + key-store: classpath:xxx.jks + # [可选] 证书密码 + key-store-password: password + # [可选] 证书类型, 默认为jks,根据实际修改 + key-store-type: JKS + # 配置证书可以使用如下两项,如上面二选一即可 + # PEM 编码证书 + certificate: xx.pem + # 私钥文件 + certificate-private-key: xx.key + # protocols: TLSv1.2 + # ciphers: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + + +# 作为28181服务器的配置 +sip: + # [必须修改] 本机的IP,对应你的网卡,监听什么ip就是使用什么网卡, + # 如果要监听多张网卡,可以使用逗号分隔多个IP, 例如: 192.168.1.4,10.0.0.4 + # 如果不明白,就使用0.0.0.0,大部分情况都是可以的 + # 请不要使用127.0.0.1,任何包括localhost在内的域名都是不可以的。 + ip: 0.0.0.0 + # [可选] 没有任何业务需求,仅仅是在前端展示的时候用 + show-ip: 192.168.0.100 + # [可选] 28181服务监听的端口 + port: 5060 + # 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007) + # 后两位为行业编码,定义参照附录D.3 + # 3701020049标识山东济南历下区 信息行业接入 + # [可选] + domain: 4401020049 + # [可选] + id: 44010200492000000001 + # [可选] 默认设备认证密码,后续扩展使用设备单独密码, 移除密码将不进行校验 + password: admin123 + # [可选] 国标级联注册失败,再次发起注册的时间间隔。 默认60秒 + register-time-interval: 60 + # [可选] 云台控制速度 + ptz-speed: 50 + # TODO [可选] 收到心跳后自动上线, 重启服务后会将所有设备置为离线,默认false,等待注册后上线。设置为true则收到心跳设置为上线。 + # keepalliveToOnline: false + # 是否存储alarm信息 + alarm: false + # 命令发送等待回复的超时时间, 单位:毫秒 + timeout: 1000 + +# 做为JT1078服务器的配置 +ftp: + #[必须修改] 是否开启1078的服务 + enable: true + port: 2121 + passive-ports: 10000-10500 + +# 做为JT1078服务器的配置 +jt1078: + #[必须修改] 是否开启1078的服务 + enable: true + #[必修修改] 1708设备接入的端口 + port: 21078 + #[可选] 设备鉴权的密码 + password: admin123 + +sy: + enable: false + # 云台控制开始到结束的时间间隔(毫秒) + ptz-control-time-interval: 300 + +#zlm 默认服务器配置 +media: + # [必须修改] zlm服务器唯一id,用于触发hook时区别是哪台服务器,general.mediaServerId + id: + # [必须修改] zlm服务器的内网IP + ip: 192.168.0.100 + # [可选] 有公网IP就配置公网IP, 不可用域名 + wan_ip: + # [可选] 返回流地址时的ip,置空使用 media.ip + stream-ip: + # [可选] wvp在国标信令中使用的ip,此ip为摄像机可以访问到的ip, 置空使用 media.ip + sdp-ip: + # [可选] zlm服务器访问WVP所使用的IP, 默认使用127.0.0.1,zlm和wvp没有部署在同一台服务器时必须配置 + hook-ip: 172.19.128.50 + # [必须修改] zlm服务器的http.port + http-port: 80 + # [可选] zlm服务器的http.sslport, 置空使用zlm配置文件配置 + http-ssl-port: + # [可选] zlm服务器的rtmp.port, 置空使用zlm配置文件配置 + rtmp-port: + # [可选] zlm服务器的rtmp.sslport, 置空使用zlm配置文件配置 + rtmp-ssl-port: + # [可选] zlm服务器的 rtp_proxy.port, 置空使用zlm配置文件配置 + rtp-proxy-port: + # [可选] zlm服务器的 rtsp.port, 置空使用zlm配置文件配置 + rtsp-port: + # [可选] zlm服务器的 rtsp.sslport, 置空使用zlm配置文件配置 + rtsp-ssl-port: + # [可选] 是否自动配置ZLM, 如果希望手动配置ZLM, 可以设为false, 不建议新接触的用户修改 + auto-config: true + # [可选] zlm服务器的hook.admin_params=secret + secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc + # 录像路径 + record-path: ./www/record + # 录像保存时长 + record-day: 7 + # 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试 + rtp: + # [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输 + enable: true + # [可选] 在此范围内选择端口用于媒体流传输, 必须提前在zlm上配置该属性,不然自动配置此属性可能不成功 + port-range: 30000,30500 # 端口范围 + # [可选] 国标级联在此范围内选择端口发送媒体流,请不要与收流端口范围重合 + send-port-range: 50502,50506 # 端口范围 + # 录像辅助服务, 部署此服务可以实现zlm录像的管理与下载, 0 表示不使用 + record-assist-port: 0 + +# [可选] 日志配置, 如果不需要在jar外修改日志内容那么可以不配置此项 +logging: + config: classpath:logback-spring.xml + +# [根据业务需求配置] +user-settings: + # 服务ID,不写则为000000 + server-id: + # [可选] 自动点播, 使用固定流地址进行播放时,如果未点播则自动进行点播, 需要rtp.enable=true + auto-apply-play: false + # [可选] 部分设备需要扩展SDP,需要打开此设置 + senior-sdp: false + # 保存移动位置历史轨迹:true:保留历史数据,false:仅保留最后的位置(默认) + save-position-history: false + # 点播/录像回放 等待超时时间,单位:毫秒 + play-timeout: 18000 + # 获取设备录像数据超时时间,单位:毫秒 + record-info-timeout: 10000 + # 上级点播等待超时时间,单位:毫秒 + platform-play-timeout: 60000 + # 是否开启接口鉴权 + interface-authentication: true + # 接口鉴权例外的接口, 即不进行接口鉴权的接口,尽量详细书写,尽量不用/**,至少两级目录 + interface-authentication-excludes: + - /api/v1/** + # 推流直播是否录制 + record-push-live: true + # 国标是否录制 + record-sip: true + # 使用推流状态作为推流通道状态 + use-pushing-as-status: true + # 使用来源请求ip作为streamIp,当且仅当你只有zlm节点它与wvp在一起的情况下开启 + use-source-ip-as-stream-ip: false + # 国标点播 按需拉流, true:有人观看拉流,无人观看释放, false:拉起后不自动释放 + stream-on-demand: true + # 推流鉴权, 默认开启 + push-authority: true + # 设备上线时是否自动同步通道 + sync-channel-on-device-online: false + # 国标级联语音喊话发流模式 * UDP:udp传输 TCP-ACTIVE:tcp主动模式 TCP-PASSIVE:tcp被动模式 + broadcast-for-platform: UDP + # 是否使用设备来源Ip作为回复IP, 不设置则为 false + sip-use-source-ip-as-remote-address: false + # 是否开启sip日志 + sip-log: true + # 是否开启sql日志 + sql-log: true + # 消息通道功能-缺少国标ID是否给所有上级发送消息 + send-to-platforms-when-id-lost: true + # 保持通道状态,不接受notify通道状态变化, 兼容海康平台发送错误消息 + refuse-channel-status-channel-form-notify: false + # 设置notify缓存队列最大长度,超过此长度的数据将返回486 BUSY_HERE,消息丢弃, 默认10000 + max-notify-count-queue: 10000 + # 设备/通道状态变化时发送消息 + device-status-notify: false + # 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式 + use-custom-ssrc-for-parent-invite: true + # 国标级联离线后多久重试一次注册 + register-again-after-time: 60 + # 国标续订方式,true为续订,每次注册在同一个会话里,false为重新注册,每次使用新的会话 + register-keep-int-dialog: false + # 开启接口文档页面。 默认开启,生产环境建议关闭,遇到swagger相关的漏洞时也可以关闭 + doc-enable: true + # 跨域配置,不配置此项则允许所有跨域请求,配置后则只允许配置的页面的地址请求, 可以配置多个 + allowed-origins: + - http://localhost:8008 + - http://192.168.1.3:8008 + # 国标设备离线后的上线策略, + # 0: 国标标准实现,设备离线后不回复心跳,直到设备重新注册上线, + # 1(默认): 对于离线设备,收到心跳就把设备设置为上线,并更新注册时间为上次这次心跳的时间。防止过期时间判断异常 + gb-device-online: 0 + # 登录超时时间(分钟), + login-timeout: 30 + # jwk文件路径,若不指定则使用resources目录下的jwk.json + jwk-file: classpath:jwk.json + # wvp集群模式下如果注册向上级的wvp奔溃,则自动选择一个其他wvp继续注册到上级 + auto-register-platform: true + # 按需发送位置, 默认发送移动位置订阅时如果位置不变则不发送, 设置为false按照国标间隔持续发送 + send-position-on-demand: true + # 部分设备会在短时间内发送大量注册, 导致协议栈内存溢出, 开启此项可以防止这部分设备注册, 避免服务崩溃,但是会降低系统性能, 描述如下 + # 默认值为 true。 + # 将此设置为 false 会使 Stack 在 Server Transaction 进入 TERMINATED 状态后关闭服务器套接字。 + # 这允许服务器防止客户端发起的基于 TCP 的拒绝服务攻击(即发起数百个客户端事务)。 + # 如果为 true(默认作),则堆栈将保持套接字打开,以便以牺牲线程和内存资源为代价来最大化性能 - 使自身容易受到 DOS 攻击。 + sip-cache-server-connections: true + +# 关闭在线文档(生产环境建议关闭) +springdoc: + api-docs: + enabled: false + swagger-ui: + enabled: false diff --git a/src/test/java/com/genersoft/iot/vmp/jt1078/JT1078ServerTest.java b/src/test/java/com/genersoft/iot/vmp/jt1078/JT1078ServerTest.java new file mode 100644 index 0000000..2a48961 --- /dev/null +++ b/src/test/java/com/genersoft/iot/vmp/jt1078/JT1078ServerTest.java @@ -0,0 +1,103 @@ +package com.genersoft.iot.vmp.jt1078; + +import com.genersoft.iot.vmp.jt1078.cmd.JT1078Template; +import com.genersoft.iot.vmp.jt1078.codec.netty.TcpServer; +import com.genersoft.iot.vmp.jt1078.proc.response.J9102; +import com.genersoft.iot.vmp.jt1078.proc.response.J9201; +import com.genersoft.iot.vmp.jt1078.proc.response.J9202; +import com.genersoft.iot.vmp.jt1078.proc.response.J9205; + +import java.util.Scanner; + +/** + * @author QingtaiJiang + * @date 2023/4/28 14:22 + * @email qingtaij@163.com + */ +public class JT1078ServerTest { + + private static final JT1078Template jt1078Template = new JT1078Template(); + + public static void main(String[] args) { + System.out.println("Starting jt1078 server..."); + TcpServer tcpServer = new TcpServer(21078, null, null); + tcpServer.start(); + System.out.println("Start jt1078 server success!"); + + + Scanner s = new Scanner(System.in); + while (true) { + String code = s.nextLine(); + switch (code) { + case "1": + test9102(); + break; + case "2": + test9201(); + break; + case "3": + test9202(); + break; + case "4": + test9205(); + break; + default: + break; + } + } + } + + private static void test9102() { + J9102 j9102 = new J9102(); + j9102.setChannel(1); + j9102.setCommand(0); + j9102.setCloseType(0); + j9102.setStreamType(0); + + Object s = jt1078Template.stopLive("18864197066", j9102, 6); + System.out.println(s); + } + + private static void test9201() { + J9201 j9201 = new J9201(); + j9201.setIp("192.168.1.1"); + j9201.setChannel(1); + j9201.setTcpPort(7618); + j9201.setUdpPort(7618); + j9201.setType(0); + j9201.setRate(0); + j9201.setStorageType(0); + j9201.setPlaybackType(0); + j9201.setPlaybackSpeed(0); + j9201.setStartTime("230428134100"); + j9201.setEndTime("230428134200"); + + Object s = jt1078Template.startBackLive("18864197066", j9201, 6); + System.out.println(s); + } + + private static void test9202() { + J9202 j9202 = new J9202(); + + j9202.setChannel(1); + j9202.setPlaybackType(2); + j9202.setPlaybackSpeed(0); + j9202.setPlaybackTime("230428134100"); + + Object s = jt1078Template.controlBackLive("18864197066", j9202, 6); + System.out.println(s); + } + + private static void test9205() { + J9205 j9205 = new J9205(); + j9205.setChannelId(1); + j9205.setStartTime("230428134100"); + j9205.setEndTime("230428134100"); + j9205.setMediaType(0); + j9205.setStreamType(0); + j9205.setStorageType(0); + + Object s = jt1078Template.queryBackTime("18864197066", j9205, 6); + System.out.println(s); + } +} diff --git a/web/.editorconfig b/web/.editorconfig new file mode 100644 index 0000000..ea6e20f --- /dev/null +++ b/web/.editorconfig @@ -0,0 +1,14 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/web/.env.development b/web/.env.development new file mode 100644 index 0000000..de583d0 --- /dev/null +++ b/web/.env.development @@ -0,0 +1,5 @@ +# just a flag +ENV = 'development' + +# base api +VUE_APP_BASE_API = '/dev-api' diff --git a/web/.env.production b/web/.env.production new file mode 100644 index 0000000..8994f69 --- /dev/null +++ b/web/.env.production @@ -0,0 +1,6 @@ +# just a flag +ENV = 'production' + +# base api +VUE_APP_BASE_API = '' + diff --git a/web/.env.staging b/web/.env.staging new file mode 100644 index 0000000..a8793a0 --- /dev/null +++ b/web/.env.staging @@ -0,0 +1,8 @@ +NODE_ENV = production + +# just a flag +ENV = 'staging' + +# base api +VUE_APP_BASE_API = '/stage-api' + diff --git a/web/.eslintignore b/web/.eslintignore new file mode 100644 index 0000000..e6529fc --- /dev/null +++ b/web/.eslintignore @@ -0,0 +1,4 @@ +build/*.js +src/assets +public +dist diff --git a/web/.eslintrc.js b/web/.eslintrc.js new file mode 100644 index 0000000..303d049 --- /dev/null +++ b/web/.eslintrc.js @@ -0,0 +1,48 @@ +module.exports = { + root: true, + env: { + node: true, + browser: true, + }, + extends: ["plugin:vue/essential", "eslint:recommended"], + parserOptions: { + parser: "babel-eslint", + }, + rules: { + // Disable or downgrade problematic rules + "vue/require-prop-types": "off", + "vue/require-default-prop": "off", + "vue/no-unused-vars": "warn", + "no-unused-vars": "warn", + "no-undef": "warn", + eqeqeq: "warn", + "no-return-assign": "warn", + "new-cap": "warn", + "vue/html-self-closing": "off", + "vue/html-closing-bracket-spacing": "off", + "vue/this-in-template": "off", + "vue/require-v-for-key": "warn", + "vue/valid-v-model": "warn", + "vue/attributes-order": "off", + "no-multiple-empty-lines": "warn", + + // Style rules - make them warnings instead of errors + quotes: ["warn", "single"], + "comma-dangle": ["warn", "never"], + "space-in-parens": "warn", + "comma-spacing": "warn", + "object-curly-spacing": ["warn", "always"], + "arrow-spacing": "warn", + semi: ["warn", "never"], + "no-multi-spaces": "warn", + + // Turn off console warnings for development + "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", + "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", + }, + globals: { + // Define global variables to prevent 'undefined' errors + ZLMRTCClient: "readonly", + jessibuca: "readonly", + }, +} diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..9ad28d2 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,16 @@ +.DS_Store +node_modules/ +dist/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json +tests/**/coverage/ + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln diff --git a/web/.travis.yml b/web/.travis.yml new file mode 100644 index 0000000..f4be7a0 --- /dev/null +++ b/web/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: 10 +script: npm run test +notifications: + email: false diff --git a/web/LICENSE b/web/LICENSE new file mode 100644 index 0000000..6151575 --- /dev/null +++ b/web/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017-present PanJiaChen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/web/README-zh.md b/web/README-zh.md new file mode 100644 index 0000000..1beec9b --- /dev/null +++ b/web/README-zh.md @@ -0,0 +1,111 @@ +# vue-admin-template + +> 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。 + +[线上地址](http://panjiachen.github.io/vue-admin-template) + +[国内访问](https://panjiachen.gitee.io/vue-admin-template) + +目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`。 + +

    + SPONSORED BY +

    +

    + + + +

    + +## Extra + +如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) + +## 相关项目 + +- [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) + +- [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) + +- [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) + +- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312) + +写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目: + +- [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2) +- [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac) +- [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35) +- [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56) +- [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836) + +## Build Setup + +```bash +# 克隆项目 +git clone https://github.com/PanJiaChen/vue-admin-template.git + +# 进入项目目录 +cd vue-admin-template + +# 安装依赖 +npm install + +# 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 +npm install --registry=https://registry.npm.taobao.org + +# 启动服务 +npm run dev +``` + +浏览器访问 [http://localhost:9528](http://localhost:9528) + +## 发布 + +```bash +# 构建测试环境 +npm run build:stage + +# 构建生产环境 +npm run build:prod +``` + +## 其它 + +```bash +# 预览发布环境效果 +npm run preview + +# 预览发布环境效果 + 静态资源分析 +npm run preview -- --report + +# 代码格式检查 +npm run lint + +# 代码格式检查并自动修复 +npm run lint -- --fix +``` + +更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/) + +## 购买贴纸 + +你也可以通过 购买[官方授权的贴纸](https://smallsticker.com/product/vue-element-admin) 的方式来支持 vue-element-admin - 每售出一张贴纸,我们将获得 2 元的捐赠。 + +## Demo + +![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif) + +## Browsers support + +Modern browsers and Internet Explorer 10+. + +| [IE / Edge](http://godban.github.io/browsers-support-badges/)
    IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
    Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
    Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
    Safari | +| --------- | --------- | --------- | --------- | +| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions + +## License + +[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license. + +Copyright (c) 2017-present PanJiaChen diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..fa54b78 --- /dev/null +++ b/web/README.md @@ -0,0 +1,99 @@ +# vue-admin-template + +English | [简体中文](./README-zh.md) + +> A minimal vue admin template with Element UI & axios & iconfont & permission control & lint + +**Live demo:** http://panjiachen.github.io/vue-admin-template + + +**The current version is `v4.0+` build on `vue-cli`. If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0), it does not rely on `vue-cli`** + +

    + SPONSORED BY +

    +

    + + + +

    + +## Build Setup + +```bash +# clone the project +git clone https://github.com/PanJiaChen/vue-admin-template.git + +# enter the project directory +cd vue-admin-template + +# install dependency +npm install + +# develop +npm run dev +``` + +This will automatically open http://localhost:9528 + +## Build + +```bash +# build for test environment +npm run build:stage + +# build for production environment +npm run build:prod +``` + +## Advanced + +```bash +# preview the release environment effect +npm run preview + +# preview the release environment effect + static resource analysis +npm run preview -- --report + +# code format check +npm run lint + +# code format check and auto fix +npm run lint -- --fix +``` + +Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information + +## Demo + +![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif) + +## Extra + +If you want router permission && generate menu by user roles , you can use this branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) + +For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour)) + +## Related Project + +- [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) + +- [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) + +- [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) + +- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312) + +## Browsers support + +Modern browsers and Internet Explorer 10+. + +| [IE / Edge](http://godban.github.io/browsers-support-badges/)
    IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
    Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
    Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
    Safari | +| --------- | --------- | --------- | --------- | +| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions + +## License + +[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license. + +Copyright (c) 2017-present PanJiaChen diff --git a/web/babel.config.js b/web/babel.config.js new file mode 100644 index 0000000..fb82b27 --- /dev/null +++ b/web/babel.config.js @@ -0,0 +1,14 @@ +module.exports = { + presets: [ + // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app + '@vue/cli-plugin-babel/preset' + ], + 'env': { + 'development': { + // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require(). + // This plugin can significantly increase the speed of hot updates, when you have a large number of pages. + // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html + 'plugins': ['dynamic-import-node'] + } + } +} diff --git a/web/build/index.js b/web/build/index.js new file mode 100644 index 0000000..293d372 --- /dev/null +++ b/web/build/index.js @@ -0,0 +1,35 @@ +const { run } = require('runjs') +const chalk = require('chalk') +const config = require('../vue.config.js') +const rawArgv = process.argv.slice(2) +const args = rawArgv.join(' ') + +if (process.env.npm_config_preview || rawArgv.includes('--preview')) { + const report = rawArgv.includes('--report') + + run(`vue-cli-service build ${args}`) + + const port = 9526 + const publicPath = config.publicPath + + var connect = require('connect') + var serveStatic = require('serve-static') + const app = connect() + + app.use( + publicPath, + serveStatic('./src/main/resources/static', { + index: ['index.html', '/'] + }) + ) + + app.listen(port, function () { + console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) + if (report) { + console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) + } + + }) +} else { + run(`vue-cli-service build ${args}`) +} diff --git a/web/jest.config.js b/web/jest.config.js new file mode 100644 index 0000000..143cdc8 --- /dev/null +++ b/web/jest.config.js @@ -0,0 +1,24 @@ +module.exports = { + moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], + transform: { + '^.+\\.vue$': 'vue-jest', + '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': + 'jest-transform-stub', + '^.+\\.jsx?$': 'babel-jest' + }, + moduleNameMapper: { + '^@/(.*)$': '/src/$1' + }, + snapshotSerializers: ['jest-serializer-vue'], + testMatch: [ + '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' + ], + collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], + coverageDirectory: '/tests/unit/coverage', + // 'collectCoverage': true, + 'coverageReporters': [ + 'lcov', + 'text-summary' + ], + testURL: 'http://localhost/' +} diff --git a/web/jsconfig.json b/web/jsconfig.json new file mode 100644 index 0000000..ed079e2 --- /dev/null +++ b/web/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@/*": ["src/*"] + } + }, + "exclude": ["node_modules", "dist"] +} diff --git a/web/mock/index.js b/web/mock/index.js new file mode 100644 index 0000000..c514c13 --- /dev/null +++ b/web/mock/index.js @@ -0,0 +1,57 @@ +const Mock = require('mockjs') +const { param2Obj } = require('./utils') + +const user = require('./user') +const table = require('./table') + +const mocks = [ + ...user, + ...table +] + +// for front mock +// please use it cautiously, it will redefine XMLHttpRequest, +// which will cause many of your third-party libraries to be invalidated(like progress event). +function mockXHR() { + // mock patch + // https://github.com/nuysoft/Mock/issues/300 + Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send + Mock.XHR.prototype.send = function() { + if (this.custom.xhr) { + this.custom.xhr.withCredentials = this.withCredentials || false + + if (this.responseType) { + this.custom.xhr.responseType = this.responseType + } + } + this.proxy_send(...arguments) + } + + function XHR2ExpressReqWrap(respond) { + return function(options) { + let result = null + if (respond instanceof Function) { + const { body, type, url } = options + // https://expressjs.com/en/4x/api.html#req + result = respond({ + method: type, + body: JSON.parse(body), + query: param2Obj(url) + }) + } else { + result = respond + } + return Mock.mock(result) + } + } + + for (const i of mocks) { + Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) + } +} + +module.exports = { + mocks, + mockXHR +} + diff --git a/web/mock/mock-server.js b/web/mock/mock-server.js new file mode 100644 index 0000000..8941ec0 --- /dev/null +++ b/web/mock/mock-server.js @@ -0,0 +1,81 @@ +const chokidar = require('chokidar') +const bodyParser = require('body-parser') +const chalk = require('chalk') +const path = require('path') +const Mock = require('mockjs') + +const mockDir = path.join(process.cwd(), 'mock') + +function registerRoutes(app) { + let mockLastIndex + const { mocks } = require('./index.js') + const mocksForServer = mocks.map(route => { + return responseFake(route.url, route.type, route.response) + }) + for (const mock of mocksForServer) { + app[mock.type](mock.url, mock.response) + mockLastIndex = app._router.stack.length + } + const mockRoutesLength = Object.keys(mocksForServer).length + return { + mockRoutesLength: mockRoutesLength, + mockStartIndex: mockLastIndex - mockRoutesLength + } +} + +function unregisterRoutes() { + Object.keys(require.cache).forEach(i => { + if (i.includes(mockDir)) { + delete require.cache[require.resolve(i)] + } + }) +} + +// for mock server +const responseFake = (url, type, respond) => { + return { + url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`), + type: type || 'get', + response(req, res) { + console.log('request invoke:' + req.path) + res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) + } + } +} + +module.exports = app => { + // parse app.body + // https://expressjs.com/en/4x/api.html#req.body + app.use(bodyParser.json()) + app.use(bodyParser.urlencoded({ + extended: true + })) + + const mockRoutes = registerRoutes(app) + var mockRoutesLength = mockRoutes.mockRoutesLength + var mockStartIndex = mockRoutes.mockStartIndex + + // watch files, hot reload mock server + chokidar.watch(mockDir, { + ignored: /mock-server/, + ignoreInitial: true + }).on('all', (event, path) => { + if (event === 'change' || event === 'add') { + try { + // remove mock routes stack + app._router.stack.splice(mockStartIndex, mockRoutesLength) + + // clear routes cache + unregisterRoutes() + + const mockRoutes = registerRoutes(app) + mockRoutesLength = mockRoutes.mockRoutesLength + mockStartIndex = mockRoutes.mockStartIndex + + console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`)) + } catch (error) { + console.log(chalk.redBright(error)) + } + } + }) +} diff --git a/web/mock/table.js b/web/mock/table.js new file mode 100644 index 0000000..bd0e013 --- /dev/null +++ b/web/mock/table.js @@ -0,0 +1,29 @@ +const Mock = require('mockjs') + +const data = Mock.mock({ + 'items|30': [{ + id: '@id', + title: '@sentence(10, 20)', + 'status|1': ['published', 'draft', 'deleted'], + author: 'name', + display_time: '@datetime', + pageviews: '@integer(300, 5000)' + }] +}) + +module.exports = [ + { + url: '/vue-admin-template/table/list', + type: 'get', + response: config => { + const items = data.items + return { + code: 20000, + data: { + total: items.length, + items: items + } + } + } + } +] diff --git a/web/mock/user.js b/web/mock/user.js new file mode 100644 index 0000000..7555338 --- /dev/null +++ b/web/mock/user.js @@ -0,0 +1,84 @@ + +const tokens = { + admin: { + token: 'admin-token' + }, + editor: { + token: 'editor-token' + } +} + +const users = { + 'admin-token': { + roles: ['admin'], + introduction: 'I am a super administrator', + avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', + name: 'Super Admin' + }, + 'editor-token': { + roles: ['editor'], + introduction: 'I am an editor', + avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', + name: 'Normal Editor' + } +} + +module.exports = [ + // user login + { + url: '/vue-admin-template/user/login', + type: 'post', + response: config => { + const { username } = config.body + const token = tokens[username] + + // mock error + if (!token) { + return { + code: 60204, + message: 'Account and password are incorrect.' + } + } + + return { + code: 20000, + data: token + } + } + }, + + // get user info + { + url: '/vue-admin-template/user/info\.*', + type: 'get', + response: config => { + const { token } = config.query + const info = users[token] + + // mock error + if (!info) { + return { + code: 50008, + message: 'Login failed, unable to get user details.' + } + } + + return { + code: 20000, + data: info + } + } + }, + + // user logout + { + url: '/vue-admin-template/user/logout', + type: 'post', + response: _ => { + return { + code: 20000, + data: 'success' + } + } + } +] diff --git a/web/mock/utils.js b/web/mock/utils.js new file mode 100644 index 0000000..95cc27d --- /dev/null +++ b/web/mock/utils.js @@ -0,0 +1,25 @@ +/** + * @param {string} url + * @returns {Object} + */ +function param2Obj(url) { + const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') + if (!search) { + return {} + } + const obj = {} + const searchArr = search.split('&') + searchArr.forEach(v => { + const index = v.indexOf('=') + if (index !== -1) { + const name = v.substring(0, index) + const val = v.substring(index + 1, v.length) + obj[name] = val + } + }) + return obj +} + +module.exports = { + param2Obj +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..b136971 --- /dev/null +++ b/web/package.json @@ -0,0 +1,78 @@ +{ + "name": "vue-admin-template", + "version": "4.4.0", + "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint", + "author": "Pan ", + "scripts": { + "dev": "vue-cli-service serve --host=0.0.0.0", + "build:prod": "vue-cli-service build", + "build:stage": "vue-cli-service build --mode staging", + "preview": "node build/index.js --preview", + "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", + "lint": "eslint --ext .js,.vue src", + "test:unit": "jest --clearCache && vue-cli-service test:unit", + "test:ci": "npm run lint && npm run test:unit" + }, + "dependencies": { + "@femessage/log-viewer": "^1.5.0", + "@wchbrad/vue-easy-tree": "^1.0.13", + "axios": "^0.24.0", + "core-js": "3.6.5", + "dayjs": "^1.11.13", + "echarts": "^4.9.0", + "element-ui": "^2.15.14", + "gcoord": "^1.0.7", + "js-cookie": "2.2.0", + "moment": "^2.29.1", + "moment-duration-format": "^2.3.2", + "normalize.css": "7.0.0", + "nprogress": "0.2.0", + "ol": "^10.6.1", + "path-to-regexp": "2.4.0", + "screenfull": "5.1.0", + "strip-ansi": "^7.1.0", + "v-charts": "^1.19.0", + "vue": "2.6.10", + "vue-clipboard2": "^0.3.3", + "vue-clipboards": "^1.3.0", + "vue-contextmenujs": "^1.4.11", + "vue-router": "3.0.6", + "vue-ztree-2.0": "^1.0.4", + "vuex": "3.1.0", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "4.4.4", + "@vue/cli-plugin-eslint": "4.4.4", + "@vue/cli-plugin-unit-jest": "4.4.4", + "@vue/cli-service": "4.4.4", + "@vue/test-utils": "1.0.0-beta.29", + "autoprefixer": "9.5.1", + "babel-eslint": "10.1.0", + "babel-jest": "23.6.0", + "babel-plugin-dynamic-import-node": "2.3.3", + "chalk": "2.4.2", + "connect": "3.6.6", + "eslint": "6.7.2", + "eslint-plugin-vue": "6.2.2", + "html-webpack-plugin": "3.2.0", + "mockjs": "1.0.1-beta3", + "runjs": "4.3.2", + "sass": "1.26.8", + "sass-loader": "8.0.2", + "script-ext-html-webpack-plugin": "2.1.3", + "serve-static": "1.13.2", + "svg-sprite-loader": "4.1.3", + "svgo": "1.2.2", + "vue-template-compiler": "2.6.10" + }, + "browserslist": [ + "> 1%", + "last 2 versions" + ], + "engines": { + "node": ">=8.9", + "npm": ">= 3.0.0" + }, + "license": "MIT" +} diff --git a/web/postcss.config.js b/web/postcss.config.js new file mode 100644 index 0000000..10473ef --- /dev/null +++ b/web/postcss.config.js @@ -0,0 +1,8 @@ +// https://github.com/michael-ciniawsky/postcss-load-config + +module.exports = { + 'plugins': { + // to edit target browsers: use "browserslist" field in package.json + 'autoprefixer': {} + } +} diff --git a/web/public/favicon.ico b/web/public/favicon.ico new file mode 100644 index 0000000..892f195 Binary files /dev/null and b/web/public/favicon.ico differ diff --git a/web/public/index.html b/web/public/index.html new file mode 100644 index 0000000..6b89ee4 --- /dev/null +++ b/web/public/index.html @@ -0,0 +1,22 @@ + + + + + + + + <%= webpackConfig.name %> + + + + + + + + +
    + + + diff --git a/web/public/libDecoder.wasm b/web/public/libDecoder.wasm new file mode 100644 index 0000000..a45028d Binary files /dev/null and b/web/public/libDecoder.wasm differ diff --git a/web/public/static/file/设置电话本模板.xlsx b/web/public/static/file/设置电话本模板.xlsx new file mode 100644 index 0000000..a993210 Binary files /dev/null and b/web/public/static/file/设置电话本模板.xlsx differ diff --git a/web/public/static/images/abl-logo.jpg b/web/public/static/images/abl-logo.jpg new file mode 100644 index 0000000..82a564d Binary files /dev/null and b/web/public/static/images/abl-logo.jpg differ diff --git a/web/public/static/images/arrow.png b/web/public/static/images/arrow.png new file mode 100644 index 0000000..4d8df46 Binary files /dev/null and b/web/public/static/images/arrow.png differ diff --git a/web/public/static/images/bg19.png b/web/public/static/images/bg19.png new file mode 100644 index 0000000..9f3f8b2 Binary files /dev/null and b/web/public/static/images/bg19.png differ diff --git a/web/public/static/images/bg19.webp b/web/public/static/images/bg19.webp new file mode 100644 index 0000000..b106c59 Binary files /dev/null and b/web/public/static/images/bg19.webp differ diff --git a/web/public/static/images/gis/camera-offline.png b/web/public/static/images/gis/camera-offline.png new file mode 100644 index 0000000..67eb0fc Binary files /dev/null and b/web/public/static/images/gis/camera-offline.png differ diff --git a/web/public/static/images/gis/camera.png b/web/public/static/images/gis/camera.png new file mode 100644 index 0000000..a93bd55 Binary files /dev/null and b/web/public/static/images/gis/camera.png differ diff --git a/web/public/static/images/gis/camera1-offline.png b/web/public/static/images/gis/camera1-offline.png new file mode 100644 index 0000000..597209b Binary files /dev/null and b/web/public/static/images/gis/camera1-offline.png differ diff --git a/web/public/static/images/gis/camera1-red.png b/web/public/static/images/gis/camera1-red.png new file mode 100644 index 0000000..04346f3 Binary files /dev/null and b/web/public/static/images/gis/camera1-red.png differ diff --git a/web/public/static/images/gis/camera1.png b/web/public/static/images/gis/camera1.png new file mode 100644 index 0000000..e5f2b5f Binary files /dev/null and b/web/public/static/images/gis/camera1.png differ diff --git a/web/public/static/images/gis/camera2-offline.png b/web/public/static/images/gis/camera2-offline.png new file mode 100644 index 0000000..4ddae23 Binary files /dev/null and b/web/public/static/images/gis/camera2-offline.png differ diff --git a/web/public/static/images/gis/camera2.png b/web/public/static/images/gis/camera2.png new file mode 100644 index 0000000..073bceb Binary files /dev/null and b/web/public/static/images/gis/camera2.png differ diff --git a/web/public/static/images/gis/camera3-offline.png b/web/public/static/images/gis/camera3-offline.png new file mode 100644 index 0000000..f05c2a3 Binary files /dev/null and b/web/public/static/images/gis/camera3-offline.png differ diff --git a/web/public/static/images/gis/camera3.png b/web/public/static/images/gis/camera3.png new file mode 100644 index 0000000..b40f67b Binary files /dev/null and b/web/public/static/images/gis/camera3.png differ diff --git a/web/public/static/images/gis/sprite.png b/web/public/static/images/gis/sprite.png new file mode 100644 index 0000000..5737ef6 Binary files /dev/null and b/web/public/static/images/gis/sprite.png differ diff --git a/web/public/static/images/zlm-logo.png b/web/public/static/images/zlm-logo.png new file mode 100644 index 0000000..5f492dc Binary files /dev/null and b/web/public/static/images/zlm-logo.png differ diff --git a/web/public/static/js/ZLMRTCClient.js b/web/public/static/js/ZLMRTCClient.js new file mode 100644 index 0000000..dfe04a4 --- /dev/null +++ b/web/public/static/js/ZLMRTCClient.js @@ -0,0 +1,8222 @@ +var ZLMRTCClient = (function (exports) { + 'use strict'; + + const Events$1 = { + WEBRTC_NOT_SUPPORT: 'WEBRTC_NOT_SUPPORT', + WEBRTC_ICE_CANDIDATE_ERROR: 'WEBRTC_ICE_CANDIDATE_ERROR', + WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED: 'WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED', + WEBRTC_ON_REMOTE_STREAMS: 'WEBRTC_ON_REMOTE_STREAMS', + WEBRTC_ON_LOCAL_STREAM: 'WEBRTC_ON_LOCAL_STREAM', + WEBRTC_ON_CONNECTION_STATE_CHANGE: 'WEBRTC_ON_CONNECTION_STATE_CHANGE', + WEBRTC_ON_DATA_CHANNEL_OPEN: 'WEBRTC_ON_DATA_CHANNEL_OPEN', + WEBRTC_ON_DATA_CHANNEL_CLOSE: 'WEBRTC_ON_DATA_CHANNEL_CLOSE', + WEBRTC_ON_DATA_CHANNEL_ERR: 'WEBRTC_ON_DATA_CHANNEL_ERR', + WEBRTC_ON_DATA_CHANNEL_MSG: 'WEBRTC_ON_DATA_CHANNEL_MSG', + CAPTURE_STREAM_FAILED: 'CAPTURE_STREAM_FAILED' + }; + + const VERSION$1 = '1.0.1'; + const BUILD_DATE = 'Mon Mar 27 2023 19:11:59 GMT+0800 (China Standard Time)'; + + // Copyright (C) <2018> Intel Corporation + // + // SPDX-License-Identifier: Apache-2.0 + + // eslint-disable-next-line require-jsdoc + function isFirefox() { + return window.navigator.userAgent.match('Firefox') !== null; + } + // eslint-disable-next-line require-jsdoc + function isChrome() { + return window.navigator.userAgent.match('Chrome') !== null; + } + // eslint-disable-next-line require-jsdoc + function isEdge() { + return window.navigator.userAgent.match(/Edge\/(\d+).(\d+)$/) !== null; + } + + // Copyright (C) <2018> Intel Corporation + + /** + * @class AudioSourceInfo + * @classDesc Source info about an audio track. Values: 'mic', 'screen-cast', 'file', 'mixed'. + * @memberOf Owt.Base + * @readonly + * @enum {string} + */ + const AudioSourceInfo = { + MIC: 'mic', + SCREENCAST: 'screen-cast', + FILE: 'file', + MIXED: 'mixed' + }; + + /** + * @class VideoSourceInfo + * @classDesc Source info about a video track. Values: 'camera', 'screen-cast', 'file', 'mixed'. + * @memberOf Owt.Base + * @readonly + * @enum {string} + */ + const VideoSourceInfo = { + CAMERA: 'camera', + SCREENCAST: 'screen-cast', + FILE: 'file', + MIXED: 'mixed' + }; + + /** + * @class TrackKind + * @classDesc Kind of a track. Values: 'audio' for audio track, 'video' for video track, 'av' for both audio and video tracks. + * @memberOf Owt.Base + * @readonly + * @enum {string} + */ + const TrackKind = { + /** + * Audio tracks. + * @type string + */ + AUDIO: 'audio', + /** + * Video tracks. + * @type string + */ + VIDEO: 'video', + /** + * Both audio and video tracks. + * @type string + */ + AUDIO_AND_VIDEO: 'av' + }; + /** + * @class Resolution + * @memberOf Owt.Base + * @classDesc The Resolution defines the size of a rectangle. + * @constructor + * @param {number} width + * @param {number} height + */ + class Resolution { + // eslint-disable-next-line require-jsdoc + constructor(width, height) { + /** + * @member {number} width + * @instance + * @memberof Owt.Base.Resolution + */ + this.width = width; + /** + * @member {number} height + * @instance + * @memberof Owt.Base.Resolution + */ + this.height = height; + } + } + + /* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + + let logDisabled_ = true; + let deprecationWarnings_ = true; + + /** + * Extract browser version out of the provided user agent string. + * + * @param {!string} uastring userAgent string. + * @param {!string} expr Regular expression used as match criteria. + * @param {!number} pos position in the version string to be returned. + * @return {!number} browser version. + */ + function extractVersion(uastring, expr, pos) { + const match = uastring.match(expr); + return match && match.length >= pos && parseInt(match[pos], 10); + } + + // Wraps the peerconnection event eventNameToWrap in a function + // which returns the modified event object (or false to prevent + // the event). + function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) { + if (!window.RTCPeerConnection) { + return; + } + const proto = window.RTCPeerConnection.prototype; + const nativeAddEventListener = proto.addEventListener; + proto.addEventListener = function(nativeEventName, cb) { + if (nativeEventName !== eventNameToWrap) { + return nativeAddEventListener.apply(this, arguments); + } + const wrappedCallback = (e) => { + const modifiedEvent = wrapper(e); + if (modifiedEvent) { + if (cb.handleEvent) { + cb.handleEvent(modifiedEvent); + } else { + cb(modifiedEvent); + } + } + }; + this._eventMap = this._eventMap || {}; + if (!this._eventMap[eventNameToWrap]) { + this._eventMap[eventNameToWrap] = new Map(); + } + this._eventMap[eventNameToWrap].set(cb, wrappedCallback); + return nativeAddEventListener.apply(this, [nativeEventName, + wrappedCallback]); + }; + + const nativeRemoveEventListener = proto.removeEventListener; + proto.removeEventListener = function(nativeEventName, cb) { + if (nativeEventName !== eventNameToWrap || !this._eventMap + || !this._eventMap[eventNameToWrap]) { + return nativeRemoveEventListener.apply(this, arguments); + } + if (!this._eventMap[eventNameToWrap].has(cb)) { + return nativeRemoveEventListener.apply(this, arguments); + } + const unwrappedCb = this._eventMap[eventNameToWrap].get(cb); + this._eventMap[eventNameToWrap].delete(cb); + if (this._eventMap[eventNameToWrap].size === 0) { + delete this._eventMap[eventNameToWrap]; + } + if (Object.keys(this._eventMap).length === 0) { + delete this._eventMap; + } + return nativeRemoveEventListener.apply(this, [nativeEventName, + unwrappedCb]); + }; + + Object.defineProperty(proto, 'on' + eventNameToWrap, { + get() { + return this['_on' + eventNameToWrap]; + }, + set(cb) { + if (this['_on' + eventNameToWrap]) { + this.removeEventListener(eventNameToWrap, + this['_on' + eventNameToWrap]); + delete this['_on' + eventNameToWrap]; + } + if (cb) { + this.addEventListener(eventNameToWrap, + this['_on' + eventNameToWrap] = cb); + } + }, + enumerable: true, + configurable: true + }); + } + + function disableLog(bool) { + if (typeof bool !== 'boolean') { + return new Error('Argument type: ' + typeof bool + + '. Please use a boolean.'); + } + logDisabled_ = bool; + return (bool) ? 'adapter.js logging disabled' : + 'adapter.js logging enabled'; + } + + /** + * Disable or enable deprecation warnings + * @param {!boolean} bool set to true to disable warnings. + */ + function disableWarnings(bool) { + if (typeof bool !== 'boolean') { + return new Error('Argument type: ' + typeof bool + + '. Please use a boolean.'); + } + deprecationWarnings_ = !bool; + return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled'); + } + + function log$1() { + if (typeof window === 'object') { + if (logDisabled_) { + return; + } + if (typeof console !== 'undefined' && typeof console.log === 'function') { + console.log.apply(console, arguments); + } + } + } + + /** + * Shows a deprecation warning suggesting the modern and spec-compatible API. + */ + function deprecated(oldMethod, newMethod) { + if (!deprecationWarnings_) { + return; + } + console.warn(oldMethod + ' is deprecated, please use ' + newMethod + + ' instead.'); + } + + /** + * Browser detector. + * + * @return {object} result containing browser and version + * properties. + */ + function detectBrowser(window) { + // Returned result object. + const result = {browser: null, version: null}; + + // Fail early if it's not a browser + if (typeof window === 'undefined' || !window.navigator) { + result.browser = 'Not a browser.'; + return result; + } + + const {navigator} = window; + + if (navigator.mozGetUserMedia) { // Firefox. + result.browser = 'firefox'; + result.version = extractVersion(navigator.userAgent, + /Firefox\/(\d+)\./, 1); + } else if (navigator.webkitGetUserMedia || + (window.isSecureContext === false && window.webkitRTCPeerConnection && + !window.RTCIceGatherer)) { + // Chrome, Chromium, Webview, Opera. + // Version matches Chrome/WebRTC version. + // Chrome 74 removed webkitGetUserMedia on http as well so we need the + // more complicated fallback to webkitRTCPeerConnection. + result.browser = 'chrome'; + result.version = extractVersion(navigator.userAgent, + /Chrom(e|ium)\/(\d+)\./, 2); + } else if (navigator.mediaDevices && + navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { // Edge. + result.browser = 'edge'; + result.version = extractVersion(navigator.userAgent, + /Edge\/(\d+).(\d+)$/, 2); + } else if (window.RTCPeerConnection && + navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) { // Safari. + result.browser = 'safari'; + result.version = extractVersion(navigator.userAgent, + /AppleWebKit\/(\d+)\./, 1); + result.supportsUnifiedPlan = window.RTCRtpTransceiver && + 'currentDirection' in window.RTCRtpTransceiver.prototype; + } else { // Default fallthrough: not supported. + result.browser = 'Not a supported browser.'; + return result; + } + + return result; + } + + /** + * Checks if something is an object. + * + * @param {*} val The something you want to check. + * @return true if val is an object, false otherwise. + */ + function isObject$1(val) { + return Object.prototype.toString.call(val) === '[object Object]'; + } + + /** + * Remove all empty objects and undefined values + * from a nested object -- an enhanced and vanilla version + * of Lodash's `compact`. + */ + function compactObject(data) { + if (!isObject$1(data)) { + return data; + } + + return Object.keys(data).reduce(function(accumulator, key) { + const isObj = isObject$1(data[key]); + const value = isObj ? compactObject(data[key]) : data[key]; + const isEmptyObject = isObj && !Object.keys(value).length; + if (value === undefined || isEmptyObject) { + return accumulator; + } + return Object.assign(accumulator, {[key]: value}); + }, {}); + } + + /* iterates the stats graph recursively. */ + function walkStats(stats, base, resultSet) { + if (!base || resultSet.has(base.id)) { + return; + } + resultSet.set(base.id, base); + Object.keys(base).forEach(name => { + if (name.endsWith('Id')) { + walkStats(stats, stats.get(base[name]), resultSet); + } else if (name.endsWith('Ids')) { + base[name].forEach(id => { + walkStats(stats, stats.get(id), resultSet); + }); + } + }); + } + + /* filter getStats for a sender/receiver track. */ + function filterStats(result, track, outbound) { + const streamStatsType = outbound ? 'outbound-rtp' : 'inbound-rtp'; + const filteredResult = new Map(); + if (track === null) { + return filteredResult; + } + const trackStats = []; + result.forEach(value => { + if (value.type === 'track' && + value.trackIdentifier === track.id) { + trackStats.push(value); + } + }); + trackStats.forEach(trackStat => { + result.forEach(stats => { + if (stats.type === streamStatsType && stats.trackId === trackStat.id) { + walkStats(result, stats, filteredResult); + } + }); + }); + return filteredResult; + } + + /* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + const logging = log$1; + + function shimGetUserMedia$3(window, browserDetails) { + const navigator = window && window.navigator; + + if (!navigator.mediaDevices) { + return; + } + + const constraintsToChrome_ = function(c) { + if (typeof c !== 'object' || c.mandatory || c.optional) { + return c; + } + const cc = {}; + Object.keys(c).forEach(key => { + if (key === 'require' || key === 'advanced' || key === 'mediaSource') { + return; + } + const r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]}; + if (r.exact !== undefined && typeof r.exact === 'number') { + r.min = r.max = r.exact; + } + const oldname_ = function(prefix, name) { + if (prefix) { + return prefix + name.charAt(0).toUpperCase() + name.slice(1); + } + return (name === 'deviceId') ? 'sourceId' : name; + }; + if (r.ideal !== undefined) { + cc.optional = cc.optional || []; + let oc = {}; + if (typeof r.ideal === 'number') { + oc[oldname_('min', key)] = r.ideal; + cc.optional.push(oc); + oc = {}; + oc[oldname_('max', key)] = r.ideal; + cc.optional.push(oc); + } else { + oc[oldname_('', key)] = r.ideal; + cc.optional.push(oc); + } + } + if (r.exact !== undefined && typeof r.exact !== 'number') { + cc.mandatory = cc.mandatory || {}; + cc.mandatory[oldname_('', key)] = r.exact; + } else { + ['min', 'max'].forEach(mix => { + if (r[mix] !== undefined) { + cc.mandatory = cc.mandatory || {}; + cc.mandatory[oldname_(mix, key)] = r[mix]; + } + }); + } + }); + if (c.advanced) { + cc.optional = (cc.optional || []).concat(c.advanced); + } + return cc; + }; + + const shimConstraints_ = function(constraints, func) { + if (browserDetails.version >= 61) { + return func(constraints); + } + constraints = JSON.parse(JSON.stringify(constraints)); + if (constraints && typeof constraints.audio === 'object') { + const remap = function(obj, a, b) { + if (a in obj && !(b in obj)) { + obj[b] = obj[a]; + delete obj[a]; + } + }; + constraints = JSON.parse(JSON.stringify(constraints)); + remap(constraints.audio, 'autoGainControl', 'googAutoGainControl'); + remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression'); + constraints.audio = constraintsToChrome_(constraints.audio); + } + if (constraints && typeof constraints.video === 'object') { + // Shim facingMode for mobile & surface pro. + let face = constraints.video.facingMode; + face = face && ((typeof face === 'object') ? face : {ideal: face}); + const getSupportedFacingModeLies = browserDetails.version < 66; + + if ((face && (face.exact === 'user' || face.exact === 'environment' || + face.ideal === 'user' || face.ideal === 'environment')) && + !(navigator.mediaDevices.getSupportedConstraints && + navigator.mediaDevices.getSupportedConstraints().facingMode && + !getSupportedFacingModeLies)) { + delete constraints.video.facingMode; + let matches; + if (face.exact === 'environment' || face.ideal === 'environment') { + matches = ['back', 'rear']; + } else if (face.exact === 'user' || face.ideal === 'user') { + matches = ['front']; + } + if (matches) { + // Look for matches in label, or use last cam for back (typical). + return navigator.mediaDevices.enumerateDevices() + .then(devices => { + devices = devices.filter(d => d.kind === 'videoinput'); + let dev = devices.find(d => matches.some(match => + d.label.toLowerCase().includes(match))); + if (!dev && devices.length && matches.includes('back')) { + dev = devices[devices.length - 1]; // more likely the back cam + } + if (dev) { + constraints.video.deviceId = face.exact ? {exact: dev.deviceId} : + {ideal: dev.deviceId}; + } + constraints.video = constraintsToChrome_(constraints.video); + logging('chrome: ' + JSON.stringify(constraints)); + return func(constraints); + }); + } + } + constraints.video = constraintsToChrome_(constraints.video); + } + logging('chrome: ' + JSON.stringify(constraints)); + return func(constraints); + }; + + const shimError_ = function(e) { + if (browserDetails.version >= 64) { + return e; + } + return { + name: { + PermissionDeniedError: 'NotAllowedError', + PermissionDismissedError: 'NotAllowedError', + InvalidStateError: 'NotAllowedError', + DevicesNotFoundError: 'NotFoundError', + ConstraintNotSatisfiedError: 'OverconstrainedError', + TrackStartError: 'NotReadableError', + MediaDeviceFailedDueToShutdown: 'NotAllowedError', + MediaDeviceKillSwitchOn: 'NotAllowedError', + TabCaptureError: 'AbortError', + ScreenCaptureError: 'AbortError', + DeviceCaptureError: 'AbortError' + }[e.name] || e.name, + message: e.message, + constraint: e.constraint || e.constraintName, + toString() { + return this.name + (this.message && ': ') + this.message; + } + }; + }; + + const getUserMedia_ = function(constraints, onSuccess, onError) { + shimConstraints_(constraints, c => { + navigator.webkitGetUserMedia(c, onSuccess, e => { + if (onError) { + onError(shimError_(e)); + } + }); + }); + }; + navigator.getUserMedia = getUserMedia_.bind(navigator); + + // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia + // function which returns a Promise, it does not accept spec-style + // constraints. + if (navigator.mediaDevices.getUserMedia) { + const origGetUserMedia = navigator.mediaDevices.getUserMedia. + bind(navigator.mediaDevices); + navigator.mediaDevices.getUserMedia = function(cs) { + return shimConstraints_(cs, c => origGetUserMedia(c).then(stream => { + if (c.audio && !stream.getAudioTracks().length || + c.video && !stream.getVideoTracks().length) { + stream.getTracks().forEach(track => { + track.stop(); + }); + throw new DOMException('', 'NotFoundError'); + } + return stream; + }, e => Promise.reject(shimError_(e)))); + }; + } + } + + /* + * Copyright (c) 2018 The adapter.js project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + function shimGetDisplayMedia$2(window, getSourceId) { + if (window.navigator.mediaDevices && + 'getDisplayMedia' in window.navigator.mediaDevices) { + return; + } + if (!(window.navigator.mediaDevices)) { + return; + } + // getSourceId is a function that returns a promise resolving with + // the sourceId of the screen/window/tab to be shared. + if (typeof getSourceId !== 'function') { + console.error('shimGetDisplayMedia: getSourceId argument is not ' + + 'a function'); + return; + } + window.navigator.mediaDevices.getDisplayMedia = + function getDisplayMedia(constraints) { + return getSourceId(constraints) + .then(sourceId => { + const widthSpecified = constraints.video && constraints.video.width; + const heightSpecified = constraints.video && + constraints.video.height; + const frameRateSpecified = constraints.video && + constraints.video.frameRate; + constraints.video = { + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: sourceId, + maxFrameRate: frameRateSpecified || 3 + } + }; + if (widthSpecified) { + constraints.video.mandatory.maxWidth = widthSpecified; + } + if (heightSpecified) { + constraints.video.mandatory.maxHeight = heightSpecified; + } + return window.navigator.mediaDevices.getUserMedia(constraints); + }); + }; + } + + /* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + + function shimMediaStream(window) { + window.MediaStream = window.MediaStream || window.webkitMediaStream; + } + + function shimOnTrack$1(window) { + if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in + window.RTCPeerConnection.prototype)) { + Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { + get() { + return this._ontrack; + }, + set(f) { + if (this._ontrack) { + this.removeEventListener('track', this._ontrack); + } + this.addEventListener('track', this._ontrack = f); + }, + enumerable: true, + configurable: true + }); + const origSetRemoteDescription = + window.RTCPeerConnection.prototype.setRemoteDescription; + window.RTCPeerConnection.prototype.setRemoteDescription = + function setRemoteDescription() { + if (!this._ontrackpoly) { + this._ontrackpoly = (e) => { + // onaddstream does not fire when a track is added to an existing + // stream. But stream.onaddtrack is implemented so we use that. + e.stream.addEventListener('addtrack', te => { + let receiver; + if (window.RTCPeerConnection.prototype.getReceivers) { + receiver = this.getReceivers() + .find(r => r.track && r.track.id === te.track.id); + } else { + receiver = {track: te.track}; + } + + const event = new Event('track'); + event.track = te.track; + event.receiver = receiver; + event.transceiver = {receiver}; + event.streams = [e.stream]; + this.dispatchEvent(event); + }); + e.stream.getTracks().forEach(track => { + let receiver; + if (window.RTCPeerConnection.prototype.getReceivers) { + receiver = this.getReceivers() + .find(r => r.track && r.track.id === track.id); + } else { + receiver = {track}; + } + const event = new Event('track'); + event.track = track; + event.receiver = receiver; + event.transceiver = {receiver}; + event.streams = [e.stream]; + this.dispatchEvent(event); + }); + }; + this.addEventListener('addstream', this._ontrackpoly); + } + return origSetRemoteDescription.apply(this, arguments); + }; + } else { + // even if RTCRtpTransceiver is in window, it is only used and + // emitted in unified-plan. Unfortunately this means we need + // to unconditionally wrap the event. + wrapPeerConnectionEvent(window, 'track', e => { + if (!e.transceiver) { + Object.defineProperty(e, 'transceiver', + {value: {receiver: e.receiver}}); + } + return e; + }); + } + } + + function shimGetSendersWithDtmf(window) { + // Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack. + if (typeof window === 'object' && window.RTCPeerConnection && + !('getSenders' in window.RTCPeerConnection.prototype) && + 'createDTMFSender' in window.RTCPeerConnection.prototype) { + const shimSenderWithDtmf = function(pc, track) { + return { + track, + get dtmf() { + if (this._dtmf === undefined) { + if (track.kind === 'audio') { + this._dtmf = pc.createDTMFSender(track); + } else { + this._dtmf = null; + } + } + return this._dtmf; + }, + _pc: pc + }; + }; + + // augment addTrack when getSenders is not available. + if (!window.RTCPeerConnection.prototype.getSenders) { + window.RTCPeerConnection.prototype.getSenders = function getSenders() { + this._senders = this._senders || []; + return this._senders.slice(); // return a copy of the internal state. + }; + const origAddTrack = window.RTCPeerConnection.prototype.addTrack; + window.RTCPeerConnection.prototype.addTrack = + function addTrack(track, stream) { + let sender = origAddTrack.apply(this, arguments); + if (!sender) { + sender = shimSenderWithDtmf(this, track); + this._senders.push(sender); + } + return sender; + }; + + const origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; + window.RTCPeerConnection.prototype.removeTrack = + function removeTrack(sender) { + origRemoveTrack.apply(this, arguments); + const idx = this._senders.indexOf(sender); + if (idx !== -1) { + this._senders.splice(idx, 1); + } + }; + } + const origAddStream = window.RTCPeerConnection.prototype.addStream; + window.RTCPeerConnection.prototype.addStream = function addStream(stream) { + this._senders = this._senders || []; + origAddStream.apply(this, [stream]); + stream.getTracks().forEach(track => { + this._senders.push(shimSenderWithDtmf(this, track)); + }); + }; + + const origRemoveStream = window.RTCPeerConnection.prototype.removeStream; + window.RTCPeerConnection.prototype.removeStream = + function removeStream(stream) { + this._senders = this._senders || []; + origRemoveStream.apply(this, [stream]); + + stream.getTracks().forEach(track => { + const sender = this._senders.find(s => s.track === track); + if (sender) { // remove sender + this._senders.splice(this._senders.indexOf(sender), 1); + } + }); + }; + } else if (typeof window === 'object' && window.RTCPeerConnection && + 'getSenders' in window.RTCPeerConnection.prototype && + 'createDTMFSender' in window.RTCPeerConnection.prototype && + window.RTCRtpSender && + !('dtmf' in window.RTCRtpSender.prototype)) { + const origGetSenders = window.RTCPeerConnection.prototype.getSenders; + window.RTCPeerConnection.prototype.getSenders = function getSenders() { + const senders = origGetSenders.apply(this, []); + senders.forEach(sender => sender._pc = this); + return senders; + }; + + Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', { + get() { + if (this._dtmf === undefined) { + if (this.track.kind === 'audio') { + this._dtmf = this._pc.createDTMFSender(this.track); + } else { + this._dtmf = null; + } + } + return this._dtmf; + } + }); + } + } + + function shimGetStats(window) { + if (!window.RTCPeerConnection) { + return; + } + + const origGetStats = window.RTCPeerConnection.prototype.getStats; + window.RTCPeerConnection.prototype.getStats = function getStats() { + const [selector, onSucc, onErr] = arguments; + + // If selector is a function then we are in the old style stats so just + // pass back the original getStats format to avoid breaking old users. + if (arguments.length > 0 && typeof selector === 'function') { + return origGetStats.apply(this, arguments); + } + + // When spec-style getStats is supported, return those when called with + // either no arguments or the selector argument is null. + if (origGetStats.length === 0 && (arguments.length === 0 || + typeof selector !== 'function')) { + return origGetStats.apply(this, []); + } + + const fixChromeStats_ = function(response) { + const standardReport = {}; + const reports = response.result(); + reports.forEach(report => { + const standardStats = { + id: report.id, + timestamp: report.timestamp, + type: { + localcandidate: 'local-candidate', + remotecandidate: 'remote-candidate' + }[report.type] || report.type + }; + report.names().forEach(name => { + standardStats[name] = report.stat(name); + }); + standardReport[standardStats.id] = standardStats; + }); + + return standardReport; + }; + + // shim getStats with maplike support + const makeMapStats = function(stats) { + return new Map(Object.keys(stats).map(key => [key, stats[key]])); + }; + + if (arguments.length >= 2) { + const successCallbackWrapper_ = function(response) { + onSucc(makeMapStats(fixChromeStats_(response))); + }; + + return origGetStats.apply(this, [successCallbackWrapper_, + selector]); + } + + // promise-support + return new Promise((resolve, reject) => { + origGetStats.apply(this, [ + function(response) { + resolve(makeMapStats(fixChromeStats_(response))); + }, reject]); + }).then(onSucc, onErr); + }; + } + + function shimSenderReceiverGetStats(window) { + if (!(typeof window === 'object' && window.RTCPeerConnection && + window.RTCRtpSender && window.RTCRtpReceiver)) { + return; + } + + // shim sender stats. + if (!('getStats' in window.RTCRtpSender.prototype)) { + const origGetSenders = window.RTCPeerConnection.prototype.getSenders; + if (origGetSenders) { + window.RTCPeerConnection.prototype.getSenders = function getSenders() { + const senders = origGetSenders.apply(this, []); + senders.forEach(sender => sender._pc = this); + return senders; + }; + } + + const origAddTrack = window.RTCPeerConnection.prototype.addTrack; + if (origAddTrack) { + window.RTCPeerConnection.prototype.addTrack = function addTrack() { + const sender = origAddTrack.apply(this, arguments); + sender._pc = this; + return sender; + }; + } + window.RTCRtpSender.prototype.getStats = function getStats() { + const sender = this; + return this._pc.getStats().then(result => + /* Note: this will include stats of all senders that + * send a track with the same id as sender.track as + * it is not possible to identify the RTCRtpSender. + */ + filterStats(result, sender.track, true)); + }; + } + + // shim receiver stats. + if (!('getStats' in window.RTCRtpReceiver.prototype)) { + const origGetReceivers = window.RTCPeerConnection.prototype.getReceivers; + if (origGetReceivers) { + window.RTCPeerConnection.prototype.getReceivers = + function getReceivers() { + const receivers = origGetReceivers.apply(this, []); + receivers.forEach(receiver => receiver._pc = this); + return receivers; + }; + } + wrapPeerConnectionEvent(window, 'track', e => { + e.receiver._pc = e.srcElement; + return e; + }); + window.RTCRtpReceiver.prototype.getStats = function getStats() { + const receiver = this; + return this._pc.getStats().then(result => + filterStats(result, receiver.track, false)); + }; + } + + if (!('getStats' in window.RTCRtpSender.prototype && + 'getStats' in window.RTCRtpReceiver.prototype)) { + return; + } + + // shim RTCPeerConnection.getStats(track). + const origGetStats = window.RTCPeerConnection.prototype.getStats; + window.RTCPeerConnection.prototype.getStats = function getStats() { + if (arguments.length > 0 && + arguments[0] instanceof window.MediaStreamTrack) { + const track = arguments[0]; + let sender; + let receiver; + let err; + this.getSenders().forEach(s => { + if (s.track === track) { + if (sender) { + err = true; + } else { + sender = s; + } + } + }); + this.getReceivers().forEach(r => { + if (r.track === track) { + if (receiver) { + err = true; + } else { + receiver = r; + } + } + return r.track === track; + }); + if (err || (sender && receiver)) { + return Promise.reject(new DOMException( + 'There are more than one sender or receiver for the track.', + 'InvalidAccessError')); + } else if (sender) { + return sender.getStats(); + } else if (receiver) { + return receiver.getStats(); + } + return Promise.reject(new DOMException( + 'There is no sender or receiver for the track.', + 'InvalidAccessError')); + } + return origGetStats.apply(this, arguments); + }; + } + + function shimAddTrackRemoveTrackWithNative(window) { + // shim addTrack/removeTrack with native variants in order to make + // the interactions with legacy getLocalStreams behave as in other browsers. + // Keeps a mapping stream.id => [stream, rtpsenders...] + window.RTCPeerConnection.prototype.getLocalStreams = + function getLocalStreams() { + this._shimmedLocalStreams = this._shimmedLocalStreams || {}; + return Object.keys(this._shimmedLocalStreams) + .map(streamId => this._shimmedLocalStreams[streamId][0]); + }; + + const origAddTrack = window.RTCPeerConnection.prototype.addTrack; + window.RTCPeerConnection.prototype.addTrack = + function addTrack(track, stream) { + if (!stream) { + return origAddTrack.apply(this, arguments); + } + this._shimmedLocalStreams = this._shimmedLocalStreams || {}; + + const sender = origAddTrack.apply(this, arguments); + if (!this._shimmedLocalStreams[stream.id]) { + this._shimmedLocalStreams[stream.id] = [stream, sender]; + } else if (this._shimmedLocalStreams[stream.id].indexOf(sender) === -1) { + this._shimmedLocalStreams[stream.id].push(sender); + } + return sender; + }; + + const origAddStream = window.RTCPeerConnection.prototype.addStream; + window.RTCPeerConnection.prototype.addStream = function addStream(stream) { + this._shimmedLocalStreams = this._shimmedLocalStreams || {}; + + stream.getTracks().forEach(track => { + const alreadyExists = this.getSenders().find(s => s.track === track); + if (alreadyExists) { + throw new DOMException('Track already exists.', + 'InvalidAccessError'); + } + }); + const existingSenders = this.getSenders(); + origAddStream.apply(this, arguments); + const newSenders = this.getSenders() + .filter(newSender => existingSenders.indexOf(newSender) === -1); + this._shimmedLocalStreams[stream.id] = [stream].concat(newSenders); + }; + + const origRemoveStream = window.RTCPeerConnection.prototype.removeStream; + window.RTCPeerConnection.prototype.removeStream = + function removeStream(stream) { + this._shimmedLocalStreams = this._shimmedLocalStreams || {}; + delete this._shimmedLocalStreams[stream.id]; + return origRemoveStream.apply(this, arguments); + }; + + const origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; + window.RTCPeerConnection.prototype.removeTrack = + function removeTrack(sender) { + this._shimmedLocalStreams = this._shimmedLocalStreams || {}; + if (sender) { + Object.keys(this._shimmedLocalStreams).forEach(streamId => { + const idx = this._shimmedLocalStreams[streamId].indexOf(sender); + if (idx !== -1) { + this._shimmedLocalStreams[streamId].splice(idx, 1); + } + if (this._shimmedLocalStreams[streamId].length === 1) { + delete this._shimmedLocalStreams[streamId]; + } + }); + } + return origRemoveTrack.apply(this, arguments); + }; + } + + function shimAddTrackRemoveTrack(window, browserDetails) { + if (!window.RTCPeerConnection) { + return; + } + // shim addTrack and removeTrack. + if (window.RTCPeerConnection.prototype.addTrack && + browserDetails.version >= 65) { + return shimAddTrackRemoveTrackWithNative(window); + } + + // also shim pc.getLocalStreams when addTrack is shimmed + // to return the original streams. + const origGetLocalStreams = window.RTCPeerConnection.prototype + .getLocalStreams; + window.RTCPeerConnection.prototype.getLocalStreams = + function getLocalStreams() { + const nativeStreams = origGetLocalStreams.apply(this); + this._reverseStreams = this._reverseStreams || {}; + return nativeStreams.map(stream => this._reverseStreams[stream.id]); + }; + + const origAddStream = window.RTCPeerConnection.prototype.addStream; + window.RTCPeerConnection.prototype.addStream = function addStream(stream) { + this._streams = this._streams || {}; + this._reverseStreams = this._reverseStreams || {}; + + stream.getTracks().forEach(track => { + const alreadyExists = this.getSenders().find(s => s.track === track); + if (alreadyExists) { + throw new DOMException('Track already exists.', + 'InvalidAccessError'); + } + }); + // Add identity mapping for consistency with addTrack. + // Unless this is being used with a stream from addTrack. + if (!this._reverseStreams[stream.id]) { + const newStream = new window.MediaStream(stream.getTracks()); + this._streams[stream.id] = newStream; + this._reverseStreams[newStream.id] = stream; + stream = newStream; + } + origAddStream.apply(this, [stream]); + }; + + const origRemoveStream = window.RTCPeerConnection.prototype.removeStream; + window.RTCPeerConnection.prototype.removeStream = + function removeStream(stream) { + this._streams = this._streams || {}; + this._reverseStreams = this._reverseStreams || {}; + + origRemoveStream.apply(this, [(this._streams[stream.id] || stream)]); + delete this._reverseStreams[(this._streams[stream.id] ? + this._streams[stream.id].id : stream.id)]; + delete this._streams[stream.id]; + }; + + window.RTCPeerConnection.prototype.addTrack = + function addTrack(track, stream) { + if (this.signalingState === 'closed') { + throw new DOMException( + 'The RTCPeerConnection\'s signalingState is \'closed\'.', + 'InvalidStateError'); + } + const streams = [].slice.call(arguments, 1); + if (streams.length !== 1 || + !streams[0].getTracks().find(t => t === track)) { + // this is not fully correct but all we can manage without + // [[associated MediaStreams]] internal slot. + throw new DOMException( + 'The adapter.js addTrack polyfill only supports a single ' + + ' stream which is associated with the specified track.', + 'NotSupportedError'); + } + + const alreadyExists = this.getSenders().find(s => s.track === track); + if (alreadyExists) { + throw new DOMException('Track already exists.', + 'InvalidAccessError'); + } + + this._streams = this._streams || {}; + this._reverseStreams = this._reverseStreams || {}; + const oldStream = this._streams[stream.id]; + if (oldStream) { + // this is using odd Chrome behaviour, use with caution: + // https://bugs.chromium.org/p/webrtc/issues/detail?id=7815 + // Note: we rely on the high-level addTrack/dtmf shim to + // create the sender with a dtmf sender. + oldStream.addTrack(track); + + // Trigger ONN async. + Promise.resolve().then(() => { + this.dispatchEvent(new Event('negotiationneeded')); + }); + } else { + const newStream = new window.MediaStream([track]); + this._streams[stream.id] = newStream; + this._reverseStreams[newStream.id] = stream; + this.addStream(newStream); + } + return this.getSenders().find(s => s.track === track); + }; + + // replace the internal stream id with the external one and + // vice versa. + function replaceInternalStreamId(pc, description) { + let sdp = description.sdp; + Object.keys(pc._reverseStreams || []).forEach(internalId => { + const externalStream = pc._reverseStreams[internalId]; + const internalStream = pc._streams[externalStream.id]; + sdp = sdp.replace(new RegExp(internalStream.id, 'g'), + externalStream.id); + }); + return new RTCSessionDescription({ + type: description.type, + sdp + }); + } + function replaceExternalStreamId(pc, description) { + let sdp = description.sdp; + Object.keys(pc._reverseStreams || []).forEach(internalId => { + const externalStream = pc._reverseStreams[internalId]; + const internalStream = pc._streams[externalStream.id]; + sdp = sdp.replace(new RegExp(externalStream.id, 'g'), + internalStream.id); + }); + return new RTCSessionDescription({ + type: description.type, + sdp + }); + } + ['createOffer', 'createAnswer'].forEach(function(method) { + const nativeMethod = window.RTCPeerConnection.prototype[method]; + const methodObj = {[method]() { + const args = arguments; + const isLegacyCall = arguments.length && + typeof arguments[0] === 'function'; + if (isLegacyCall) { + return nativeMethod.apply(this, [ + (description) => { + const desc = replaceInternalStreamId(this, description); + args[0].apply(null, [desc]); + }, + (err) => { + if (args[1]) { + args[1].apply(null, err); + } + }, arguments[2] + ]); + } + return nativeMethod.apply(this, arguments) + .then(description => replaceInternalStreamId(this, description)); + }}; + window.RTCPeerConnection.prototype[method] = methodObj[method]; + }); + + const origSetLocalDescription = + window.RTCPeerConnection.prototype.setLocalDescription; + window.RTCPeerConnection.prototype.setLocalDescription = + function setLocalDescription() { + if (!arguments.length || !arguments[0].type) { + return origSetLocalDescription.apply(this, arguments); + } + arguments[0] = replaceExternalStreamId(this, arguments[0]); + return origSetLocalDescription.apply(this, arguments); + }; + + // TODO: mangle getStats: https://w3c.github.io/webrtc-stats/#dom-rtcmediastreamstats-streamidentifier + + const origLocalDescription = Object.getOwnPropertyDescriptor( + window.RTCPeerConnection.prototype, 'localDescription'); + Object.defineProperty(window.RTCPeerConnection.prototype, + 'localDescription', { + get() { + const description = origLocalDescription.get.apply(this); + if (description.type === '') { + return description; + } + return replaceInternalStreamId(this, description); + } + }); + + window.RTCPeerConnection.prototype.removeTrack = + function removeTrack(sender) { + if (this.signalingState === 'closed') { + throw new DOMException( + 'The RTCPeerConnection\'s signalingState is \'closed\'.', + 'InvalidStateError'); + } + // We can not yet check for sender instanceof RTCRtpSender + // since we shim RTPSender. So we check if sender._pc is set. + if (!sender._pc) { + throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' + + 'does not implement interface RTCRtpSender.', 'TypeError'); + } + const isLocal = sender._pc === this; + if (!isLocal) { + throw new DOMException('Sender was not created by this connection.', + 'InvalidAccessError'); + } + + // Search for the native stream the senders track belongs to. + this._streams = this._streams || {}; + let stream; + Object.keys(this._streams).forEach(streamid => { + const hasTrack = this._streams[streamid].getTracks() + .find(track => sender.track === track); + if (hasTrack) { + stream = this._streams[streamid]; + } + }); + + if (stream) { + if (stream.getTracks().length === 1) { + // if this is the last track of the stream, remove the stream. This + // takes care of any shimmed _senders. + this.removeStream(this._reverseStreams[stream.id]); + } else { + // relying on the same odd chrome behaviour as above. + stream.removeTrack(sender.track); + } + this.dispatchEvent(new Event('negotiationneeded')); + } + }; + } + + function shimPeerConnection$2(window, browserDetails) { + if (!window.RTCPeerConnection && window.webkitRTCPeerConnection) { + // very basic support for old versions. + window.RTCPeerConnection = window.webkitRTCPeerConnection; + } + if (!window.RTCPeerConnection) { + return; + } + + // shim implicit creation of RTCSessionDescription/RTCIceCandidate + if (browserDetails.version < 53) { + ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] + .forEach(function(method) { + const nativeMethod = window.RTCPeerConnection.prototype[method]; + const methodObj = {[method]() { + arguments[0] = new ((method === 'addIceCandidate') ? + window.RTCIceCandidate : + window.RTCSessionDescription)(arguments[0]); + return nativeMethod.apply(this, arguments); + }}; + window.RTCPeerConnection.prototype[method] = methodObj[method]; + }); + } + } + + // Attempt to fix ONN in plan-b mode. + function fixNegotiationNeeded(window, browserDetails) { + wrapPeerConnectionEvent(window, 'negotiationneeded', e => { + const pc = e.target; + if (browserDetails.version < 72 || (pc.getConfiguration && + pc.getConfiguration().sdpSemantics === 'plan-b')) { + if (pc.signalingState !== 'stable') { + return; + } + } + return e; + }); + } + + var chromeShim = /*#__PURE__*/Object.freeze({ + __proto__: null, + shimMediaStream: shimMediaStream, + shimOnTrack: shimOnTrack$1, + shimGetSendersWithDtmf: shimGetSendersWithDtmf, + shimGetStats: shimGetStats, + shimSenderReceiverGetStats: shimSenderReceiverGetStats, + shimAddTrackRemoveTrackWithNative: shimAddTrackRemoveTrackWithNative, + shimAddTrackRemoveTrack: shimAddTrackRemoveTrack, + shimPeerConnection: shimPeerConnection$2, + fixNegotiationNeeded: fixNegotiationNeeded, + shimGetUserMedia: shimGetUserMedia$3, + shimGetDisplayMedia: shimGetDisplayMedia$2 + }); + + /* + * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + // Edge does not like + // 1) stun: filtered after 14393 unless ?transport=udp is present + // 2) turn: that does not have all of turn:host:port?transport=udp + // 3) turn: with ipv6 addresses + // 4) turn: occurring muliple times + function filterIceServers$1(iceServers, edgeVersion) { + let hasTurn = false; + iceServers = JSON.parse(JSON.stringify(iceServers)); + return iceServers.filter(server => { + if (server && (server.urls || server.url)) { + let urls = server.urls || server.url; + if (server.url && !server.urls) { + deprecated('RTCIceServer.url', 'RTCIceServer.urls'); + } + const isString = typeof urls === 'string'; + if (isString) { + urls = [urls]; + } + urls = urls.filter(url => { + // filter STUN unconditionally. + if (url.indexOf('stun:') === 0) { + return false; + } + + const validTurn = url.startsWith('turn') && + !url.startsWith('turn:[') && + url.includes('transport=udp'); + if (validTurn && !hasTurn) { + hasTurn = true; + return true; + } + return validTurn && !hasTurn; + }); + + delete server.url; + server.urls = isString ? urls[0] : urls; + return !!urls.length; + } + }); + } + + function createCommonjsModule(fn) { + var module = { exports: {} }; + return fn(module, module.exports), module.exports; + } + + /* eslint-env node */ + + var sdp = createCommonjsModule(function (module) { + + // SDP helpers. + var SDPUtils = {}; + + // Generate an alphanumeric identifier for cname or mids. + // TODO: use UUIDs instead? https://gist.github.com/jed/982883 + SDPUtils.generateIdentifier = function() { + return Math.random().toString(36).substr(2, 10); + }; + + // The RTCP CNAME used by all peerconnections from the same JS. + SDPUtils.localCName = SDPUtils.generateIdentifier(); + + // Splits SDP into lines, dealing with both CRLF and LF. + SDPUtils.splitLines = function(blob) { + return blob.trim().split('\n').map(function(line) { + return line.trim(); + }); + }; + // Splits SDP into sessionpart and mediasections. Ensures CRLF. + SDPUtils.splitSections = function(blob) { + var parts = blob.split('\nm='); + return parts.map(function(part, index) { + return (index > 0 ? 'm=' + part : part).trim() + '\r\n'; + }); + }; + + // returns the session description. + SDPUtils.getDescription = function(blob) { + var sections = SDPUtils.splitSections(blob); + return sections && sections[0]; + }; + + // returns the individual media sections. + SDPUtils.getMediaSections = function(blob) { + var sections = SDPUtils.splitSections(blob); + sections.shift(); + return sections; + }; + + // Returns lines that start with a certain prefix. + SDPUtils.matchPrefix = function(blob, prefix) { + return SDPUtils.splitLines(blob).filter(function(line) { + return line.indexOf(prefix) === 0; + }); + }; + + // Parses an ICE candidate line. Sample input: + // candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8 + // rport 55996" + SDPUtils.parseCandidate = function(line) { + var parts; + // Parse both variants. + if (line.indexOf('a=candidate:') === 0) { + parts = line.substring(12).split(' '); + } else { + parts = line.substring(10).split(' '); + } + + var candidate = { + foundation: parts[0], + component: parseInt(parts[1], 10), + protocol: parts[2].toLowerCase(), + priority: parseInt(parts[3], 10), + ip: parts[4], + address: parts[4], // address is an alias for ip. + port: parseInt(parts[5], 10), + // skip parts[6] == 'typ' + type: parts[7] + }; + + for (var i = 8; i < parts.length; i += 2) { + switch (parts[i]) { + case 'raddr': + candidate.relatedAddress = parts[i + 1]; + break; + case 'rport': + candidate.relatedPort = parseInt(parts[i + 1], 10); + break; + case 'tcptype': + candidate.tcpType = parts[i + 1]; + break; + case 'ufrag': + candidate.ufrag = parts[i + 1]; // for backward compability. + candidate.usernameFragment = parts[i + 1]; + break; + default: // extension handling, in particular ufrag + candidate[parts[i]] = parts[i + 1]; + break; + } + } + return candidate; + }; + + // Translates a candidate object into SDP candidate attribute. + SDPUtils.writeCandidate = function(candidate) { + var sdp = []; + sdp.push(candidate.foundation); + sdp.push(candidate.component); + sdp.push(candidate.protocol.toUpperCase()); + sdp.push(candidate.priority); + sdp.push(candidate.address || candidate.ip); + sdp.push(candidate.port); + + var type = candidate.type; + sdp.push('typ'); + sdp.push(type); + if (type !== 'host' && candidate.relatedAddress && + candidate.relatedPort) { + sdp.push('raddr'); + sdp.push(candidate.relatedAddress); + sdp.push('rport'); + sdp.push(candidate.relatedPort); + } + if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') { + sdp.push('tcptype'); + sdp.push(candidate.tcpType); + } + if (candidate.usernameFragment || candidate.ufrag) { + sdp.push('ufrag'); + sdp.push(candidate.usernameFragment || candidate.ufrag); + } + return 'candidate:' + sdp.join(' '); + }; + + // Parses an ice-options line, returns an array of option tags. + // a=ice-options:foo bar + SDPUtils.parseIceOptions = function(line) { + return line.substr(14).split(' '); + }; + + // Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input: + // a=rtpmap:111 opus/48000/2 + SDPUtils.parseRtpMap = function(line) { + var parts = line.substr(9).split(' '); + var parsed = { + payloadType: parseInt(parts.shift(), 10) // was: id + }; + + parts = parts[0].split('/'); + + parsed.name = parts[0]; + parsed.clockRate = parseInt(parts[1], 10); // was: clockrate + parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1; + // legacy alias, got renamed back to channels in ORTC. + parsed.numChannels = parsed.channels; + return parsed; + }; + + // Generate an a=rtpmap line from RTCRtpCodecCapability or + // RTCRtpCodecParameters. + SDPUtils.writeRtpMap = function(codec) { + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + var channels = codec.channels || codec.numChannels || 1; + return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + + (channels !== 1 ? '/' + channels : '') + '\r\n'; + }; + + // Parses an a=extmap line (headerextension from RFC 5285). Sample input: + // a=extmap:2 urn:ietf:params:rtp-hdrext:toffset + // a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset + SDPUtils.parseExtmap = function(line) { + var parts = line.substr(9).split(' '); + return { + id: parseInt(parts[0], 10), + direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv', + uri: parts[1] + }; + }; + + // Generates a=extmap line from RTCRtpHeaderExtensionParameters or + // RTCRtpHeaderExtension. + SDPUtils.writeExtmap = function(headerExtension) { + return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) + + (headerExtension.direction && headerExtension.direction !== 'sendrecv' + ? '/' + headerExtension.direction + : '') + + ' ' + headerExtension.uri + '\r\n'; + }; + + // Parses an ftmp line, returns dictionary. Sample input: + // a=fmtp:96 vbr=on;cng=on + // Also deals with vbr=on; cng=on + SDPUtils.parseFmtp = function(line) { + var parsed = {}; + var kv; + var parts = line.substr(line.indexOf(' ') + 1).split(';'); + for (var j = 0; j < parts.length; j++) { + kv = parts[j].trim().split('='); + parsed[kv[0].trim()] = kv[1]; + } + return parsed; + }; + + // Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters. + SDPUtils.writeFmtp = function(codec) { + var line = ''; + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + if (codec.parameters && Object.keys(codec.parameters).length) { + var params = []; + Object.keys(codec.parameters).forEach(function(param) { + if (codec.parameters[param]) { + params.push(param + '=' + codec.parameters[param]); + } else { + params.push(param); + } + }); + line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n'; + } + return line; + }; + + // Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: + // a=rtcp-fb:98 nack rpsi + SDPUtils.parseRtcpFb = function(line) { + var parts = line.substr(line.indexOf(' ') + 1).split(' '); + return { + type: parts.shift(), + parameter: parts.join(' ') + }; + }; + // Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters. + SDPUtils.writeRtcpFb = function(codec) { + var lines = ''; + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + if (codec.rtcpFeedback && codec.rtcpFeedback.length) { + // FIXME: special handling for trr-int? + codec.rtcpFeedback.forEach(function(fb) { + lines += 'a=rtcp-fb:' + pt + ' ' + fb.type + + (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') + + '\r\n'; + }); + } + return lines; + }; + + // Parses an RFC 5576 ssrc media attribute. Sample input: + // a=ssrc:3735928559 cname:something + SDPUtils.parseSsrcMedia = function(line) { + var sp = line.indexOf(' '); + var parts = { + ssrc: parseInt(line.substr(7, sp - 7), 10) + }; + var colon = line.indexOf(':', sp); + if (colon > -1) { + parts.attribute = line.substr(sp + 1, colon - sp - 1); + parts.value = line.substr(colon + 1); + } else { + parts.attribute = line.substr(sp + 1); + } + return parts; + }; + + SDPUtils.parseSsrcGroup = function(line) { + var parts = line.substr(13).split(' '); + return { + semantics: parts.shift(), + ssrcs: parts.map(function(ssrc) { + return parseInt(ssrc, 10); + }) + }; + }; + + // Extracts the MID (RFC 5888) from a media section. + // returns the MID or undefined if no mid line was found. + SDPUtils.getMid = function(mediaSection) { + var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0]; + if (mid) { + return mid.substr(6); + } + }; + + SDPUtils.parseFingerprint = function(line) { + var parts = line.substr(14).split(' '); + return { + algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge. + value: parts[1] + }; + }; + + // Extracts DTLS parameters from SDP media section or sessionpart. + // FIXME: for consistency with other functions this should only + // get the fingerprint line as input. See also getIceParameters. + SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) { + var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, + 'a=fingerprint:'); + // Note: a=setup line is ignored since we use the 'auto' role. + // Note2: 'algorithm' is not case sensitive except in Edge. + return { + role: 'auto', + fingerprints: lines.map(SDPUtils.parseFingerprint) + }; + }; + + // Serializes DTLS parameters to SDP. + SDPUtils.writeDtlsParameters = function(params, setupType) { + var sdp = 'a=setup:' + setupType + '\r\n'; + params.fingerprints.forEach(function(fp) { + sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n'; + }); + return sdp; + }; + + // Parses a=crypto lines into + // https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#dictionary-rtcsrtpsdesparameters-members + SDPUtils.parseCryptoLine = function(line) { + var parts = line.substr(9).split(' '); + return { + tag: parseInt(parts[0], 10), + cryptoSuite: parts[1], + keyParams: parts[2], + sessionParams: parts.slice(3), + }; + }; + + SDPUtils.writeCryptoLine = function(parameters) { + return 'a=crypto:' + parameters.tag + ' ' + + parameters.cryptoSuite + ' ' + + (typeof parameters.keyParams === 'object' + ? SDPUtils.writeCryptoKeyParams(parameters.keyParams) + : parameters.keyParams) + + (parameters.sessionParams ? ' ' + parameters.sessionParams.join(' ') : '') + + '\r\n'; + }; + + // Parses the crypto key parameters into + // https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#rtcsrtpkeyparam* + SDPUtils.parseCryptoKeyParams = function(keyParams) { + if (keyParams.indexOf('inline:') !== 0) { + return null; + } + var parts = keyParams.substr(7).split('|'); + return { + keyMethod: 'inline', + keySalt: parts[0], + lifeTime: parts[1], + mkiValue: parts[2] ? parts[2].split(':')[0] : undefined, + mkiLength: parts[2] ? parts[2].split(':')[1] : undefined, + }; + }; + + SDPUtils.writeCryptoKeyParams = function(keyParams) { + return keyParams.keyMethod + ':' + + keyParams.keySalt + + (keyParams.lifeTime ? '|' + keyParams.lifeTime : '') + + (keyParams.mkiValue && keyParams.mkiLength + ? '|' + keyParams.mkiValue + ':' + keyParams.mkiLength + : ''); + }; + + // Extracts all SDES paramters. + SDPUtils.getCryptoParameters = function(mediaSection, sessionpart) { + var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, + 'a=crypto:'); + return lines.map(SDPUtils.parseCryptoLine); + }; + + // Parses ICE information from SDP media section or sessionpart. + // FIXME: for consistency with other functions this should only + // get the ice-ufrag and ice-pwd lines as input. + SDPUtils.getIceParameters = function(mediaSection, sessionpart) { + var ufrag = SDPUtils.matchPrefix(mediaSection + sessionpart, + 'a=ice-ufrag:')[0]; + var pwd = SDPUtils.matchPrefix(mediaSection + sessionpart, + 'a=ice-pwd:')[0]; + if (!(ufrag && pwd)) { + return null; + } + return { + usernameFragment: ufrag.substr(12), + password: pwd.substr(10), + }; + }; + + // Serializes ICE parameters to SDP. + SDPUtils.writeIceParameters = function(params) { + return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' + + 'a=ice-pwd:' + params.password + '\r\n'; + }; + + // Parses the SDP media section and returns RTCRtpParameters. + SDPUtils.parseRtpParameters = function(mediaSection) { + var description = { + codecs: [], + headerExtensions: [], + fecMechanisms: [], + rtcp: [] + }; + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].split(' '); + for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..] + var pt = mline[i]; + var rtpmapline = SDPUtils.matchPrefix( + mediaSection, 'a=rtpmap:' + pt + ' ')[0]; + if (rtpmapline) { + var codec = SDPUtils.parseRtpMap(rtpmapline); + var fmtps = SDPUtils.matchPrefix( + mediaSection, 'a=fmtp:' + pt + ' '); + // Only the first a=fmtp: is considered. + codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {}; + codec.rtcpFeedback = SDPUtils.matchPrefix( + mediaSection, 'a=rtcp-fb:' + pt + ' ') + .map(SDPUtils.parseRtcpFb); + description.codecs.push(codec); + // parse FEC mechanisms from rtpmap lines. + switch (codec.name.toUpperCase()) { + case 'RED': + case 'ULPFEC': + description.fecMechanisms.push(codec.name.toUpperCase()); + break; + } + } + } + SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) { + description.headerExtensions.push(SDPUtils.parseExtmap(line)); + }); + // FIXME: parse rtcp. + return description; + }; + + // Generates parts of the SDP media section describing the capabilities / + // parameters. + SDPUtils.writeRtpDescription = function(kind, caps) { + var sdp = ''; + + // Build the mline. + sdp += 'm=' + kind + ' '; + sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs. + sdp += ' UDP/TLS/RTP/SAVPF '; + sdp += caps.codecs.map(function(codec) { + if (codec.preferredPayloadType !== undefined) { + return codec.preferredPayloadType; + } + return codec.payloadType; + }).join(' ') + '\r\n'; + + sdp += 'c=IN IP4 0.0.0.0\r\n'; + sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n'; + + // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb. + caps.codecs.forEach(function(codec) { + sdp += SDPUtils.writeRtpMap(codec); + sdp += SDPUtils.writeFmtp(codec); + sdp += SDPUtils.writeRtcpFb(codec); + }); + var maxptime = 0; + caps.codecs.forEach(function(codec) { + if (codec.maxptime > maxptime) { + maxptime = codec.maxptime; + } + }); + if (maxptime > 0) { + sdp += 'a=maxptime:' + maxptime + '\r\n'; + } + sdp += 'a=rtcp-mux\r\n'; + + if (caps.headerExtensions) { + caps.headerExtensions.forEach(function(extension) { + sdp += SDPUtils.writeExtmap(extension); + }); + } + // FIXME: write fecMechanisms. + return sdp; + }; + + // Parses the SDP media section and returns an array of + // RTCRtpEncodingParameters. + SDPUtils.parseRtpEncodingParameters = function(mediaSection) { + var encodingParameters = []; + var description = SDPUtils.parseRtpParameters(mediaSection); + var hasRed = description.fecMechanisms.indexOf('RED') !== -1; + var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1; + + // filter a=ssrc:... cname:, ignore PlanB-msid + var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') + .map(function(line) { + return SDPUtils.parseSsrcMedia(line); + }) + .filter(function(parts) { + return parts.attribute === 'cname'; + }); + var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc; + var secondarySsrc; + + var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID') + .map(function(line) { + var parts = line.substr(17).split(' '); + return parts.map(function(part) { + return parseInt(part, 10); + }); + }); + if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) { + secondarySsrc = flows[0][1]; + } + + description.codecs.forEach(function(codec) { + if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) { + var encParam = { + ssrc: primarySsrc, + codecPayloadType: parseInt(codec.parameters.apt, 10) + }; + if (primarySsrc && secondarySsrc) { + encParam.rtx = {ssrc: secondarySsrc}; + } + encodingParameters.push(encParam); + if (hasRed) { + encParam = JSON.parse(JSON.stringify(encParam)); + encParam.fec = { + ssrc: primarySsrc, + mechanism: hasUlpfec ? 'red+ulpfec' : 'red' + }; + encodingParameters.push(encParam); + } + } + }); + if (encodingParameters.length === 0 && primarySsrc) { + encodingParameters.push({ + ssrc: primarySsrc + }); + } + + // we support both b=AS and b=TIAS but interpret AS as TIAS. + var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b='); + if (bandwidth.length) { + if (bandwidth[0].indexOf('b=TIAS:') === 0) { + bandwidth = parseInt(bandwidth[0].substr(7), 10); + } else if (bandwidth[0].indexOf('b=AS:') === 0) { + // use formula from JSEP to convert b=AS to TIAS value. + bandwidth = parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95 + - (50 * 40 * 8); + } else { + bandwidth = undefined; + } + encodingParameters.forEach(function(params) { + params.maxBitrate = bandwidth; + }); + } + return encodingParameters; + }; + + // parses http://draft.ortc.org/#rtcrtcpparameters* + SDPUtils.parseRtcpParameters = function(mediaSection) { + var rtcpParameters = {}; + + // Gets the first SSRC. Note tha with RTX there might be multiple + // SSRCs. + var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') + .map(function(line) { + return SDPUtils.parseSsrcMedia(line); + }) + .filter(function(obj) { + return obj.attribute === 'cname'; + })[0]; + if (remoteSsrc) { + rtcpParameters.cname = remoteSsrc.value; + rtcpParameters.ssrc = remoteSsrc.ssrc; + } + + // Edge uses the compound attribute instead of reducedSize + // compound is !reducedSize + var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize'); + rtcpParameters.reducedSize = rsize.length > 0; + rtcpParameters.compound = rsize.length === 0; + + // parses the rtcp-mux attrіbute. + // Note that Edge does not support unmuxed RTCP. + var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux'); + rtcpParameters.mux = mux.length > 0; + + return rtcpParameters; + }; + + // parses either a=msid: or a=ssrc:... msid lines and returns + // the id of the MediaStream and MediaStreamTrack. + SDPUtils.parseMsid = function(mediaSection) { + var parts; + var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:'); + if (spec.length === 1) { + parts = spec[0].substr(7).split(' '); + return {stream: parts[0], track: parts[1]}; + } + var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') + .map(function(line) { + return SDPUtils.parseSsrcMedia(line); + }) + .filter(function(msidParts) { + return msidParts.attribute === 'msid'; + }); + if (planB.length > 0) { + parts = planB[0].value.split(' '); + return {stream: parts[0], track: parts[1]}; + } + }; + + // SCTP + // parses draft-ietf-mmusic-sctp-sdp-26 first and falls back + // to draft-ietf-mmusic-sctp-sdp-05 + SDPUtils.parseSctpDescription = function(mediaSection) { + var mline = SDPUtils.parseMLine(mediaSection); + var maxSizeLine = SDPUtils.matchPrefix(mediaSection, 'a=max-message-size:'); + var maxMessageSize; + if (maxSizeLine.length > 0) { + maxMessageSize = parseInt(maxSizeLine[0].substr(19), 10); + } + if (isNaN(maxMessageSize)) { + maxMessageSize = 65536; + } + var sctpPort = SDPUtils.matchPrefix(mediaSection, 'a=sctp-port:'); + if (sctpPort.length > 0) { + return { + port: parseInt(sctpPort[0].substr(12), 10), + protocol: mline.fmt, + maxMessageSize: maxMessageSize + }; + } + var sctpMapLines = SDPUtils.matchPrefix(mediaSection, 'a=sctpmap:'); + if (sctpMapLines.length > 0) { + var parts = SDPUtils.matchPrefix(mediaSection, 'a=sctpmap:')[0] + .substr(10) + .split(' '); + return { + port: parseInt(parts[0], 10), + protocol: parts[1], + maxMessageSize: maxMessageSize + }; + } + }; + + // SCTP + // outputs the draft-ietf-mmusic-sctp-sdp-26 version that all browsers + // support by now receiving in this format, unless we originally parsed + // as the draft-ietf-mmusic-sctp-sdp-05 format (indicated by the m-line + // protocol of DTLS/SCTP -- without UDP/ or TCP/) + SDPUtils.writeSctpDescription = function(media, sctp) { + var output = []; + if (media.protocol !== 'DTLS/SCTP') { + output = [ + 'm=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.protocol + '\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=sctp-port:' + sctp.port + '\r\n' + ]; + } else { + output = [ + 'm=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.port + '\r\n', + 'c=IN IP4 0.0.0.0\r\n', + 'a=sctpmap:' + sctp.port + ' ' + sctp.protocol + ' 65535\r\n' + ]; + } + if (sctp.maxMessageSize !== undefined) { + output.push('a=max-message-size:' + sctp.maxMessageSize + '\r\n'); + } + return output.join(''); + }; + + // Generate a session ID for SDP. + // https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1 + // recommends using a cryptographically random +ve 64-bit value + // but right now this should be acceptable and within the right range + SDPUtils.generateSessionId = function() { + return Math.random().toString().substr(2, 21); + }; + + // Write boilder plate for start of SDP + // sessId argument is optional - if not supplied it will + // be generated randomly + // sessVersion is optional and defaults to 2 + // sessUser is optional and defaults to 'thisisadapterortc' + SDPUtils.writeSessionBoilerplate = function(sessId, sessVer, sessUser) { + var sessionId; + var version = sessVer !== undefined ? sessVer : 2; + if (sessId) { + sessionId = sessId; + } else { + sessionId = SDPUtils.generateSessionId(); + } + var user = sessUser || 'thisisadapterortc'; + // FIXME: sess-id should be an NTP timestamp. + return 'v=0\r\n' + + 'o=' + user + ' ' + sessionId + ' ' + version + + ' IN IP4 127.0.0.1\r\n' + + 's=-\r\n' + + 't=0 0\r\n'; + }; + + SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) { + var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps); + + // Map ICE parameters (ufrag, pwd) to SDP. + sdp += SDPUtils.writeIceParameters( + transceiver.iceGatherer.getLocalParameters()); + + // Map DTLS parameters to SDP. + sdp += SDPUtils.writeDtlsParameters( + transceiver.dtlsTransport.getLocalParameters(), + type === 'offer' ? 'actpass' : 'active'); + + sdp += 'a=mid:' + transceiver.mid + '\r\n'; + + if (transceiver.direction) { + sdp += 'a=' + transceiver.direction + '\r\n'; + } else if (transceiver.rtpSender && transceiver.rtpReceiver) { + sdp += 'a=sendrecv\r\n'; + } else if (transceiver.rtpSender) { + sdp += 'a=sendonly\r\n'; + } else if (transceiver.rtpReceiver) { + sdp += 'a=recvonly\r\n'; + } else { + sdp += 'a=inactive\r\n'; + } + + if (transceiver.rtpSender) { + // spec. + var msid = 'msid:' + stream.id + ' ' + + transceiver.rtpSender.track.id + '\r\n'; + sdp += 'a=' + msid; + + // for Chrome. + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + + ' ' + msid; + if (transceiver.sendEncodingParameters[0].rtx) { + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + + ' ' + msid; + sdp += 'a=ssrc-group:FID ' + + transceiver.sendEncodingParameters[0].ssrc + ' ' + + transceiver.sendEncodingParameters[0].rtx.ssrc + + '\r\n'; + } + } + // FIXME: this should be written by writeRtpDescription. + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + + ' cname:' + SDPUtils.localCName + '\r\n'; + if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) { + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + + ' cname:' + SDPUtils.localCName + '\r\n'; + } + return sdp; + }; + + // Gets the direction from the mediaSection or the sessionpart. + SDPUtils.getDirection = function(mediaSection, sessionpart) { + // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv. + var lines = SDPUtils.splitLines(mediaSection); + for (var i = 0; i < lines.length; i++) { + switch (lines[i]) { + case 'a=sendrecv': + case 'a=sendonly': + case 'a=recvonly': + case 'a=inactive': + return lines[i].substr(2); + // FIXME: What should happen here? + } + } + if (sessionpart) { + return SDPUtils.getDirection(sessionpart); + } + return 'sendrecv'; + }; + + SDPUtils.getKind = function(mediaSection) { + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].split(' '); + return mline[0].substr(2); + }; + + SDPUtils.isRejected = function(mediaSection) { + return mediaSection.split(' ', 2)[1] === '0'; + }; + + SDPUtils.parseMLine = function(mediaSection) { + var lines = SDPUtils.splitLines(mediaSection); + var parts = lines[0].substr(2).split(' '); + return { + kind: parts[0], + port: parseInt(parts[1], 10), + protocol: parts[2], + fmt: parts.slice(3).join(' ') + }; + }; + + SDPUtils.parseOLine = function(mediaSection) { + var line = SDPUtils.matchPrefix(mediaSection, 'o=')[0]; + var parts = line.substr(2).split(' '); + return { + username: parts[0], + sessionId: parts[1], + sessionVersion: parseInt(parts[2], 10), + netType: parts[3], + addressType: parts[4], + address: parts[5] + }; + }; + + // a very naive interpretation of a valid SDP. + SDPUtils.isValidSDP = function(blob) { + if (typeof blob !== 'string' || blob.length === 0) { + return false; + } + var lines = SDPUtils.splitLines(blob); + for (var i = 0; i < lines.length; i++) { + if (lines[i].length < 2 || lines[i].charAt(1) !== '=') { + return false; + } + // TODO: check the modifier a bit more. + } + return true; + }; + + // Expose public methods. + { + module.exports = SDPUtils; + } + }); + + /* + * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + + + + function fixStatsType(stat) { + return { + inboundrtp: 'inbound-rtp', + outboundrtp: 'outbound-rtp', + candidatepair: 'candidate-pair', + localcandidate: 'local-candidate', + remotecandidate: 'remote-candidate' + }[stat.type] || stat.type; + } + + function writeMediaSection(transceiver, caps, type, stream, dtlsRole) { + var sdp$1 = sdp.writeRtpDescription(transceiver.kind, caps); + + // Map ICE parameters (ufrag, pwd) to SDP. + sdp$1 += sdp.writeIceParameters( + transceiver.iceGatherer.getLocalParameters()); + + // Map DTLS parameters to SDP. + sdp$1 += sdp.writeDtlsParameters( + transceiver.dtlsTransport.getLocalParameters(), + type === 'offer' ? 'actpass' : dtlsRole || 'active'); + + sdp$1 += 'a=mid:' + transceiver.mid + '\r\n'; + + if (transceiver.rtpSender && transceiver.rtpReceiver) { + sdp$1 += 'a=sendrecv\r\n'; + } else if (transceiver.rtpSender) { + sdp$1 += 'a=sendonly\r\n'; + } else if (transceiver.rtpReceiver) { + sdp$1 += 'a=recvonly\r\n'; + } else { + sdp$1 += 'a=inactive\r\n'; + } + + if (transceiver.rtpSender) { + var trackId = transceiver.rtpSender._initialTrackId || + transceiver.rtpSender.track.id; + transceiver.rtpSender._initialTrackId = trackId; + // spec. + var msid = 'msid:' + (stream ? stream.id : '-') + ' ' + + trackId + '\r\n'; + sdp$1 += 'a=' + msid; + // for Chrome. Legacy should no longer be required. + sdp$1 += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + + ' ' + msid; + + // RTX + if (transceiver.sendEncodingParameters[0].rtx) { + sdp$1 += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + + ' ' + msid; + sdp$1 += 'a=ssrc-group:FID ' + + transceiver.sendEncodingParameters[0].ssrc + ' ' + + transceiver.sendEncodingParameters[0].rtx.ssrc + + '\r\n'; + } + } + // FIXME: this should be written by writeRtpDescription. + sdp$1 += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + + ' cname:' + sdp.localCName + '\r\n'; + if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) { + sdp$1 += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + + ' cname:' + sdp.localCName + '\r\n'; + } + return sdp$1; + } + + // Edge does not like + // 1) stun: filtered after 14393 unless ?transport=udp is present + // 2) turn: that does not have all of turn:host:port?transport=udp + // 3) turn: with ipv6 addresses + // 4) turn: occurring muliple times + function filterIceServers(iceServers, edgeVersion) { + var hasTurn = false; + iceServers = JSON.parse(JSON.stringify(iceServers)); + return iceServers.filter(function(server) { + if (server && (server.urls || server.url)) { + var urls = server.urls || server.url; + if (server.url && !server.urls) { + console.warn('RTCIceServer.url is deprecated! Use urls instead.'); + } + var isString = typeof urls === 'string'; + if (isString) { + urls = [urls]; + } + urls = urls.filter(function(url) { + var validTurn = url.indexOf('turn:') === 0 && + url.indexOf('transport=udp') !== -1 && + url.indexOf('turn:[') === -1 && + !hasTurn; + + if (validTurn) { + hasTurn = true; + return true; + } + return url.indexOf('stun:') === 0 && edgeVersion >= 14393 && + url.indexOf('?transport=udp') === -1; + }); + + delete server.url; + server.urls = isString ? urls[0] : urls; + return !!urls.length; + } + }); + } + + // Determines the intersection of local and remote capabilities. + function getCommonCapabilities(localCapabilities, remoteCapabilities) { + var commonCapabilities = { + codecs: [], + headerExtensions: [], + fecMechanisms: [] + }; + + var findCodecByPayloadType = function(pt, codecs) { + pt = parseInt(pt, 10); + for (var i = 0; i < codecs.length; i++) { + if (codecs[i].payloadType === pt || + codecs[i].preferredPayloadType === pt) { + return codecs[i]; + } + } + }; + + var rtxCapabilityMatches = function(lRtx, rRtx, lCodecs, rCodecs) { + var lCodec = findCodecByPayloadType(lRtx.parameters.apt, lCodecs); + var rCodec = findCodecByPayloadType(rRtx.parameters.apt, rCodecs); + return lCodec && rCodec && + lCodec.name.toLowerCase() === rCodec.name.toLowerCase(); + }; + + localCapabilities.codecs.forEach(function(lCodec) { + for (var i = 0; i < remoteCapabilities.codecs.length; i++) { + var rCodec = remoteCapabilities.codecs[i]; + if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() && + lCodec.clockRate === rCodec.clockRate) { + if (lCodec.name.toLowerCase() === 'rtx' && + lCodec.parameters && rCodec.parameters.apt) { + // for RTX we need to find the local rtx that has a apt + // which points to the same local codec as the remote one. + if (!rtxCapabilityMatches(lCodec, rCodec, + localCapabilities.codecs, remoteCapabilities.codecs)) { + continue; + } + } + rCodec = JSON.parse(JSON.stringify(rCodec)); // deepcopy + // number of channels is the highest common number of channels + rCodec.numChannels = Math.min(lCodec.numChannels, + rCodec.numChannels); + // push rCodec so we reply with offerer payload type + commonCapabilities.codecs.push(rCodec); + + // determine common feedback mechanisms + rCodec.rtcpFeedback = rCodec.rtcpFeedback.filter(function(fb) { + for (var j = 0; j < lCodec.rtcpFeedback.length; j++) { + if (lCodec.rtcpFeedback[j].type === fb.type && + lCodec.rtcpFeedback[j].parameter === fb.parameter) { + return true; + } + } + return false; + }); + // FIXME: also need to determine .parameters + // see https://github.com/openpeer/ortc/issues/569 + break; + } + } + }); + + localCapabilities.headerExtensions.forEach(function(lHeaderExtension) { + for (var i = 0; i < remoteCapabilities.headerExtensions.length; + i++) { + var rHeaderExtension = remoteCapabilities.headerExtensions[i]; + if (lHeaderExtension.uri === rHeaderExtension.uri) { + commonCapabilities.headerExtensions.push(rHeaderExtension); + break; + } + } + }); + + // FIXME: fecMechanisms + return commonCapabilities; + } + + // is action=setLocalDescription with type allowed in signalingState + function isActionAllowedInSignalingState(action, type, signalingState) { + return { + offer: { + setLocalDescription: ['stable', 'have-local-offer'], + setRemoteDescription: ['stable', 'have-remote-offer'] + }, + answer: { + setLocalDescription: ['have-remote-offer', 'have-local-pranswer'], + setRemoteDescription: ['have-local-offer', 'have-remote-pranswer'] + } + }[type][action].indexOf(signalingState) !== -1; + } + + function maybeAddCandidate(iceTransport, candidate) { + // Edge's internal representation adds some fields therefore + // not all fieldѕ are taken into account. + var alreadyAdded = iceTransport.getRemoteCandidates() + .find(function(remoteCandidate) { + return candidate.foundation === remoteCandidate.foundation && + candidate.ip === remoteCandidate.ip && + candidate.port === remoteCandidate.port && + candidate.priority === remoteCandidate.priority && + candidate.protocol === remoteCandidate.protocol && + candidate.type === remoteCandidate.type; + }); + if (!alreadyAdded) { + iceTransport.addRemoteCandidate(candidate); + } + return !alreadyAdded; + } + + + function makeError(name, description) { + var e = new Error(description); + e.name = name; + // legacy error codes from https://heycam.github.io/webidl/#idl-DOMException-error-names + e.code = { + NotSupportedError: 9, + InvalidStateError: 11, + InvalidAccessError: 15, + TypeError: undefined, + OperationError: undefined + }[name]; + return e; + } + + var rtcpeerconnection = function(window, edgeVersion) { + // https://w3c.github.io/mediacapture-main/#mediastream + // Helper function to add the track to the stream and + // dispatch the event ourselves. + function addTrackToStreamAndFireEvent(track, stream) { + stream.addTrack(track); + stream.dispatchEvent(new window.MediaStreamTrackEvent('addtrack', + {track: track})); + } + + function removeTrackFromStreamAndFireEvent(track, stream) { + stream.removeTrack(track); + stream.dispatchEvent(new window.MediaStreamTrackEvent('removetrack', + {track: track})); + } + + function fireAddTrack(pc, track, receiver, streams) { + var trackEvent = new Event('track'); + trackEvent.track = track; + trackEvent.receiver = receiver; + trackEvent.transceiver = {receiver: receiver}; + trackEvent.streams = streams; + window.setTimeout(function() { + pc._dispatchEvent('track', trackEvent); + }); + } + + var RTCPeerConnection = function(config) { + var pc = this; + + var _eventTarget = document.createDocumentFragment(); + ['addEventListener', 'removeEventListener', 'dispatchEvent'] + .forEach(function(method) { + pc[method] = _eventTarget[method].bind(_eventTarget); + }); + + this.canTrickleIceCandidates = null; + + this.needNegotiation = false; + + this.localStreams = []; + this.remoteStreams = []; + + this._localDescription = null; + this._remoteDescription = null; + + this.signalingState = 'stable'; + this.iceConnectionState = 'new'; + this.connectionState = 'new'; + this.iceGatheringState = 'new'; + + config = JSON.parse(JSON.stringify(config || {})); + + this.usingBundle = config.bundlePolicy === 'max-bundle'; + if (config.rtcpMuxPolicy === 'negotiate') { + throw(makeError('NotSupportedError', + 'rtcpMuxPolicy \'negotiate\' is not supported')); + } else if (!config.rtcpMuxPolicy) { + config.rtcpMuxPolicy = 'require'; + } + + switch (config.iceTransportPolicy) { + case 'all': + case 'relay': + break; + default: + config.iceTransportPolicy = 'all'; + break; + } + + switch (config.bundlePolicy) { + case 'balanced': + case 'max-compat': + case 'max-bundle': + break; + default: + config.bundlePolicy = 'balanced'; + break; + } + + config.iceServers = filterIceServers(config.iceServers || [], edgeVersion); + + this._iceGatherers = []; + if (config.iceCandidatePoolSize) { + for (var i = config.iceCandidatePoolSize; i > 0; i--) { + this._iceGatherers.push(new window.RTCIceGatherer({ + iceServers: config.iceServers, + gatherPolicy: config.iceTransportPolicy + })); + } + } else { + config.iceCandidatePoolSize = 0; + } + + this._config = config; + + // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ... + // everything that is needed to describe a SDP m-line. + this.transceivers = []; + + this._sdpSessionId = sdp.generateSessionId(); + this._sdpSessionVersion = 0; + + this._dtlsRole = undefined; // role for a=setup to use in answers. + + this._isClosed = false; + }; + + Object.defineProperty(RTCPeerConnection.prototype, 'localDescription', { + configurable: true, + get: function() { + return this._localDescription; + } + }); + Object.defineProperty(RTCPeerConnection.prototype, 'remoteDescription', { + configurable: true, + get: function() { + return this._remoteDescription; + } + }); + + // set up event handlers on prototype + RTCPeerConnection.prototype.onicecandidate = null; + RTCPeerConnection.prototype.onaddstream = null; + RTCPeerConnection.prototype.ontrack = null; + RTCPeerConnection.prototype.onremovestream = null; + RTCPeerConnection.prototype.onsignalingstatechange = null; + RTCPeerConnection.prototype.oniceconnectionstatechange = null; + RTCPeerConnection.prototype.onconnectionstatechange = null; + RTCPeerConnection.prototype.onicegatheringstatechange = null; + RTCPeerConnection.prototype.onnegotiationneeded = null; + RTCPeerConnection.prototype.ondatachannel = null; + + RTCPeerConnection.prototype._dispatchEvent = function(name, event) { + if (this._isClosed) { + return; + } + this.dispatchEvent(event); + if (typeof this['on' + name] === 'function') { + this['on' + name](event); + } + }; + + RTCPeerConnection.prototype._emitGatheringStateChange = function() { + var event = new Event('icegatheringstatechange'); + this._dispatchEvent('icegatheringstatechange', event); + }; + + RTCPeerConnection.prototype.getConfiguration = function() { + return this._config; + }; + + RTCPeerConnection.prototype.getLocalStreams = function() { + return this.localStreams; + }; + + RTCPeerConnection.prototype.getRemoteStreams = function() { + return this.remoteStreams; + }; + + // internal helper to create a transceiver object. + // (which is not yet the same as the WebRTC 1.0 transceiver) + RTCPeerConnection.prototype._createTransceiver = function(kind, doNotAdd) { + var hasBundleTransport = this.transceivers.length > 0; + var transceiver = { + track: null, + iceGatherer: null, + iceTransport: null, + dtlsTransport: null, + localCapabilities: null, + remoteCapabilities: null, + rtpSender: null, + rtpReceiver: null, + kind: kind, + mid: null, + sendEncodingParameters: null, + recvEncodingParameters: null, + stream: null, + associatedRemoteMediaStreams: [], + wantReceive: true + }; + if (this.usingBundle && hasBundleTransport) { + transceiver.iceTransport = this.transceivers[0].iceTransport; + transceiver.dtlsTransport = this.transceivers[0].dtlsTransport; + } else { + var transports = this._createIceAndDtlsTransports(); + transceiver.iceTransport = transports.iceTransport; + transceiver.dtlsTransport = transports.dtlsTransport; + } + if (!doNotAdd) { + this.transceivers.push(transceiver); + } + return transceiver; + }; + + RTCPeerConnection.prototype.addTrack = function(track, stream) { + if (this._isClosed) { + throw makeError('InvalidStateError', + 'Attempted to call addTrack on a closed peerconnection.'); + } + + var alreadyExists = this.transceivers.find(function(s) { + return s.track === track; + }); + + if (alreadyExists) { + throw makeError('InvalidAccessError', 'Track already exists.'); + } + + var transceiver; + for (var i = 0; i < this.transceivers.length; i++) { + if (!this.transceivers[i].track && + this.transceivers[i].kind === track.kind) { + transceiver = this.transceivers[i]; + } + } + if (!transceiver) { + transceiver = this._createTransceiver(track.kind); + } + + this._maybeFireNegotiationNeeded(); + + if (this.localStreams.indexOf(stream) === -1) { + this.localStreams.push(stream); + } + + transceiver.track = track; + transceiver.stream = stream; + transceiver.rtpSender = new window.RTCRtpSender(track, + transceiver.dtlsTransport); + return transceiver.rtpSender; + }; + + RTCPeerConnection.prototype.addStream = function(stream) { + var pc = this; + if (edgeVersion >= 15025) { + stream.getTracks().forEach(function(track) { + pc.addTrack(track, stream); + }); + } else { + // Clone is necessary for local demos mostly, attaching directly + // to two different senders does not work (build 10547). + // Fixed in 15025 (or earlier) + var clonedStream = stream.clone(); + stream.getTracks().forEach(function(track, idx) { + var clonedTrack = clonedStream.getTracks()[idx]; + track.addEventListener('enabled', function(event) { + clonedTrack.enabled = event.enabled; + }); + }); + clonedStream.getTracks().forEach(function(track) { + pc.addTrack(track, clonedStream); + }); + } + }; + + RTCPeerConnection.prototype.removeTrack = function(sender) { + if (this._isClosed) { + throw makeError('InvalidStateError', + 'Attempted to call removeTrack on a closed peerconnection.'); + } + + if (!(sender instanceof window.RTCRtpSender)) { + throw new TypeError('Argument 1 of RTCPeerConnection.removeTrack ' + + 'does not implement interface RTCRtpSender.'); + } + + var transceiver = this.transceivers.find(function(t) { + return t.rtpSender === sender; + }); + + if (!transceiver) { + throw makeError('InvalidAccessError', + 'Sender was not created by this connection.'); + } + var stream = transceiver.stream; + + transceiver.rtpSender.stop(); + transceiver.rtpSender = null; + transceiver.track = null; + transceiver.stream = null; + + // remove the stream from the set of local streams + var localStreams = this.transceivers.map(function(t) { + return t.stream; + }); + if (localStreams.indexOf(stream) === -1 && + this.localStreams.indexOf(stream) > -1) { + this.localStreams.splice(this.localStreams.indexOf(stream), 1); + } + + this._maybeFireNegotiationNeeded(); + }; + + RTCPeerConnection.prototype.removeStream = function(stream) { + var pc = this; + stream.getTracks().forEach(function(track) { + var sender = pc.getSenders().find(function(s) { + return s.track === track; + }); + if (sender) { + pc.removeTrack(sender); + } + }); + }; + + RTCPeerConnection.prototype.getSenders = function() { + return this.transceivers.filter(function(transceiver) { + return !!transceiver.rtpSender; + }) + .map(function(transceiver) { + return transceiver.rtpSender; + }); + }; + + RTCPeerConnection.prototype.getReceivers = function() { + return this.transceivers.filter(function(transceiver) { + return !!transceiver.rtpReceiver; + }) + .map(function(transceiver) { + return transceiver.rtpReceiver; + }); + }; + + + RTCPeerConnection.prototype._createIceGatherer = function(sdpMLineIndex, + usingBundle) { + var pc = this; + if (usingBundle && sdpMLineIndex > 0) { + return this.transceivers[0].iceGatherer; + } else if (this._iceGatherers.length) { + return this._iceGatherers.shift(); + } + var iceGatherer = new window.RTCIceGatherer({ + iceServers: this._config.iceServers, + gatherPolicy: this._config.iceTransportPolicy + }); + Object.defineProperty(iceGatherer, 'state', + {value: 'new', writable: true} + ); + + this.transceivers[sdpMLineIndex].bufferedCandidateEvents = []; + this.transceivers[sdpMLineIndex].bufferCandidates = function(event) { + var end = !event.candidate || Object.keys(event.candidate).length === 0; + // polyfill since RTCIceGatherer.state is not implemented in + // Edge 10547 yet. + iceGatherer.state = end ? 'completed' : 'gathering'; + if (pc.transceivers[sdpMLineIndex].bufferedCandidateEvents !== null) { + pc.transceivers[sdpMLineIndex].bufferedCandidateEvents.push(event); + } + }; + iceGatherer.addEventListener('localcandidate', + this.transceivers[sdpMLineIndex].bufferCandidates); + return iceGatherer; + }; + + // start gathering from an RTCIceGatherer. + RTCPeerConnection.prototype._gather = function(mid, sdpMLineIndex) { + var pc = this; + var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer; + if (iceGatherer.onlocalcandidate) { + return; + } + var bufferedCandidateEvents = + this.transceivers[sdpMLineIndex].bufferedCandidateEvents; + this.transceivers[sdpMLineIndex].bufferedCandidateEvents = null; + iceGatherer.removeEventListener('localcandidate', + this.transceivers[sdpMLineIndex].bufferCandidates); + iceGatherer.onlocalcandidate = function(evt) { + if (pc.usingBundle && sdpMLineIndex > 0) { + // if we know that we use bundle we can drop candidates with + // ѕdpMLineIndex > 0. If we don't do this then our state gets + // confused since we dispose the extra ice gatherer. + return; + } + var event = new Event('icecandidate'); + event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex}; + + var cand = evt.candidate; + // Edge emits an empty object for RTCIceCandidateComplete‥ + var end = !cand || Object.keys(cand).length === 0; + if (end) { + // polyfill since RTCIceGatherer.state is not implemented in + // Edge 10547 yet. + if (iceGatherer.state === 'new' || iceGatherer.state === 'gathering') { + iceGatherer.state = 'completed'; + } + } else { + if (iceGatherer.state === 'new') { + iceGatherer.state = 'gathering'; + } + // RTCIceCandidate doesn't have a component, needs to be added + cand.component = 1; + // also the usernameFragment. TODO: update SDP to take both variants. + cand.ufrag = iceGatherer.getLocalParameters().usernameFragment; + + var serializedCandidate = sdp.writeCandidate(cand); + event.candidate = Object.assign(event.candidate, + sdp.parseCandidate(serializedCandidate)); + + event.candidate.candidate = serializedCandidate; + event.candidate.toJSON = function() { + return { + candidate: event.candidate.candidate, + sdpMid: event.candidate.sdpMid, + sdpMLineIndex: event.candidate.sdpMLineIndex, + usernameFragment: event.candidate.usernameFragment + }; + }; + } + + // update local description. + var sections = sdp.getMediaSections(pc._localDescription.sdp); + if (!end) { + sections[event.candidate.sdpMLineIndex] += + 'a=' + event.candidate.candidate + '\r\n'; + } else { + sections[event.candidate.sdpMLineIndex] += + 'a=end-of-candidates\r\n'; + } + pc._localDescription.sdp = + sdp.getDescription(pc._localDescription.sdp) + + sections.join(''); + var complete = pc.transceivers.every(function(transceiver) { + return transceiver.iceGatherer && + transceiver.iceGatherer.state === 'completed'; + }); + + if (pc.iceGatheringState !== 'gathering') { + pc.iceGatheringState = 'gathering'; + pc._emitGatheringStateChange(); + } + + // Emit candidate. Also emit null candidate when all gatherers are + // complete. + if (!end) { + pc._dispatchEvent('icecandidate', event); + } + if (complete) { + pc._dispatchEvent('icecandidate', new Event('icecandidate')); + pc.iceGatheringState = 'complete'; + pc._emitGatheringStateChange(); + } + }; + + // emit already gathered candidates. + window.setTimeout(function() { + bufferedCandidateEvents.forEach(function(e) { + iceGatherer.onlocalcandidate(e); + }); + }, 0); + }; + + // Create ICE transport and DTLS transport. + RTCPeerConnection.prototype._createIceAndDtlsTransports = function() { + var pc = this; + var iceTransport = new window.RTCIceTransport(null); + iceTransport.onicestatechange = function() { + pc._updateIceConnectionState(); + pc._updateConnectionState(); + }; + + var dtlsTransport = new window.RTCDtlsTransport(iceTransport); + dtlsTransport.ondtlsstatechange = function() { + pc._updateConnectionState(); + }; + dtlsTransport.onerror = function() { + // onerror does not set state to failed by itself. + Object.defineProperty(dtlsTransport, 'state', + {value: 'failed', writable: true}); + pc._updateConnectionState(); + }; + + return { + iceTransport: iceTransport, + dtlsTransport: dtlsTransport + }; + }; + + // Destroy ICE gatherer, ICE transport and DTLS transport. + // Without triggering the callbacks. + RTCPeerConnection.prototype._disposeIceAndDtlsTransports = function( + sdpMLineIndex) { + var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer; + if (iceGatherer) { + delete iceGatherer.onlocalcandidate; + delete this.transceivers[sdpMLineIndex].iceGatherer; + } + var iceTransport = this.transceivers[sdpMLineIndex].iceTransport; + if (iceTransport) { + delete iceTransport.onicestatechange; + delete this.transceivers[sdpMLineIndex].iceTransport; + } + var dtlsTransport = this.transceivers[sdpMLineIndex].dtlsTransport; + if (dtlsTransport) { + delete dtlsTransport.ondtlsstatechange; + delete dtlsTransport.onerror; + delete this.transceivers[sdpMLineIndex].dtlsTransport; + } + }; + + // Start the RTP Sender and Receiver for a transceiver. + RTCPeerConnection.prototype._transceive = function(transceiver, + send, recv) { + var params = getCommonCapabilities(transceiver.localCapabilities, + transceiver.remoteCapabilities); + if (send && transceiver.rtpSender) { + params.encodings = transceiver.sendEncodingParameters; + params.rtcp = { + cname: sdp.localCName, + compound: transceiver.rtcpParameters.compound + }; + if (transceiver.recvEncodingParameters.length) { + params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc; + } + transceiver.rtpSender.send(params); + } + if (recv && transceiver.rtpReceiver && params.codecs.length > 0) { + // remove RTX field in Edge 14942 + if (transceiver.kind === 'video' + && transceiver.recvEncodingParameters + && edgeVersion < 15019) { + transceiver.recvEncodingParameters.forEach(function(p) { + delete p.rtx; + }); + } + if (transceiver.recvEncodingParameters.length) { + params.encodings = transceiver.recvEncodingParameters; + } else { + params.encodings = [{}]; + } + params.rtcp = { + compound: transceiver.rtcpParameters.compound + }; + if (transceiver.rtcpParameters.cname) { + params.rtcp.cname = transceiver.rtcpParameters.cname; + } + if (transceiver.sendEncodingParameters.length) { + params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc; + } + transceiver.rtpReceiver.receive(params); + } + }; + + RTCPeerConnection.prototype.setLocalDescription = function(description) { + var pc = this; + + // Note: pranswer is not supported. + if (['offer', 'answer'].indexOf(description.type) === -1) { + return Promise.reject(makeError('TypeError', + 'Unsupported type "' + description.type + '"')); + } + + if (!isActionAllowedInSignalingState('setLocalDescription', + description.type, pc.signalingState) || pc._isClosed) { + return Promise.reject(makeError('InvalidStateError', + 'Can not set local ' + description.type + + ' in state ' + pc.signalingState)); + } + + var sections; + var sessionpart; + if (description.type === 'offer') { + // VERY limited support for SDP munging. Limited to: + // * changing the order of codecs + sections = sdp.splitSections(description.sdp); + sessionpart = sections.shift(); + sections.forEach(function(mediaSection, sdpMLineIndex) { + var caps = sdp.parseRtpParameters(mediaSection); + pc.transceivers[sdpMLineIndex].localCapabilities = caps; + }); + + pc.transceivers.forEach(function(transceiver, sdpMLineIndex) { + pc._gather(transceiver.mid, sdpMLineIndex); + }); + } else if (description.type === 'answer') { + sections = sdp.splitSections(pc._remoteDescription.sdp); + sessionpart = sections.shift(); + var isIceLite = sdp.matchPrefix(sessionpart, + 'a=ice-lite').length > 0; + sections.forEach(function(mediaSection, sdpMLineIndex) { + var transceiver = pc.transceivers[sdpMLineIndex]; + var iceGatherer = transceiver.iceGatherer; + var iceTransport = transceiver.iceTransport; + var dtlsTransport = transceiver.dtlsTransport; + var localCapabilities = transceiver.localCapabilities; + var remoteCapabilities = transceiver.remoteCapabilities; + + // treat bundle-only as not-rejected. + var rejected = sdp.isRejected(mediaSection) && + sdp.matchPrefix(mediaSection, 'a=bundle-only').length === 0; + + if (!rejected && !transceiver.rejected) { + var remoteIceParameters = sdp.getIceParameters( + mediaSection, sessionpart); + var remoteDtlsParameters = sdp.getDtlsParameters( + mediaSection, sessionpart); + if (isIceLite) { + remoteDtlsParameters.role = 'server'; + } + + if (!pc.usingBundle || sdpMLineIndex === 0) { + pc._gather(transceiver.mid, sdpMLineIndex); + if (iceTransport.state === 'new') { + iceTransport.start(iceGatherer, remoteIceParameters, + isIceLite ? 'controlling' : 'controlled'); + } + if (dtlsTransport.state === 'new') { + dtlsTransport.start(remoteDtlsParameters); + } + } + + // Calculate intersection of capabilities. + var params = getCommonCapabilities(localCapabilities, + remoteCapabilities); + + // Start the RTCRtpSender. The RTCRtpReceiver for this + // transceiver has already been started in setRemoteDescription. + pc._transceive(transceiver, + params.codecs.length > 0, + false); + } + }); + } + + pc._localDescription = { + type: description.type, + sdp: description.sdp + }; + if (description.type === 'offer') { + pc._updateSignalingState('have-local-offer'); + } else { + pc._updateSignalingState('stable'); + } + + return Promise.resolve(); + }; + + RTCPeerConnection.prototype.setRemoteDescription = function(description) { + var pc = this; + + // Note: pranswer is not supported. + if (['offer', 'answer'].indexOf(description.type) === -1) { + return Promise.reject(makeError('TypeError', + 'Unsupported type "' + description.type + '"')); + } + + if (!isActionAllowedInSignalingState('setRemoteDescription', + description.type, pc.signalingState) || pc._isClosed) { + return Promise.reject(makeError('InvalidStateError', + 'Can not set remote ' + description.type + + ' in state ' + pc.signalingState)); + } + + var streams = {}; + pc.remoteStreams.forEach(function(stream) { + streams[stream.id] = stream; + }); + var receiverList = []; + var sections = sdp.splitSections(description.sdp); + var sessionpart = sections.shift(); + var isIceLite = sdp.matchPrefix(sessionpart, + 'a=ice-lite').length > 0; + var usingBundle = sdp.matchPrefix(sessionpart, + 'a=group:BUNDLE ').length > 0; + pc.usingBundle = usingBundle; + var iceOptions = sdp.matchPrefix(sessionpart, + 'a=ice-options:')[0]; + if (iceOptions) { + pc.canTrickleIceCandidates = iceOptions.substr(14).split(' ') + .indexOf('trickle') >= 0; + } else { + pc.canTrickleIceCandidates = false; + } + + sections.forEach(function(mediaSection, sdpMLineIndex) { + var lines = sdp.splitLines(mediaSection); + var kind = sdp.getKind(mediaSection); + // treat bundle-only as not-rejected. + var rejected = sdp.isRejected(mediaSection) && + sdp.matchPrefix(mediaSection, 'a=bundle-only').length === 0; + var protocol = lines[0].substr(2).split(' ')[2]; + + var direction = sdp.getDirection(mediaSection, sessionpart); + var remoteMsid = sdp.parseMsid(mediaSection); + + var mid = sdp.getMid(mediaSection) || sdp.generateIdentifier(); + + // Reject datachannels which are not implemented yet. + if (rejected || (kind === 'application' && (protocol === 'DTLS/SCTP' || + protocol === 'UDP/DTLS/SCTP'))) { + // TODO: this is dangerous in the case where a non-rejected m-line + // becomes rejected. + pc.transceivers[sdpMLineIndex] = { + mid: mid, + kind: kind, + protocol: protocol, + rejected: true + }; + return; + } + + if (!rejected && pc.transceivers[sdpMLineIndex] && + pc.transceivers[sdpMLineIndex].rejected) { + // recycle a rejected transceiver. + pc.transceivers[sdpMLineIndex] = pc._createTransceiver(kind, true); + } + + var transceiver; + var iceGatherer; + var iceTransport; + var dtlsTransport; + var rtpReceiver; + var sendEncodingParameters; + var recvEncodingParameters; + var localCapabilities; + + var track; + // FIXME: ensure the mediaSection has rtcp-mux set. + var remoteCapabilities = sdp.parseRtpParameters(mediaSection); + var remoteIceParameters; + var remoteDtlsParameters; + if (!rejected) { + remoteIceParameters = sdp.getIceParameters(mediaSection, + sessionpart); + remoteDtlsParameters = sdp.getDtlsParameters(mediaSection, + sessionpart); + remoteDtlsParameters.role = 'client'; + } + recvEncodingParameters = + sdp.parseRtpEncodingParameters(mediaSection); + + var rtcpParameters = sdp.parseRtcpParameters(mediaSection); + + var isComplete = sdp.matchPrefix(mediaSection, + 'a=end-of-candidates', sessionpart).length > 0; + var cands = sdp.matchPrefix(mediaSection, 'a=candidate:') + .map(function(cand) { + return sdp.parseCandidate(cand); + }) + .filter(function(cand) { + return cand.component === 1; + }); + + // Check if we can use BUNDLE and dispose transports. + if ((description.type === 'offer' || description.type === 'answer') && + !rejected && usingBundle && sdpMLineIndex > 0 && + pc.transceivers[sdpMLineIndex]) { + pc._disposeIceAndDtlsTransports(sdpMLineIndex); + pc.transceivers[sdpMLineIndex].iceGatherer = + pc.transceivers[0].iceGatherer; + pc.transceivers[sdpMLineIndex].iceTransport = + pc.transceivers[0].iceTransport; + pc.transceivers[sdpMLineIndex].dtlsTransport = + pc.transceivers[0].dtlsTransport; + if (pc.transceivers[sdpMLineIndex].rtpSender) { + pc.transceivers[sdpMLineIndex].rtpSender.setTransport( + pc.transceivers[0].dtlsTransport); + } + if (pc.transceivers[sdpMLineIndex].rtpReceiver) { + pc.transceivers[sdpMLineIndex].rtpReceiver.setTransport( + pc.transceivers[0].dtlsTransport); + } + } + if (description.type === 'offer' && !rejected) { + transceiver = pc.transceivers[sdpMLineIndex] || + pc._createTransceiver(kind); + transceiver.mid = mid; + + if (!transceiver.iceGatherer) { + transceiver.iceGatherer = pc._createIceGatherer(sdpMLineIndex, + usingBundle); + } + + if (cands.length && transceiver.iceTransport.state === 'new') { + if (isComplete && (!usingBundle || sdpMLineIndex === 0)) { + transceiver.iceTransport.setRemoteCandidates(cands); + } else { + cands.forEach(function(candidate) { + maybeAddCandidate(transceiver.iceTransport, candidate); + }); + } + } + + localCapabilities = window.RTCRtpReceiver.getCapabilities(kind); + + // filter RTX until additional stuff needed for RTX is implemented + // in adapter.js + if (edgeVersion < 15019) { + localCapabilities.codecs = localCapabilities.codecs.filter( + function(codec) { + return codec.name !== 'rtx'; + }); + } + + sendEncodingParameters = transceiver.sendEncodingParameters || [{ + ssrc: (2 * sdpMLineIndex + 2) * 1001 + }]; + + // TODO: rewrite to use http://w3c.github.io/webrtc-pc/#set-associated-remote-streams + var isNewTrack = false; + if (direction === 'sendrecv' || direction === 'sendonly') { + isNewTrack = !transceiver.rtpReceiver; + rtpReceiver = transceiver.rtpReceiver || + new window.RTCRtpReceiver(transceiver.dtlsTransport, kind); + + if (isNewTrack) { + var stream; + track = rtpReceiver.track; + // FIXME: does not work with Plan B. + if (remoteMsid && remoteMsid.stream === '-') ; else if (remoteMsid) { + if (!streams[remoteMsid.stream]) { + streams[remoteMsid.stream] = new window.MediaStream(); + Object.defineProperty(streams[remoteMsid.stream], 'id', { + get: function() { + return remoteMsid.stream; + } + }); + } + Object.defineProperty(track, 'id', { + get: function() { + return remoteMsid.track; + } + }); + stream = streams[remoteMsid.stream]; + } else { + if (!streams.default) { + streams.default = new window.MediaStream(); + } + stream = streams.default; + } + if (stream) { + addTrackToStreamAndFireEvent(track, stream); + transceiver.associatedRemoteMediaStreams.push(stream); + } + receiverList.push([track, rtpReceiver, stream]); + } + } else if (transceiver.rtpReceiver && transceiver.rtpReceiver.track) { + transceiver.associatedRemoteMediaStreams.forEach(function(s) { + var nativeTrack = s.getTracks().find(function(t) { + return t.id === transceiver.rtpReceiver.track.id; + }); + if (nativeTrack) { + removeTrackFromStreamAndFireEvent(nativeTrack, s); + } + }); + transceiver.associatedRemoteMediaStreams = []; + } + + transceiver.localCapabilities = localCapabilities; + transceiver.remoteCapabilities = remoteCapabilities; + transceiver.rtpReceiver = rtpReceiver; + transceiver.rtcpParameters = rtcpParameters; + transceiver.sendEncodingParameters = sendEncodingParameters; + transceiver.recvEncodingParameters = recvEncodingParameters; + + // Start the RTCRtpReceiver now. The RTPSender is started in + // setLocalDescription. + pc._transceive(pc.transceivers[sdpMLineIndex], + false, + isNewTrack); + } else if (description.type === 'answer' && !rejected) { + transceiver = pc.transceivers[sdpMLineIndex]; + iceGatherer = transceiver.iceGatherer; + iceTransport = transceiver.iceTransport; + dtlsTransport = transceiver.dtlsTransport; + rtpReceiver = transceiver.rtpReceiver; + sendEncodingParameters = transceiver.sendEncodingParameters; + localCapabilities = transceiver.localCapabilities; + + pc.transceivers[sdpMLineIndex].recvEncodingParameters = + recvEncodingParameters; + pc.transceivers[sdpMLineIndex].remoteCapabilities = + remoteCapabilities; + pc.transceivers[sdpMLineIndex].rtcpParameters = rtcpParameters; + + if (cands.length && iceTransport.state === 'new') { + if ((isIceLite || isComplete) && + (!usingBundle || sdpMLineIndex === 0)) { + iceTransport.setRemoteCandidates(cands); + } else { + cands.forEach(function(candidate) { + maybeAddCandidate(transceiver.iceTransport, candidate); + }); + } + } + + if (!usingBundle || sdpMLineIndex === 0) { + if (iceTransport.state === 'new') { + iceTransport.start(iceGatherer, remoteIceParameters, + 'controlling'); + } + if (dtlsTransport.state === 'new') { + dtlsTransport.start(remoteDtlsParameters); + } + } + + // If the offer contained RTX but the answer did not, + // remove RTX from sendEncodingParameters. + var commonCapabilities = getCommonCapabilities( + transceiver.localCapabilities, + transceiver.remoteCapabilities); + + var hasRtx = commonCapabilities.codecs.filter(function(c) { + return c.name.toLowerCase() === 'rtx'; + }).length; + if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) { + delete transceiver.sendEncodingParameters[0].rtx; + } + + pc._transceive(transceiver, + direction === 'sendrecv' || direction === 'recvonly', + direction === 'sendrecv' || direction === 'sendonly'); + + // TODO: rewrite to use http://w3c.github.io/webrtc-pc/#set-associated-remote-streams + if (rtpReceiver && + (direction === 'sendrecv' || direction === 'sendonly')) { + track = rtpReceiver.track; + if (remoteMsid) { + if (!streams[remoteMsid.stream]) { + streams[remoteMsid.stream] = new window.MediaStream(); + } + addTrackToStreamAndFireEvent(track, streams[remoteMsid.stream]); + receiverList.push([track, rtpReceiver, streams[remoteMsid.stream]]); + } else { + if (!streams.default) { + streams.default = new window.MediaStream(); + } + addTrackToStreamAndFireEvent(track, streams.default); + receiverList.push([track, rtpReceiver, streams.default]); + } + } else { + // FIXME: actually the receiver should be created later. + delete transceiver.rtpReceiver; + } + } + }); + + if (pc._dtlsRole === undefined) { + pc._dtlsRole = description.type === 'offer' ? 'active' : 'passive'; + } + + pc._remoteDescription = { + type: description.type, + sdp: description.sdp + }; + if (description.type === 'offer') { + pc._updateSignalingState('have-remote-offer'); + } else { + pc._updateSignalingState('stable'); + } + Object.keys(streams).forEach(function(sid) { + var stream = streams[sid]; + if (stream.getTracks().length) { + if (pc.remoteStreams.indexOf(stream) === -1) { + pc.remoteStreams.push(stream); + var event = new Event('addstream'); + event.stream = stream; + window.setTimeout(function() { + pc._dispatchEvent('addstream', event); + }); + } + + receiverList.forEach(function(item) { + var track = item[0]; + var receiver = item[1]; + if (stream.id !== item[2].id) { + return; + } + fireAddTrack(pc, track, receiver, [stream]); + }); + } + }); + receiverList.forEach(function(item) { + if (item[2]) { + return; + } + fireAddTrack(pc, item[0], item[1], []); + }); + + // check whether addIceCandidate({}) was called within four seconds after + // setRemoteDescription. + window.setTimeout(function() { + if (!(pc && pc.transceivers)) { + return; + } + pc.transceivers.forEach(function(transceiver) { + if (transceiver.iceTransport && + transceiver.iceTransport.state === 'new' && + transceiver.iceTransport.getRemoteCandidates().length > 0) { + console.warn('Timeout for addRemoteCandidate. Consider sending ' + + 'an end-of-candidates notification'); + transceiver.iceTransport.addRemoteCandidate({}); + } + }); + }, 4000); + + return Promise.resolve(); + }; + + RTCPeerConnection.prototype.close = function() { + this.transceivers.forEach(function(transceiver) { + /* not yet + if (transceiver.iceGatherer) { + transceiver.iceGatherer.close(); + } + */ + if (transceiver.iceTransport) { + transceiver.iceTransport.stop(); + } + if (transceiver.dtlsTransport) { + transceiver.dtlsTransport.stop(); + } + if (transceiver.rtpSender) { + transceiver.rtpSender.stop(); + } + if (transceiver.rtpReceiver) { + transceiver.rtpReceiver.stop(); + } + }); + // FIXME: clean up tracks, local streams, remote streams, etc + this._isClosed = true; + this._updateSignalingState('closed'); + }; + + // Update the signaling state. + RTCPeerConnection.prototype._updateSignalingState = function(newState) { + this.signalingState = newState; + var event = new Event('signalingstatechange'); + this._dispatchEvent('signalingstatechange', event); + }; + + // Determine whether to fire the negotiationneeded event. + RTCPeerConnection.prototype._maybeFireNegotiationNeeded = function() { + var pc = this; + if (this.signalingState !== 'stable' || this.needNegotiation === true) { + return; + } + this.needNegotiation = true; + window.setTimeout(function() { + if (pc.needNegotiation) { + pc.needNegotiation = false; + var event = new Event('negotiationneeded'); + pc._dispatchEvent('negotiationneeded', event); + } + }, 0); + }; + + // Update the ice connection state. + RTCPeerConnection.prototype._updateIceConnectionState = function() { + var newState; + var states = { + 'new': 0, + closed: 0, + checking: 0, + connected: 0, + completed: 0, + disconnected: 0, + failed: 0 + }; + this.transceivers.forEach(function(transceiver) { + if (transceiver.iceTransport && !transceiver.rejected) { + states[transceiver.iceTransport.state]++; + } + }); + + newState = 'new'; + if (states.failed > 0) { + newState = 'failed'; + } else if (states.checking > 0) { + newState = 'checking'; + } else if (states.disconnected > 0) { + newState = 'disconnected'; + } else if (states.new > 0) { + newState = 'new'; + } else if (states.connected > 0) { + newState = 'connected'; + } else if (states.completed > 0) { + newState = 'completed'; + } + + if (newState !== this.iceConnectionState) { + this.iceConnectionState = newState; + var event = new Event('iceconnectionstatechange'); + this._dispatchEvent('iceconnectionstatechange', event); + } + }; + + // Update the connection state. + RTCPeerConnection.prototype._updateConnectionState = function() { + var newState; + var states = { + 'new': 0, + closed: 0, + connecting: 0, + connected: 0, + completed: 0, + disconnected: 0, + failed: 0 + }; + this.transceivers.forEach(function(transceiver) { + if (transceiver.iceTransport && transceiver.dtlsTransport && + !transceiver.rejected) { + states[transceiver.iceTransport.state]++; + states[transceiver.dtlsTransport.state]++; + } + }); + // ICETransport.completed and connected are the same for this purpose. + states.connected += states.completed; + + newState = 'new'; + if (states.failed > 0) { + newState = 'failed'; + } else if (states.connecting > 0) { + newState = 'connecting'; + } else if (states.disconnected > 0) { + newState = 'disconnected'; + } else if (states.new > 0) { + newState = 'new'; + } else if (states.connected > 0) { + newState = 'connected'; + } + + if (newState !== this.connectionState) { + this.connectionState = newState; + var event = new Event('connectionstatechange'); + this._dispatchEvent('connectionstatechange', event); + } + }; + + RTCPeerConnection.prototype.createOffer = function() { + var pc = this; + + if (pc._isClosed) { + return Promise.reject(makeError('InvalidStateError', + 'Can not call createOffer after close')); + } + + var numAudioTracks = pc.transceivers.filter(function(t) { + return t.kind === 'audio'; + }).length; + var numVideoTracks = pc.transceivers.filter(function(t) { + return t.kind === 'video'; + }).length; + + // Determine number of audio and video tracks we need to send/recv. + var offerOptions = arguments[0]; + if (offerOptions) { + // Reject Chrome legacy constraints. + if (offerOptions.mandatory || offerOptions.optional) { + throw new TypeError( + 'Legacy mandatory/optional constraints not supported.'); + } + if (offerOptions.offerToReceiveAudio !== undefined) { + if (offerOptions.offerToReceiveAudio === true) { + numAudioTracks = 1; + } else if (offerOptions.offerToReceiveAudio === false) { + numAudioTracks = 0; + } else { + numAudioTracks = offerOptions.offerToReceiveAudio; + } + } + if (offerOptions.offerToReceiveVideo !== undefined) { + if (offerOptions.offerToReceiveVideo === true) { + numVideoTracks = 1; + } else if (offerOptions.offerToReceiveVideo === false) { + numVideoTracks = 0; + } else { + numVideoTracks = offerOptions.offerToReceiveVideo; + } + } + } + + pc.transceivers.forEach(function(transceiver) { + if (transceiver.kind === 'audio') { + numAudioTracks--; + if (numAudioTracks < 0) { + transceiver.wantReceive = false; + } + } else if (transceiver.kind === 'video') { + numVideoTracks--; + if (numVideoTracks < 0) { + transceiver.wantReceive = false; + } + } + }); + + // Create M-lines for recvonly streams. + while (numAudioTracks > 0 || numVideoTracks > 0) { + if (numAudioTracks > 0) { + pc._createTransceiver('audio'); + numAudioTracks--; + } + if (numVideoTracks > 0) { + pc._createTransceiver('video'); + numVideoTracks--; + } + } + + var sdp$1 = sdp.writeSessionBoilerplate(pc._sdpSessionId, + pc._sdpSessionVersion++); + pc.transceivers.forEach(function(transceiver, sdpMLineIndex) { + // For each track, create an ice gatherer, ice transport, + // dtls transport, potentially rtpsender and rtpreceiver. + var track = transceiver.track; + var kind = transceiver.kind; + var mid = transceiver.mid || sdp.generateIdentifier(); + transceiver.mid = mid; + + if (!transceiver.iceGatherer) { + transceiver.iceGatherer = pc._createIceGatherer(sdpMLineIndex, + pc.usingBundle); + } + + var localCapabilities = window.RTCRtpSender.getCapabilities(kind); + // filter RTX until additional stuff needed for RTX is implemented + // in adapter.js + if (edgeVersion < 15019) { + localCapabilities.codecs = localCapabilities.codecs.filter( + function(codec) { + return codec.name !== 'rtx'; + }); + } + localCapabilities.codecs.forEach(function(codec) { + // work around https://bugs.chromium.org/p/webrtc/issues/detail?id=6552 + // by adding level-asymmetry-allowed=1 + if (codec.name === 'H264' && + codec.parameters['level-asymmetry-allowed'] === undefined) { + codec.parameters['level-asymmetry-allowed'] = '1'; + } + + // for subsequent offers, we might have to re-use the payload + // type of the last offer. + if (transceiver.remoteCapabilities && + transceiver.remoteCapabilities.codecs) { + transceiver.remoteCapabilities.codecs.forEach(function(remoteCodec) { + if (codec.name.toLowerCase() === remoteCodec.name.toLowerCase() && + codec.clockRate === remoteCodec.clockRate) { + codec.preferredPayloadType = remoteCodec.payloadType; + } + }); + } + }); + localCapabilities.headerExtensions.forEach(function(hdrExt) { + var remoteExtensions = transceiver.remoteCapabilities && + transceiver.remoteCapabilities.headerExtensions || []; + remoteExtensions.forEach(function(rHdrExt) { + if (hdrExt.uri === rHdrExt.uri) { + hdrExt.id = rHdrExt.id; + } + }); + }); + + // generate an ssrc now, to be used later in rtpSender.send + var sendEncodingParameters = transceiver.sendEncodingParameters || [{ + ssrc: (2 * sdpMLineIndex + 1) * 1001 + }]; + if (track) { + // add RTX + if (edgeVersion >= 15019 && kind === 'video' && + !sendEncodingParameters[0].rtx) { + sendEncodingParameters[0].rtx = { + ssrc: sendEncodingParameters[0].ssrc + 1 + }; + } + } + + if (transceiver.wantReceive) { + transceiver.rtpReceiver = new window.RTCRtpReceiver( + transceiver.dtlsTransport, kind); + } + + transceiver.localCapabilities = localCapabilities; + transceiver.sendEncodingParameters = sendEncodingParameters; + }); + + // always offer BUNDLE and dispose on return if not supported. + if (pc._config.bundlePolicy !== 'max-compat') { + sdp$1 += 'a=group:BUNDLE ' + pc.transceivers.map(function(t) { + return t.mid; + }).join(' ') + '\r\n'; + } + sdp$1 += 'a=ice-options:trickle\r\n'; + + pc.transceivers.forEach(function(transceiver, sdpMLineIndex) { + sdp$1 += writeMediaSection(transceiver, transceiver.localCapabilities, + 'offer', transceiver.stream, pc._dtlsRole); + sdp$1 += 'a=rtcp-rsize\r\n'; + + if (transceiver.iceGatherer && pc.iceGatheringState !== 'new' && + (sdpMLineIndex === 0 || !pc.usingBundle)) { + transceiver.iceGatherer.getLocalCandidates().forEach(function(cand) { + cand.component = 1; + sdp$1 += 'a=' + sdp.writeCandidate(cand) + '\r\n'; + }); + + if (transceiver.iceGatherer.state === 'completed') { + sdp$1 += 'a=end-of-candidates\r\n'; + } + } + }); + + var desc = new window.RTCSessionDescription({ + type: 'offer', + sdp: sdp$1 + }); + return Promise.resolve(desc); + }; + + RTCPeerConnection.prototype.createAnswer = function() { + var pc = this; + + if (pc._isClosed) { + return Promise.reject(makeError('InvalidStateError', + 'Can not call createAnswer after close')); + } + + if (!(pc.signalingState === 'have-remote-offer' || + pc.signalingState === 'have-local-pranswer')) { + return Promise.reject(makeError('InvalidStateError', + 'Can not call createAnswer in signalingState ' + pc.signalingState)); + } + + var sdp$1 = sdp.writeSessionBoilerplate(pc._sdpSessionId, + pc._sdpSessionVersion++); + if (pc.usingBundle) { + sdp$1 += 'a=group:BUNDLE ' + pc.transceivers.map(function(t) { + return t.mid; + }).join(' ') + '\r\n'; + } + sdp$1 += 'a=ice-options:trickle\r\n'; + + var mediaSectionsInOffer = sdp.getMediaSections( + pc._remoteDescription.sdp).length; + pc.transceivers.forEach(function(transceiver, sdpMLineIndex) { + if (sdpMLineIndex + 1 > mediaSectionsInOffer) { + return; + } + if (transceiver.rejected) { + if (transceiver.kind === 'application') { + if (transceiver.protocol === 'DTLS/SCTP') { // legacy fmt + sdp$1 += 'm=application 0 DTLS/SCTP 5000\r\n'; + } else { + sdp$1 += 'm=application 0 ' + transceiver.protocol + + ' webrtc-datachannel\r\n'; + } + } else if (transceiver.kind === 'audio') { + sdp$1 += 'm=audio 0 UDP/TLS/RTP/SAVPF 0\r\n' + + 'a=rtpmap:0 PCMU/8000\r\n'; + } else if (transceiver.kind === 'video') { + sdp$1 += 'm=video 0 UDP/TLS/RTP/SAVPF 120\r\n' + + 'a=rtpmap:120 VP8/90000\r\n'; + } + sdp$1 += 'c=IN IP4 0.0.0.0\r\n' + + 'a=inactive\r\n' + + 'a=mid:' + transceiver.mid + '\r\n'; + return; + } + + // FIXME: look at direction. + if (transceiver.stream) { + var localTrack; + if (transceiver.kind === 'audio') { + localTrack = transceiver.stream.getAudioTracks()[0]; + } else if (transceiver.kind === 'video') { + localTrack = transceiver.stream.getVideoTracks()[0]; + } + if (localTrack) { + // add RTX + if (edgeVersion >= 15019 && transceiver.kind === 'video' && + !transceiver.sendEncodingParameters[0].rtx) { + transceiver.sendEncodingParameters[0].rtx = { + ssrc: transceiver.sendEncodingParameters[0].ssrc + 1 + }; + } + } + } + + // Calculate intersection of capabilities. + var commonCapabilities = getCommonCapabilities( + transceiver.localCapabilities, + transceiver.remoteCapabilities); + + var hasRtx = commonCapabilities.codecs.filter(function(c) { + return c.name.toLowerCase() === 'rtx'; + }).length; + if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) { + delete transceiver.sendEncodingParameters[0].rtx; + } + + sdp$1 += writeMediaSection(transceiver, commonCapabilities, + 'answer', transceiver.stream, pc._dtlsRole); + if (transceiver.rtcpParameters && + transceiver.rtcpParameters.reducedSize) { + sdp$1 += 'a=rtcp-rsize\r\n'; + } + }); + + var desc = new window.RTCSessionDescription({ + type: 'answer', + sdp: sdp$1 + }); + return Promise.resolve(desc); + }; + + RTCPeerConnection.prototype.addIceCandidate = function(candidate) { + var pc = this; + var sections; + if (candidate && !(candidate.sdpMLineIndex !== undefined || + candidate.sdpMid)) { + return Promise.reject(new TypeError('sdpMLineIndex or sdpMid required')); + } + + // TODO: needs to go into ops queue. + return new Promise(function(resolve, reject) { + if (!pc._remoteDescription) { + return reject(makeError('InvalidStateError', + 'Can not add ICE candidate without a remote description')); + } else if (!candidate || candidate.candidate === '') { + for (var j = 0; j < pc.transceivers.length; j++) { + if (pc.transceivers[j].rejected) { + continue; + } + pc.transceivers[j].iceTransport.addRemoteCandidate({}); + sections = sdp.getMediaSections(pc._remoteDescription.sdp); + sections[j] += 'a=end-of-candidates\r\n'; + pc._remoteDescription.sdp = + sdp.getDescription(pc._remoteDescription.sdp) + + sections.join(''); + if (pc.usingBundle) { + break; + } + } + } else { + var sdpMLineIndex = candidate.sdpMLineIndex; + if (candidate.sdpMid) { + for (var i = 0; i < pc.transceivers.length; i++) { + if (pc.transceivers[i].mid === candidate.sdpMid) { + sdpMLineIndex = i; + break; + } + } + } + var transceiver = pc.transceivers[sdpMLineIndex]; + if (transceiver) { + if (transceiver.rejected) { + return resolve(); + } + var cand = Object.keys(candidate.candidate).length > 0 ? + sdp.parseCandidate(candidate.candidate) : {}; + // Ignore Chrome's invalid candidates since Edge does not like them. + if (cand.protocol === 'tcp' && (cand.port === 0 || cand.port === 9)) { + return resolve(); + } + // Ignore RTCP candidates, we assume RTCP-MUX. + if (cand.component && cand.component !== 1) { + return resolve(); + } + // when using bundle, avoid adding candidates to the wrong + // ice transport. And avoid adding candidates added in the SDP. + if (sdpMLineIndex === 0 || (sdpMLineIndex > 0 && + transceiver.iceTransport !== pc.transceivers[0].iceTransport)) { + if (!maybeAddCandidate(transceiver.iceTransport, cand)) { + return reject(makeError('OperationError', + 'Can not add ICE candidate')); + } + } + + // update the remoteDescription. + var candidateString = candidate.candidate.trim(); + if (candidateString.indexOf('a=') === 0) { + candidateString = candidateString.substr(2); + } + sections = sdp.getMediaSections(pc._remoteDescription.sdp); + sections[sdpMLineIndex] += 'a=' + + (cand.type ? candidateString : 'end-of-candidates') + + '\r\n'; + pc._remoteDescription.sdp = + sdp.getDescription(pc._remoteDescription.sdp) + + sections.join(''); + } else { + return reject(makeError('OperationError', + 'Can not add ICE candidate')); + } + } + resolve(); + }); + }; + + RTCPeerConnection.prototype.getStats = function(selector) { + if (selector && selector instanceof window.MediaStreamTrack) { + var senderOrReceiver = null; + this.transceivers.forEach(function(transceiver) { + if (transceiver.rtpSender && + transceiver.rtpSender.track === selector) { + senderOrReceiver = transceiver.rtpSender; + } else if (transceiver.rtpReceiver && + transceiver.rtpReceiver.track === selector) { + senderOrReceiver = transceiver.rtpReceiver; + } + }); + if (!senderOrReceiver) { + throw makeError('InvalidAccessError', 'Invalid selector.'); + } + return senderOrReceiver.getStats(); + } + + var promises = []; + this.transceivers.forEach(function(transceiver) { + ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport', + 'dtlsTransport'].forEach(function(method) { + if (transceiver[method]) { + promises.push(transceiver[method].getStats()); + } + }); + }); + return Promise.all(promises).then(function(allStats) { + var results = new Map(); + allStats.forEach(function(stats) { + stats.forEach(function(stat) { + results.set(stat.id, stat); + }); + }); + return results; + }); + }; + + // fix low-level stat names and return Map instead of object. + var ortcObjects = ['RTCRtpSender', 'RTCRtpReceiver', 'RTCIceGatherer', + 'RTCIceTransport', 'RTCDtlsTransport']; + ortcObjects.forEach(function(ortcObjectName) { + var obj = window[ortcObjectName]; + if (obj && obj.prototype && obj.prototype.getStats) { + var nativeGetstats = obj.prototype.getStats; + obj.prototype.getStats = function() { + return nativeGetstats.apply(this) + .then(function(nativeStats) { + var mapStats = new Map(); + Object.keys(nativeStats).forEach(function(id) { + nativeStats[id].type = fixStatsType(nativeStats[id]); + mapStats.set(id, nativeStats[id]); + }); + return mapStats; + }); + }; + } + }); + + // legacy callback shims. Should be moved to adapter.js some days. + var methods = ['createOffer', 'createAnswer']; + methods.forEach(function(method) { + var nativeMethod = RTCPeerConnection.prototype[method]; + RTCPeerConnection.prototype[method] = function() { + var args = arguments; + if (typeof args[0] === 'function' || + typeof args[1] === 'function') { // legacy + return nativeMethod.apply(this, [arguments[2]]) + .then(function(description) { + if (typeof args[0] === 'function') { + args[0].apply(null, [description]); + } + }, function(error) { + if (typeof args[1] === 'function') { + args[1].apply(null, [error]); + } + }); + } + return nativeMethod.apply(this, arguments); + }; + }); + + methods = ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']; + methods.forEach(function(method) { + var nativeMethod = RTCPeerConnection.prototype[method]; + RTCPeerConnection.prototype[method] = function() { + var args = arguments; + if (typeof args[1] === 'function' || + typeof args[2] === 'function') { // legacy + return nativeMethod.apply(this, arguments) + .then(function() { + if (typeof args[1] === 'function') { + args[1].apply(null); + } + }, function(error) { + if (typeof args[2] === 'function') { + args[2].apply(null, [error]); + } + }); + } + return nativeMethod.apply(this, arguments); + }; + }); + + // getStats is special. It doesn't have a spec legacy method yet we support + // getStats(something, cb) without error callbacks. + ['getStats'].forEach(function(method) { + var nativeMethod = RTCPeerConnection.prototype[method]; + RTCPeerConnection.prototype[method] = function() { + var args = arguments; + if (typeof args[1] === 'function') { + return nativeMethod.apply(this, arguments) + .then(function() { + if (typeof args[1] === 'function') { + args[1].apply(null); + } + }); + } + return nativeMethod.apply(this, arguments); + }; + }); + + return RTCPeerConnection; + }; + + /* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + + function shimGetUserMedia$2(window) { + const navigator = window && window.navigator; + + const shimError_ = function(e) { + return { + name: {PermissionDeniedError: 'NotAllowedError'}[e.name] || e.name, + message: e.message, + constraint: e.constraint, + toString() { + return this.name; + } + }; + }; + + // getUserMedia error shim. + const origGetUserMedia = navigator.mediaDevices.getUserMedia. + bind(navigator.mediaDevices); + navigator.mediaDevices.getUserMedia = function(c) { + return origGetUserMedia(c).catch(e => Promise.reject(shimError_(e))); + }; + } + + /* + * Copyright (c) 2018 The adapter.js project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + + function shimGetDisplayMedia$1(window) { + if (!('getDisplayMedia' in window.navigator)) { + return; + } + if (!(window.navigator.mediaDevices)) { + return; + } + if (window.navigator.mediaDevices && + 'getDisplayMedia' in window.navigator.mediaDevices) { + return; + } + window.navigator.mediaDevices.getDisplayMedia = + window.navigator.getDisplayMedia.bind(window.navigator); + } + + /* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + + function shimPeerConnection$1(window, browserDetails) { + if (window.RTCIceGatherer) { + if (!window.RTCIceCandidate) { + window.RTCIceCandidate = function RTCIceCandidate(args) { + return args; + }; + } + if (!window.RTCSessionDescription) { + window.RTCSessionDescription = function RTCSessionDescription(args) { + return args; + }; + } + // this adds an additional event listener to MediaStrackTrack that signals + // when a tracks enabled property was changed. Workaround for a bug in + // addStream, see below. No longer required in 15025+ + if (browserDetails.version < 15025) { + const origMSTEnabled = Object.getOwnPropertyDescriptor( + window.MediaStreamTrack.prototype, 'enabled'); + Object.defineProperty(window.MediaStreamTrack.prototype, 'enabled', { + set(value) { + origMSTEnabled.set.call(this, value); + const ev = new Event('enabled'); + ev.enabled = value; + this.dispatchEvent(ev); + } + }); + } + } + + // ORTC defines the DTMF sender a bit different. + // https://github.com/w3c/ortc/issues/714 + if (window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) { + Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', { + get() { + if (this._dtmf === undefined) { + if (this.track.kind === 'audio') { + this._dtmf = new window.RTCDtmfSender(this); + } else if (this.track.kind === 'video') { + this._dtmf = null; + } + } + return this._dtmf; + } + }); + } + // Edge currently only implements the RTCDtmfSender, not the + // RTCDTMFSender alias. See http://draft.ortc.org/#rtcdtmfsender2* + if (window.RTCDtmfSender && !window.RTCDTMFSender) { + window.RTCDTMFSender = window.RTCDtmfSender; + } + + const RTCPeerConnectionShim = rtcpeerconnection(window, + browserDetails.version); + window.RTCPeerConnection = function RTCPeerConnection(config) { + if (config && config.iceServers) { + config.iceServers = filterIceServers$1(config.iceServers, + browserDetails.version); + log$1('ICE servers after filtering:', config.iceServers); + } + return new RTCPeerConnectionShim(config); + }; + window.RTCPeerConnection.prototype = RTCPeerConnectionShim.prototype; + } + + function shimReplaceTrack(window) { + // ORTC has replaceTrack -- https://github.com/w3c/ortc/issues/614 + if (window.RTCRtpSender && + !('replaceTrack' in window.RTCRtpSender.prototype)) { + window.RTCRtpSender.prototype.replaceTrack = + window.RTCRtpSender.prototype.setTrack; + } + } + + var edgeShim = /*#__PURE__*/Object.freeze({ + __proto__: null, + shimPeerConnection: shimPeerConnection$1, + shimReplaceTrack: shimReplaceTrack, + shimGetUserMedia: shimGetUserMedia$2, + shimGetDisplayMedia: shimGetDisplayMedia$1 + }); + + /* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + + function shimGetUserMedia$1(window, browserDetails) { + const navigator = window && window.navigator; + const MediaStreamTrack = window && window.MediaStreamTrack; + + navigator.getUserMedia = function(constraints, onSuccess, onError) { + // Replace Firefox 44+'s deprecation warning with unprefixed version. + deprecated('navigator.getUserMedia', + 'navigator.mediaDevices.getUserMedia'); + navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError); + }; + + if (!(browserDetails.version > 55 && + 'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) { + const remap = function(obj, a, b) { + if (a in obj && !(b in obj)) { + obj[b] = obj[a]; + delete obj[a]; + } + }; + + const nativeGetUserMedia = navigator.mediaDevices.getUserMedia. + bind(navigator.mediaDevices); + navigator.mediaDevices.getUserMedia = function(c) { + if (typeof c === 'object' && typeof c.audio === 'object') { + c = JSON.parse(JSON.stringify(c)); + remap(c.audio, 'autoGainControl', 'mozAutoGainControl'); + remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression'); + } + return nativeGetUserMedia(c); + }; + + if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) { + const nativeGetSettings = MediaStreamTrack.prototype.getSettings; + MediaStreamTrack.prototype.getSettings = function() { + const obj = nativeGetSettings.apply(this, arguments); + remap(obj, 'mozAutoGainControl', 'autoGainControl'); + remap(obj, 'mozNoiseSuppression', 'noiseSuppression'); + return obj; + }; + } + + if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) { + const nativeApplyConstraints = + MediaStreamTrack.prototype.applyConstraints; + MediaStreamTrack.prototype.applyConstraints = function(c) { + if (this.kind === 'audio' && typeof c === 'object') { + c = JSON.parse(JSON.stringify(c)); + remap(c, 'autoGainControl', 'mozAutoGainControl'); + remap(c, 'noiseSuppression', 'mozNoiseSuppression'); + } + return nativeApplyConstraints.apply(this, [c]); + }; + } + } + } + + /* + * Copyright (c) 2018 The adapter.js project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + + function shimGetDisplayMedia(window, preferredMediaSource) { + if (window.navigator.mediaDevices && + 'getDisplayMedia' in window.navigator.mediaDevices) { + return; + } + if (!(window.navigator.mediaDevices)) { + return; + } + window.navigator.mediaDevices.getDisplayMedia = + function getDisplayMedia(constraints) { + if (!(constraints && constraints.video)) { + const err = new DOMException('getDisplayMedia without video ' + + 'constraints is undefined'); + err.name = 'NotFoundError'; + // from https://heycam.github.io/webidl/#idl-DOMException-error-names + err.code = 8; + return Promise.reject(err); + } + if (constraints.video === true) { + constraints.video = {mediaSource: preferredMediaSource}; + } else { + constraints.video.mediaSource = preferredMediaSource; + } + return window.navigator.mediaDevices.getUserMedia(constraints); + }; + } + + /* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + + function shimOnTrack(window) { + if (typeof window === 'object' && window.RTCTrackEvent && + ('receiver' in window.RTCTrackEvent.prototype) && + !('transceiver' in window.RTCTrackEvent.prototype)) { + Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { + get() { + return {receiver: this.receiver}; + } + }); + } + } + + function shimPeerConnection(window, browserDetails) { + if (typeof window !== 'object' || + !(window.RTCPeerConnection || window.mozRTCPeerConnection)) { + return; // probably media.peerconnection.enabled=false in about:config + } + if (!window.RTCPeerConnection && window.mozRTCPeerConnection) { + // very basic support for old versions. + window.RTCPeerConnection = window.mozRTCPeerConnection; + } + + if (browserDetails.version < 53) { + // shim away need for obsolete RTCIceCandidate/RTCSessionDescription. + ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] + .forEach(function(method) { + const nativeMethod = window.RTCPeerConnection.prototype[method]; + const methodObj = {[method]() { + arguments[0] = new ((method === 'addIceCandidate') ? + window.RTCIceCandidate : + window.RTCSessionDescription)(arguments[0]); + return nativeMethod.apply(this, arguments); + }}; + window.RTCPeerConnection.prototype[method] = methodObj[method]; + }); + } + + const modernStatsTypes = { + inboundrtp: 'inbound-rtp', + outboundrtp: 'outbound-rtp', + candidatepair: 'candidate-pair', + localcandidate: 'local-candidate', + remotecandidate: 'remote-candidate' + }; + + const nativeGetStats = window.RTCPeerConnection.prototype.getStats; + window.RTCPeerConnection.prototype.getStats = function getStats() { + const [selector, onSucc, onErr] = arguments; + return nativeGetStats.apply(this, [selector || null]) + .then(stats => { + if (browserDetails.version < 53 && !onSucc) { + // Shim only promise getStats with spec-hyphens in type names + // Leave callback version alone; misc old uses of forEach before Map + try { + stats.forEach(stat => { + stat.type = modernStatsTypes[stat.type] || stat.type; + }); + } catch (e) { + if (e.name !== 'TypeError') { + throw e; + } + // Avoid TypeError: "type" is read-only, in old versions. 34-43ish + stats.forEach((stat, i) => { + stats.set(i, Object.assign({}, stat, { + type: modernStatsTypes[stat.type] || stat.type + })); + }); + } + } + return stats; + }) + .then(onSucc, onErr); + }; + } + + function shimSenderGetStats(window) { + if (!(typeof window === 'object' && window.RTCPeerConnection && + window.RTCRtpSender)) { + return; + } + if (window.RTCRtpSender && 'getStats' in window.RTCRtpSender.prototype) { + return; + } + const origGetSenders = window.RTCPeerConnection.prototype.getSenders; + if (origGetSenders) { + window.RTCPeerConnection.prototype.getSenders = function getSenders() { + const senders = origGetSenders.apply(this, []); + senders.forEach(sender => sender._pc = this); + return senders; + }; + } + + const origAddTrack = window.RTCPeerConnection.prototype.addTrack; + if (origAddTrack) { + window.RTCPeerConnection.prototype.addTrack = function addTrack() { + const sender = origAddTrack.apply(this, arguments); + sender._pc = this; + return sender; + }; + } + window.RTCRtpSender.prototype.getStats = function getStats() { + return this.track ? this._pc.getStats(this.track) : + Promise.resolve(new Map()); + }; + } + + function shimReceiverGetStats(window) { + if (!(typeof window === 'object' && window.RTCPeerConnection && + window.RTCRtpSender)) { + return; + } + if (window.RTCRtpSender && 'getStats' in window.RTCRtpReceiver.prototype) { + return; + } + const origGetReceivers = window.RTCPeerConnection.prototype.getReceivers; + if (origGetReceivers) { + window.RTCPeerConnection.prototype.getReceivers = function getReceivers() { + const receivers = origGetReceivers.apply(this, []); + receivers.forEach(receiver => receiver._pc = this); + return receivers; + }; + } + wrapPeerConnectionEvent(window, 'track', e => { + e.receiver._pc = e.srcElement; + return e; + }); + window.RTCRtpReceiver.prototype.getStats = function getStats() { + return this._pc.getStats(this.track); + }; + } + + function shimRemoveStream(window) { + if (!window.RTCPeerConnection || + 'removeStream' in window.RTCPeerConnection.prototype) { + return; + } + window.RTCPeerConnection.prototype.removeStream = + function removeStream(stream) { + deprecated('removeStream', 'removeTrack'); + this.getSenders().forEach(sender => { + if (sender.track && stream.getTracks().includes(sender.track)) { + this.removeTrack(sender); + } + }); + }; + } + + function shimRTCDataChannel(window) { + // rename DataChannel to RTCDataChannel (native fix in FF60): + // https://bugzilla.mozilla.org/show_bug.cgi?id=1173851 + if (window.DataChannel && !window.RTCDataChannel) { + window.RTCDataChannel = window.DataChannel; + } + } + + function shimAddTransceiver(window) { + // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647 + // Firefox ignores the init sendEncodings options passed to addTransceiver + // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918 + if (!(typeof window === 'object' && window.RTCPeerConnection)) { + return; + } + const origAddTransceiver = window.RTCPeerConnection.prototype.addTransceiver; + if (origAddTransceiver) { + window.RTCPeerConnection.prototype.addTransceiver = + function addTransceiver() { + this.setParametersPromises = []; + const initParameters = arguments[1]; + const shouldPerformCheck = initParameters && + 'sendEncodings' in initParameters; + if (shouldPerformCheck) { + // If sendEncodings params are provided, validate grammar + initParameters.sendEncodings.forEach((encodingParam) => { + if ('rid' in encodingParam) { + const ridRegex = /^[a-z0-9]{0,16}$/i; + if (!ridRegex.test(encodingParam.rid)) { + throw new TypeError('Invalid RID value provided.'); + } + } + if ('scaleResolutionDownBy' in encodingParam) { + if (!(parseFloat(encodingParam.scaleResolutionDownBy) >= 1.0)) { + throw new RangeError('scale_resolution_down_by must be >= 1.0'); + } + } + if ('maxFramerate' in encodingParam) { + if (!(parseFloat(encodingParam.maxFramerate) >= 0)) { + throw new RangeError('max_framerate must be >= 0.0'); + } + } + }); + } + const transceiver = origAddTransceiver.apply(this, arguments); + if (shouldPerformCheck) { + // Check if the init options were applied. If not we do this in an + // asynchronous way and save the promise reference in a global object. + // This is an ugly hack, but at the same time is way more robust than + // checking the sender parameters before and after the createOffer + // Also note that after the createoffer we are not 100% sure that + // the params were asynchronously applied so we might miss the + // opportunity to recreate offer. + const {sender} = transceiver; + const params = sender.getParameters(); + if (!('encodings' in params) || + // Avoid being fooled by patched getParameters() below. + (params.encodings.length === 1 && + Object.keys(params.encodings[0]).length === 0)) { + params.encodings = initParameters.sendEncodings; + sender.sendEncodings = initParameters.sendEncodings; + this.setParametersPromises.push(sender.setParameters(params) + .then(() => { + delete sender.sendEncodings; + }).catch(() => { + delete sender.sendEncodings; + }) + ); + } + } + return transceiver; + }; + } + } + + function shimGetParameters(window) { + if (!(typeof window === 'object' && window.RTCRtpSender)) { + return; + } + const origGetParameters = window.RTCRtpSender.prototype.getParameters; + if (origGetParameters) { + window.RTCRtpSender.prototype.getParameters = + function getParameters() { + const params = origGetParameters.apply(this, arguments); + if (!('encodings' in params)) { + params.encodings = [].concat(this.sendEncodings || [{}]); + } + return params; + }; + } + } + + function shimCreateOffer(window) { + // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647 + // Firefox ignores the init sendEncodings options passed to addTransceiver + // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918 + if (!(typeof window === 'object' && window.RTCPeerConnection)) { + return; + } + const origCreateOffer = window.RTCPeerConnection.prototype.createOffer; + window.RTCPeerConnection.prototype.createOffer = function createOffer() { + if (this.setParametersPromises && this.setParametersPromises.length) { + return Promise.all(this.setParametersPromises) + .then(() => { + return origCreateOffer.apply(this, arguments); + }) + .finally(() => { + this.setParametersPromises = []; + }); + } + return origCreateOffer.apply(this, arguments); + }; + } + + function shimCreateAnswer(window) { + // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647 + // Firefox ignores the init sendEncodings options passed to addTransceiver + // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918 + if (!(typeof window === 'object' && window.RTCPeerConnection)) { + return; + } + const origCreateAnswer = window.RTCPeerConnection.prototype.createAnswer; + window.RTCPeerConnection.prototype.createAnswer = function createAnswer() { + if (this.setParametersPromises && this.setParametersPromises.length) { + return Promise.all(this.setParametersPromises) + .then(() => { + return origCreateAnswer.apply(this, arguments); + }) + .finally(() => { + this.setParametersPromises = []; + }); + } + return origCreateAnswer.apply(this, arguments); + }; + } + + var firefoxShim = /*#__PURE__*/Object.freeze({ + __proto__: null, + shimOnTrack: shimOnTrack, + shimPeerConnection: shimPeerConnection, + shimSenderGetStats: shimSenderGetStats, + shimReceiverGetStats: shimReceiverGetStats, + shimRemoveStream: shimRemoveStream, + shimRTCDataChannel: shimRTCDataChannel, + shimAddTransceiver: shimAddTransceiver, + shimGetParameters: shimGetParameters, + shimCreateOffer: shimCreateOffer, + shimCreateAnswer: shimCreateAnswer, + shimGetUserMedia: shimGetUserMedia$1, + shimGetDisplayMedia: shimGetDisplayMedia + }); + + /* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + + function shimLocalStreamsAPI(window) { + if (typeof window !== 'object' || !window.RTCPeerConnection) { + return; + } + if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) { + window.RTCPeerConnection.prototype.getLocalStreams = + function getLocalStreams() { + if (!this._localStreams) { + this._localStreams = []; + } + return this._localStreams; + }; + } + if (!('addStream' in window.RTCPeerConnection.prototype)) { + const _addTrack = window.RTCPeerConnection.prototype.addTrack; + window.RTCPeerConnection.prototype.addStream = function addStream(stream) { + if (!this._localStreams) { + this._localStreams = []; + } + if (!this._localStreams.includes(stream)) { + this._localStreams.push(stream); + } + // Try to emulate Chrome's behaviour of adding in audio-video order. + // Safari orders by track id. + stream.getAudioTracks().forEach(track => _addTrack.call(this, track, + stream)); + stream.getVideoTracks().forEach(track => _addTrack.call(this, track, + stream)); + }; + + window.RTCPeerConnection.prototype.addTrack = + function addTrack(track, ...streams) { + if (streams) { + streams.forEach((stream) => { + if (!this._localStreams) { + this._localStreams = [stream]; + } else if (!this._localStreams.includes(stream)) { + this._localStreams.push(stream); + } + }); + } + return _addTrack.apply(this, arguments); + }; + } + if (!('removeStream' in window.RTCPeerConnection.prototype)) { + window.RTCPeerConnection.prototype.removeStream = + function removeStream(stream) { + if (!this._localStreams) { + this._localStreams = []; + } + const index = this._localStreams.indexOf(stream); + if (index === -1) { + return; + } + this._localStreams.splice(index, 1); + const tracks = stream.getTracks(); + this.getSenders().forEach(sender => { + if (tracks.includes(sender.track)) { + this.removeTrack(sender); + } + }); + }; + } + } + + function shimRemoteStreamsAPI(window) { + if (typeof window !== 'object' || !window.RTCPeerConnection) { + return; + } + if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) { + window.RTCPeerConnection.prototype.getRemoteStreams = + function getRemoteStreams() { + return this._remoteStreams ? this._remoteStreams : []; + }; + } + if (!('onaddstream' in window.RTCPeerConnection.prototype)) { + Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', { + get() { + return this._onaddstream; + }, + set(f) { + if (this._onaddstream) { + this.removeEventListener('addstream', this._onaddstream); + this.removeEventListener('track', this._onaddstreampoly); + } + this.addEventListener('addstream', this._onaddstream = f); + this.addEventListener('track', this._onaddstreampoly = (e) => { + e.streams.forEach(stream => { + if (!this._remoteStreams) { + this._remoteStreams = []; + } + if (this._remoteStreams.includes(stream)) { + return; + } + this._remoteStreams.push(stream); + const event = new Event('addstream'); + event.stream = stream; + this.dispatchEvent(event); + }); + }); + } + }); + const origSetRemoteDescription = + window.RTCPeerConnection.prototype.setRemoteDescription; + window.RTCPeerConnection.prototype.setRemoteDescription = + function setRemoteDescription() { + const pc = this; + if (!this._onaddstreampoly) { + this.addEventListener('track', this._onaddstreampoly = function(e) { + e.streams.forEach(stream => { + if (!pc._remoteStreams) { + pc._remoteStreams = []; + } + if (pc._remoteStreams.indexOf(stream) >= 0) { + return; + } + pc._remoteStreams.push(stream); + const event = new Event('addstream'); + event.stream = stream; + pc.dispatchEvent(event); + }); + }); + } + return origSetRemoteDescription.apply(pc, arguments); + }; + } + } + + function shimCallbacksAPI(window) { + if (typeof window !== 'object' || !window.RTCPeerConnection) { + return; + } + const prototype = window.RTCPeerConnection.prototype; + const origCreateOffer = prototype.createOffer; + const origCreateAnswer = prototype.createAnswer; + const setLocalDescription = prototype.setLocalDescription; + const setRemoteDescription = prototype.setRemoteDescription; + const addIceCandidate = prototype.addIceCandidate; + + prototype.createOffer = + function createOffer(successCallback, failureCallback) { + const options = (arguments.length >= 2) ? arguments[2] : arguments[0]; + const promise = origCreateOffer.apply(this, [options]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + + prototype.createAnswer = + function createAnswer(successCallback, failureCallback) { + const options = (arguments.length >= 2) ? arguments[2] : arguments[0]; + const promise = origCreateAnswer.apply(this, [options]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + + let withCallback = function(description, successCallback, failureCallback) { + const promise = setLocalDescription.apply(this, [description]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + prototype.setLocalDescription = withCallback; + + withCallback = function(description, successCallback, failureCallback) { + const promise = setRemoteDescription.apply(this, [description]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + prototype.setRemoteDescription = withCallback; + + withCallback = function(candidate, successCallback, failureCallback) { + const promise = addIceCandidate.apply(this, [candidate]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + prototype.addIceCandidate = withCallback; + } + + function shimGetUserMedia(window) { + const navigator = window && window.navigator; + + if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + // shim not needed in Safari 12.1 + const mediaDevices = navigator.mediaDevices; + const _getUserMedia = mediaDevices.getUserMedia.bind(mediaDevices); + navigator.mediaDevices.getUserMedia = (constraints) => { + return _getUserMedia(shimConstraints(constraints)); + }; + } + + if (!navigator.getUserMedia && navigator.mediaDevices && + navigator.mediaDevices.getUserMedia) { + navigator.getUserMedia = function getUserMedia(constraints, cb, errcb) { + navigator.mediaDevices.getUserMedia(constraints) + .then(cb, errcb); + }.bind(navigator); + } + } + + function shimConstraints(constraints) { + if (constraints && constraints.video !== undefined) { + return Object.assign({}, + constraints, + {video: compactObject(constraints.video)} + ); + } + + return constraints; + } + + function shimRTCIceServerUrls(window) { + if (!window.RTCPeerConnection) { + return; + } + // migrate from non-spec RTCIceServer.url to RTCIceServer.urls + const OrigPeerConnection = window.RTCPeerConnection; + window.RTCPeerConnection = + function RTCPeerConnection(pcConfig, pcConstraints) { + if (pcConfig && pcConfig.iceServers) { + const newIceServers = []; + for (let i = 0; i < pcConfig.iceServers.length; i++) { + let server = pcConfig.iceServers[i]; + if (!server.hasOwnProperty('urls') && + server.hasOwnProperty('url')) { + deprecated('RTCIceServer.url', 'RTCIceServer.urls'); + server = JSON.parse(JSON.stringify(server)); + server.urls = server.url; + delete server.url; + newIceServers.push(server); + } else { + newIceServers.push(pcConfig.iceServers[i]); + } + } + pcConfig.iceServers = newIceServers; + } + return new OrigPeerConnection(pcConfig, pcConstraints); + }; + window.RTCPeerConnection.prototype = OrigPeerConnection.prototype; + // wrap static methods. Currently just generateCertificate. + if ('generateCertificate' in OrigPeerConnection) { + Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { + get() { + return OrigPeerConnection.generateCertificate; + } + }); + } + } + + function shimTrackEventTransceiver(window) { + // Add event.transceiver member over deprecated event.receiver + if (typeof window === 'object' && window.RTCTrackEvent && + 'receiver' in window.RTCTrackEvent.prototype && + !('transceiver' in window.RTCTrackEvent.prototype)) { + Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { + get() { + return {receiver: this.receiver}; + } + }); + } + } + + function shimCreateOfferLegacy(window) { + const origCreateOffer = window.RTCPeerConnection.prototype.createOffer; + window.RTCPeerConnection.prototype.createOffer = + function createOffer(offerOptions) { + if (offerOptions) { + if (typeof offerOptions.offerToReceiveAudio !== 'undefined') { + // support bit values + offerOptions.offerToReceiveAudio = + !!offerOptions.offerToReceiveAudio; + } + const audioTransceiver = this.getTransceivers().find(transceiver => + transceiver.receiver.track.kind === 'audio'); + if (offerOptions.offerToReceiveAudio === false && audioTransceiver) { + if (audioTransceiver.direction === 'sendrecv') { + if (audioTransceiver.setDirection) { + audioTransceiver.setDirection('sendonly'); + } else { + audioTransceiver.direction = 'sendonly'; + } + } else if (audioTransceiver.direction === 'recvonly') { + if (audioTransceiver.setDirection) { + audioTransceiver.setDirection('inactive'); + } else { + audioTransceiver.direction = 'inactive'; + } + } + } else if (offerOptions.offerToReceiveAudio === true && + !audioTransceiver) { + this.addTransceiver('audio'); + } + + if (typeof offerOptions.offerToReceiveVideo !== 'undefined') { + // support bit values + offerOptions.offerToReceiveVideo = + !!offerOptions.offerToReceiveVideo; + } + const videoTransceiver = this.getTransceivers().find(transceiver => + transceiver.receiver.track.kind === 'video'); + if (offerOptions.offerToReceiveVideo === false && videoTransceiver) { + if (videoTransceiver.direction === 'sendrecv') { + if (videoTransceiver.setDirection) { + videoTransceiver.setDirection('sendonly'); + } else { + videoTransceiver.direction = 'sendonly'; + } + } else if (videoTransceiver.direction === 'recvonly') { + if (videoTransceiver.setDirection) { + videoTransceiver.setDirection('inactive'); + } else { + videoTransceiver.direction = 'inactive'; + } + } + } else if (offerOptions.offerToReceiveVideo === true && + !videoTransceiver) { + this.addTransceiver('video'); + } + } + return origCreateOffer.apply(this, arguments); + }; + } + + function shimAudioContext(window) { + if (typeof window !== 'object' || window.AudioContext) { + return; + } + window.AudioContext = window.webkitAudioContext; + } + + var safariShim = /*#__PURE__*/Object.freeze({ + __proto__: null, + shimLocalStreamsAPI: shimLocalStreamsAPI, + shimRemoteStreamsAPI: shimRemoteStreamsAPI, + shimCallbacksAPI: shimCallbacksAPI, + shimGetUserMedia: shimGetUserMedia, + shimConstraints: shimConstraints, + shimRTCIceServerUrls: shimRTCIceServerUrls, + shimTrackEventTransceiver: shimTrackEventTransceiver, + shimCreateOfferLegacy: shimCreateOfferLegacy, + shimAudioContext: shimAudioContext + }); + + /* + * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + + function shimRTCIceCandidate(window) { + // foundation is arbitrarily chosen as an indicator for full support for + // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface + if (!window.RTCIceCandidate || (window.RTCIceCandidate && 'foundation' in + window.RTCIceCandidate.prototype)) { + return; + } + + const NativeRTCIceCandidate = window.RTCIceCandidate; + window.RTCIceCandidate = function RTCIceCandidate(args) { + // Remove the a= which shouldn't be part of the candidate string. + if (typeof args === 'object' && args.candidate && + args.candidate.indexOf('a=') === 0) { + args = JSON.parse(JSON.stringify(args)); + args.candidate = args.candidate.substr(2); + } + + if (args.candidate && args.candidate.length) { + // Augment the native candidate with the parsed fields. + const nativeCandidate = new NativeRTCIceCandidate(args); + const parsedCandidate = sdp.parseCandidate(args.candidate); + const augmentedCandidate = Object.assign(nativeCandidate, + parsedCandidate); + + // Add a serializer that does not serialize the extra attributes. + augmentedCandidate.toJSON = function toJSON() { + return { + candidate: augmentedCandidate.candidate, + sdpMid: augmentedCandidate.sdpMid, + sdpMLineIndex: augmentedCandidate.sdpMLineIndex, + usernameFragment: augmentedCandidate.usernameFragment, + }; + }; + return augmentedCandidate; + } + return new NativeRTCIceCandidate(args); + }; + window.RTCIceCandidate.prototype = NativeRTCIceCandidate.prototype; + + // Hook up the augmented candidate in onicecandidate and + // addEventListener('icecandidate', ...) + wrapPeerConnectionEvent(window, 'icecandidate', e => { + if (e.candidate) { + Object.defineProperty(e, 'candidate', { + value: new window.RTCIceCandidate(e.candidate), + writable: 'false' + }); + } + return e; + }); + } + + function shimMaxMessageSize(window, browserDetails) { + if (!window.RTCPeerConnection) { + return; + } + + if (!('sctp' in window.RTCPeerConnection.prototype)) { + Object.defineProperty(window.RTCPeerConnection.prototype, 'sctp', { + get() { + return typeof this._sctp === 'undefined' ? null : this._sctp; + } + }); + } + + const sctpInDescription = function(description) { + if (!description || !description.sdp) { + return false; + } + const sections = sdp.splitSections(description.sdp); + sections.shift(); + return sections.some(mediaSection => { + const mLine = sdp.parseMLine(mediaSection); + return mLine && mLine.kind === 'application' + && mLine.protocol.indexOf('SCTP') !== -1; + }); + }; + + const getRemoteFirefoxVersion = function(description) { + // TODO: Is there a better solution for detecting Firefox? + const match = description.sdp.match(/mozilla...THIS_IS_SDPARTA-(\d+)/); + if (match === null || match.length < 2) { + return -1; + } + const version = parseInt(match[1], 10); + // Test for NaN (yes, this is ugly) + return version !== version ? -1 : version; + }; + + const getCanSendMaxMessageSize = function(remoteIsFirefox) { + // Every implementation we know can send at least 64 KiB. + // Note: Although Chrome is technically able to send up to 256 KiB, the + // data does not reach the other peer reliably. + // See: https://bugs.chromium.org/p/webrtc/issues/detail?id=8419 + let canSendMaxMessageSize = 65536; + if (browserDetails.browser === 'firefox') { + if (browserDetails.version < 57) { + if (remoteIsFirefox === -1) { + // FF < 57 will send in 16 KiB chunks using the deprecated PPID + // fragmentation. + canSendMaxMessageSize = 16384; + } else { + // However, other FF (and RAWRTC) can reassemble PPID-fragmented + // messages. Thus, supporting ~2 GiB when sending. + canSendMaxMessageSize = 2147483637; + } + } else if (browserDetails.version < 60) { + // Currently, all FF >= 57 will reset the remote maximum message size + // to the default value when a data channel is created at a later + // stage. :( + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831 + canSendMaxMessageSize = + browserDetails.version === 57 ? 65535 : 65536; + } else { + // FF >= 60 supports sending ~2 GiB + canSendMaxMessageSize = 2147483637; + } + } + return canSendMaxMessageSize; + }; + + const getMaxMessageSize = function(description, remoteIsFirefox) { + // Note: 65536 bytes is the default value from the SDP spec. Also, + // every implementation we know supports receiving 65536 bytes. + let maxMessageSize = 65536; + + // FF 57 has a slightly incorrect default remote max message size, so + // we need to adjust it here to avoid a failure when sending. + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1425697 + if (browserDetails.browser === 'firefox' + && browserDetails.version === 57) { + maxMessageSize = 65535; + } + + const match = sdp.matchPrefix(description.sdp, + 'a=max-message-size:'); + if (match.length > 0) { + maxMessageSize = parseInt(match[0].substr(19), 10); + } else if (browserDetails.browser === 'firefox' && + remoteIsFirefox !== -1) { + // If the maximum message size is not present in the remote SDP and + // both local and remote are Firefox, the remote peer can receive + // ~2 GiB. + maxMessageSize = 2147483637; + } + return maxMessageSize; + }; + + const origSetRemoteDescription = + window.RTCPeerConnection.prototype.setRemoteDescription; + window.RTCPeerConnection.prototype.setRemoteDescription = + function setRemoteDescription() { + this._sctp = null; + // Chrome decided to not expose .sctp in plan-b mode. + // As usual, adapter.js has to do an 'ugly worakaround' + // to cover up the mess. + if (browserDetails.browser === 'chrome' && browserDetails.version >= 76) { + const {sdpSemantics} = this.getConfiguration(); + if (sdpSemantics === 'plan-b') { + Object.defineProperty(this, 'sctp', { + get() { + return typeof this._sctp === 'undefined' ? null : this._sctp; + }, + enumerable: true, + configurable: true, + }); + } + } + + if (sctpInDescription(arguments[0])) { + // Check if the remote is FF. + const isFirefox = getRemoteFirefoxVersion(arguments[0]); + + // Get the maximum message size the local peer is capable of sending + const canSendMMS = getCanSendMaxMessageSize(isFirefox); + + // Get the maximum message size of the remote peer. + const remoteMMS = getMaxMessageSize(arguments[0], isFirefox); + + // Determine final maximum message size + let maxMessageSize; + if (canSendMMS === 0 && remoteMMS === 0) { + maxMessageSize = Number.POSITIVE_INFINITY; + } else if (canSendMMS === 0 || remoteMMS === 0) { + maxMessageSize = Math.max(canSendMMS, remoteMMS); + } else { + maxMessageSize = Math.min(canSendMMS, remoteMMS); + } + + // Create a dummy RTCSctpTransport object and the 'maxMessageSize' + // attribute. + const sctp = {}; + Object.defineProperty(sctp, 'maxMessageSize', { + get() { + return maxMessageSize; + } + }); + this._sctp = sctp; + } + + return origSetRemoteDescription.apply(this, arguments); + }; + } + + function shimSendThrowTypeError(window) { + if (!(window.RTCPeerConnection && + 'createDataChannel' in window.RTCPeerConnection.prototype)) { + return; + } + + // Note: Although Firefox >= 57 has a native implementation, the maximum + // message size can be reset for all data channels at a later stage. + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831 + + function wrapDcSend(dc, pc) { + const origDataChannelSend = dc.send; + dc.send = function send() { + const data = arguments[0]; + const length = data.length || data.size || data.byteLength; + if (dc.readyState === 'open' && + pc.sctp && length > pc.sctp.maxMessageSize) { + throw new TypeError('Message too large (can send a maximum of ' + + pc.sctp.maxMessageSize + ' bytes)'); + } + return origDataChannelSend.apply(dc, arguments); + }; + } + const origCreateDataChannel = + window.RTCPeerConnection.prototype.createDataChannel; + window.RTCPeerConnection.prototype.createDataChannel = + function createDataChannel() { + const dataChannel = origCreateDataChannel.apply(this, arguments); + wrapDcSend(dataChannel, this); + return dataChannel; + }; + wrapPeerConnectionEvent(window, 'datachannel', e => { + wrapDcSend(e.channel, e.target); + return e; + }); + } + + + /* shims RTCConnectionState by pretending it is the same as iceConnectionState. + * See https://bugs.chromium.org/p/webrtc/issues/detail?id=6145#c12 + * for why this is a valid hack in Chrome. In Firefox it is slightly incorrect + * since DTLS failures would be hidden. See + * https://bugzilla.mozilla.org/show_bug.cgi?id=1265827 + * for the Firefox tracking bug. + */ + function shimConnectionState(window) { + if (!window.RTCPeerConnection || + 'connectionState' in window.RTCPeerConnection.prototype) { + return; + } + const proto = window.RTCPeerConnection.prototype; + Object.defineProperty(proto, 'connectionState', { + get() { + return { + completed: 'connected', + checking: 'connecting' + }[this.iceConnectionState] || this.iceConnectionState; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(proto, 'onconnectionstatechange', { + get() { + return this._onconnectionstatechange || null; + }, + set(cb) { + if (this._onconnectionstatechange) { + this.removeEventListener('connectionstatechange', + this._onconnectionstatechange); + delete this._onconnectionstatechange; + } + if (cb) { + this.addEventListener('connectionstatechange', + this._onconnectionstatechange = cb); + } + }, + enumerable: true, + configurable: true + }); + + ['setLocalDescription', 'setRemoteDescription'].forEach((method) => { + const origMethod = proto[method]; + proto[method] = function() { + if (!this._connectionstatechangepoly) { + this._connectionstatechangepoly = e => { + const pc = e.target; + if (pc._lastConnectionState !== pc.connectionState) { + pc._lastConnectionState = pc.connectionState; + const newEvent = new Event('connectionstatechange', e); + pc.dispatchEvent(newEvent); + } + return e; + }; + this.addEventListener('iceconnectionstatechange', + this._connectionstatechangepoly); + } + return origMethod.apply(this, arguments); + }; + }); + } + + function removeExtmapAllowMixed(window, browserDetails) { + /* remove a=extmap-allow-mixed for webrtc.org < M71 */ + if (!window.RTCPeerConnection) { + return; + } + if (browserDetails.browser === 'chrome' && browserDetails.version >= 71) { + return; + } + if (browserDetails.browser === 'safari' && browserDetails.version >= 605) { + return; + } + const nativeSRD = window.RTCPeerConnection.prototype.setRemoteDescription; + window.RTCPeerConnection.prototype.setRemoteDescription = + function setRemoteDescription(desc) { + if (desc && desc.sdp && desc.sdp.indexOf('\na=extmap-allow-mixed') !== -1) { + const sdp = desc.sdp.split('\n').filter((line) => { + return line.trim() !== 'a=extmap-allow-mixed'; + }).join('\n'); + // Safari enforces read-only-ness of RTCSessionDescription fields. + if (window.RTCSessionDescription && + desc instanceof window.RTCSessionDescription) { + arguments[0] = new window.RTCSessionDescription({ + type: desc.type, + sdp, + }); + } else { + desc.sdp = sdp; + } + } + return nativeSRD.apply(this, arguments); + }; + } + + function shimAddIceCandidateNullOrEmpty(window, browserDetails) { + // Support for addIceCandidate(null or undefined) + // as well as addIceCandidate({candidate: "", ...}) + // https://bugs.chromium.org/p/chromium/issues/detail?id=978582 + // Note: must be called before other polyfills which change the signature. + if (!(window.RTCPeerConnection && window.RTCPeerConnection.prototype)) { + return; + } + const nativeAddIceCandidate = + window.RTCPeerConnection.prototype.addIceCandidate; + if (!nativeAddIceCandidate || nativeAddIceCandidate.length === 0) { + return; + } + window.RTCPeerConnection.prototype.addIceCandidate = + function addIceCandidate() { + if (!arguments[0]) { + if (arguments[1]) { + arguments[1].apply(null); + } + return Promise.resolve(); + } + // Firefox 68+ emits and processes {candidate: "", ...}, ignore + // in older versions. + // Native support for ignoring exists for Chrome M77+. + // Safari ignores as well, exact version unknown but works in the same + // version that also ignores addIceCandidate(null). + if (((browserDetails.browser === 'chrome' && browserDetails.version < 78) + || (browserDetails.browser === 'firefox' + && browserDetails.version < 68) + || (browserDetails.browser === 'safari')) + && arguments[0] && arguments[0].candidate === '') { + return Promise.resolve(); + } + return nativeAddIceCandidate.apply(this, arguments); + }; + } + + var commonShim = /*#__PURE__*/Object.freeze({ + __proto__: null, + shimRTCIceCandidate: shimRTCIceCandidate, + shimMaxMessageSize: shimMaxMessageSize, + shimSendThrowTypeError: shimSendThrowTypeError, + shimConnectionState: shimConnectionState, + removeExtmapAllowMixed: removeExtmapAllowMixed, + shimAddIceCandidateNullOrEmpty: shimAddIceCandidateNullOrEmpty + }); + + /* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + + // Shimming starts here. + function adapterFactory({window} = {}, options = { + shimChrome: true, + shimFirefox: true, + shimEdge: true, + shimSafari: true, + }) { + // Utils. + const logging = log$1; + const browserDetails = detectBrowser(window); + + const adapter = { + browserDetails, + commonShim, + extractVersion: extractVersion, + disableLog: disableLog, + disableWarnings: disableWarnings + }; + + // Shim browser if found. + switch (browserDetails.browser) { + case 'chrome': + if (!chromeShim || !shimPeerConnection$2 || + !options.shimChrome) { + logging('Chrome shim is not included in this adapter release.'); + return adapter; + } + if (browserDetails.version === null) { + logging('Chrome shim can not determine version, not shimming.'); + return adapter; + } + logging('adapter.js shimming chrome.'); + // Export to the adapter global object visible in the browser. + adapter.browserShim = chromeShim; + + // Must be called before shimPeerConnection. + shimAddIceCandidateNullOrEmpty(window, browserDetails); + + shimGetUserMedia$3(window, browserDetails); + shimMediaStream(window); + shimPeerConnection$2(window, browserDetails); + shimOnTrack$1(window); + shimAddTrackRemoveTrack(window, browserDetails); + shimGetSendersWithDtmf(window); + shimGetStats(window); + shimSenderReceiverGetStats(window); + fixNegotiationNeeded(window, browserDetails); + + shimRTCIceCandidate(window); + shimConnectionState(window); + shimMaxMessageSize(window, browserDetails); + shimSendThrowTypeError(window); + removeExtmapAllowMixed(window, browserDetails); + break; + case 'firefox': + if (!firefoxShim || !shimPeerConnection || + !options.shimFirefox) { + logging('Firefox shim is not included in this adapter release.'); + return adapter; + } + logging('adapter.js shimming firefox.'); + // Export to the adapter global object visible in the browser. + adapter.browserShim = firefoxShim; + + // Must be called before shimPeerConnection. + shimAddIceCandidateNullOrEmpty(window, browserDetails); + + shimGetUserMedia$1(window, browserDetails); + shimPeerConnection(window, browserDetails); + shimOnTrack(window); + shimRemoveStream(window); + shimSenderGetStats(window); + shimReceiverGetStats(window); + shimRTCDataChannel(window); + shimAddTransceiver(window); + shimGetParameters(window); + shimCreateOffer(window); + shimCreateAnswer(window); + + shimRTCIceCandidate(window); + shimConnectionState(window); + shimMaxMessageSize(window, browserDetails); + shimSendThrowTypeError(window); + break; + case 'edge': + if (!edgeShim || !shimPeerConnection$1 || !options.shimEdge) { + logging('MS edge shim is not included in this adapter release.'); + return adapter; + } + logging('adapter.js shimming edge.'); + // Export to the adapter global object visible in the browser. + adapter.browserShim = edgeShim; + + shimGetUserMedia$2(window); + shimGetDisplayMedia$1(window); + shimPeerConnection$1(window, browserDetails); + shimReplaceTrack(window); + + // the edge shim implements the full RTCIceCandidate object. + + shimMaxMessageSize(window, browserDetails); + shimSendThrowTypeError(window); + break; + case 'safari': + if (!safariShim || !options.shimSafari) { + logging('Safari shim is not included in this adapter release.'); + return adapter; + } + logging('adapter.js shimming safari.'); + // Export to the adapter global object visible in the browser. + adapter.browserShim = safariShim; + + // Must be called before shimCallbackAPI. + shimAddIceCandidateNullOrEmpty(window, browserDetails); + + shimRTCIceServerUrls(window); + shimCreateOfferLegacy(window); + shimCallbacksAPI(window); + shimLocalStreamsAPI(window); + shimRemoteStreamsAPI(window); + shimTrackEventTransceiver(window); + shimGetUserMedia(window); + shimAudioContext(window); + + shimRTCIceCandidate(window); + shimMaxMessageSize(window, browserDetails); + shimSendThrowTypeError(window); + removeExtmapAllowMixed(window, browserDetails); + break; + default: + logging('Unsupported browser!'); + break; + } + + return adapter; + } + + /* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + + adapterFactory({window: typeof window === 'undefined' ? undefined : window}); + + /** + * @class AudioTrackConstraints + * @classDesc Constraints for creating an audio MediaStreamTrack. + * @memberof Owt.Base + * @constructor + * @param {Owt.Base.AudioSourceInfo} source Source info of this audio track. + */ + class AudioTrackConstraints { + // eslint-disable-next-line require-jsdoc + constructor(source) { + if (!Object.values(AudioSourceInfo).some(v => v === source)) { + throw new TypeError('Invalid source.'); + } + /** + * @member {string} source + * @memberof Owt.Base.AudioTrackConstraints + * @desc Values could be "mic", "screen-cast", "file" or "mixed". + * @instance + */ + this.source = source; + /** + * @member {string} deviceId + * @memberof Owt.Base.AudioTrackConstraints + * @desc Do not provide deviceId if source is not "mic". + * @instance + * @see https://w3c.github.io/mediacapture-main/#def-constraint-deviceId + */ + this.deviceId = undefined; + } + } + + /** + * @class VideoTrackConstraints + * @classDesc Constraints for creating a video MediaStreamTrack. + * @memberof Owt.Base + * @constructor + * @param {Owt.Base.VideoSourceInfo} source Source info of this video track. + */ + class VideoTrackConstraints { + // eslint-disable-next-line require-jsdoc + constructor(source) { + if (!Object.values(VideoSourceInfo).some(v => v === source)) { + throw new TypeError('Invalid source.'); + } + /** + * @member {string} source + * @memberof Owt.Base.VideoTrackConstraints + * @desc Values could be "camera", "screen-cast", "file" or "mixed". + * @instance + */ + this.source = source; + /** + * @member {string} deviceId + * @memberof Owt.Base.VideoTrackConstraints + * @desc Do not provide deviceId if source is not "camera". + * @instance + * @see https://w3c.github.io/mediacapture-main/#def-constraint-deviceId + */ + + this.deviceId = undefined; + + /** + * @member {Owt.Base.Resolution} resolution + * @memberof Owt.Base.VideoTrackConstraints + * @instance + */ + this.resolution = undefined; + + /** + * @member {number} frameRate + * @memberof Owt.Base.VideoTrackConstraints + * @instance + */ + this.frameRate = undefined; + } + } + /** + * @class StreamConstraints + * @classDesc Constraints for creating a MediaStream from screen mic and camera. + * @memberof Owt.Base + * @constructor + * @param {?Owt.Base.AudioTrackConstraints} audioConstraints + * @param {?Owt.Base.VideoTrackConstraints} videoConstraints + */ + class StreamConstraints { + // eslint-disable-next-line require-jsdoc + constructor(audioConstraints = false, videoConstraints = false) { + /** + * @member {Owt.Base.MediaStreamTrackDeviceConstraintsForAudio} audio + * @memberof Owt.Base.MediaStreamDeviceConstraints + * @instance + */ + this.audio = audioConstraints; + /** + * @member {Owt.Base.MediaStreamTrackDeviceConstraintsForVideo} Video + * @memberof Owt.Base.MediaStreamDeviceConstraints + * @instance + */ + this.video = videoConstraints; + } + } + + // eslint-disable-next-line require-jsdoc + function isVideoConstrainsForScreenCast(constraints) { + return typeof constraints.video === 'object' && constraints.video.source === VideoSourceInfo.SCREENCAST; + } + + /** + * @class MediaStreamFactory + * @classDesc A factory to create MediaStream. You can also create MediaStream by yourself. + * @memberof Owt.Base + */ + class MediaStreamFactory { + /** + * @function createMediaStream + * @static + * @desc Create a MediaStream with given constraints. If you want to create a MediaStream for screen cast, please make sure both audio and video's source are "screen-cast". + * @memberof Owt.Base.MediaStreamFactory + * @return {Promise} Return a promise that is resolved when stream is successfully created, or rejected if one of the following error happened: + * - One or more parameters cannot be satisfied. + * - Specified device is busy. + * - Cannot obtain necessary permission or operation is canceled by user. + * - Video source is screen cast, while audio source is not. + * - Audio source is screen cast, while video source is disabled. + * @param {Owt.Base.StreamConstraints} constraints + */ + static createMediaStream(constraints) { + if (typeof constraints !== 'object' || !constraints.audio && !constraints.video) { + return Promise.reject(new TypeError('Invalid constrains')); + } + if (!isVideoConstrainsForScreenCast(constraints) && typeof constraints.audio === 'object' && constraints.audio.source === AudioSourceInfo.SCREENCAST) { + return Promise.reject(new TypeError('Cannot share screen without video.')); + } + if (isVideoConstrainsForScreenCast(constraints) && !isChrome() && !isFirefox()) { + return Promise.reject(new TypeError('Screen sharing only supports Chrome and Firefox.')); + } + if (isVideoConstrainsForScreenCast(constraints) && typeof constraints.audio === 'object' && constraints.audio.source !== AudioSourceInfo.SCREENCAST) { + return Promise.reject(new TypeError('Cannot capture video from screen cast while capture audio from' + ' other source.')); + } + + // Check and convert constraints. + if (!constraints.audio && !constraints.video) { + return Promise.reject(new TypeError('At least one of audio and video must be requested.')); + } + const mediaConstraints = Object.create({}); + if (typeof constraints.audio === 'object' && constraints.audio.source === AudioSourceInfo.MIC) { + mediaConstraints.audio = Object.create({}); + if (isEdge()) { + mediaConstraints.audio.deviceId = constraints.audio.deviceId; + } else { + mediaConstraints.audio.deviceId = { + exact: constraints.audio.deviceId + }; + } + } else { + if (constraints.audio.source === AudioSourceInfo.SCREENCAST) { + mediaConstraints.audio = true; + } else { + mediaConstraints.audio = constraints.audio; + } + } + if (typeof constraints.video === 'object') { + mediaConstraints.video = Object.create({}); + if (typeof constraints.video.frameRate === 'number') { + mediaConstraints.video.frameRate = constraints.video.frameRate; + } + if (constraints.video.resolution && constraints.video.resolution.width && constraints.video.resolution.height) { + if (constraints.video.source === VideoSourceInfo.SCREENCAST) { + mediaConstraints.video.width = constraints.video.resolution.width; + mediaConstraints.video.height = constraints.video.resolution.height; + } else { + mediaConstraints.video.width = Object.create({}); + mediaConstraints.video.width.exact = constraints.video.resolution.width; + mediaConstraints.video.height = Object.create({}); + mediaConstraints.video.height.exact = constraints.video.resolution.height; + } + } + if (typeof constraints.video.deviceId === 'string') { + mediaConstraints.video.deviceId = { + exact: constraints.video.deviceId + }; + } + if (isFirefox() && constraints.video.source === VideoSourceInfo.SCREENCAST) { + mediaConstraints.video.mediaSource = 'screen'; + } + } else { + mediaConstraints.video = constraints.video; + } + if (isVideoConstrainsForScreenCast(constraints)) { + return navigator.mediaDevices.getDisplayMedia(mediaConstraints); + } else { + return navigator.mediaDevices.getUserMedia(mediaConstraints); + } + } + } + + // Copyright (C) <2018> Intel Corporation + + var media = /*#__PURE__*/Object.freeze({ + __proto__: null, + AudioTrackConstraints: AudioTrackConstraints, + VideoTrackConstraints: VideoTrackConstraints, + StreamConstraints: StreamConstraints, + MediaStreamFactory: MediaStreamFactory, + AudioSourceInfo: AudioSourceInfo, + VideoSourceInfo: VideoSourceInfo, + TrackKind: TrackKind, + Resolution: Resolution + }); + + let logger; + let errorLogger; + function setLogger() { + /*eslint-disable */ + logger = console.log; + errorLogger = console.error; + /*eslint-enable */ + } + function log(message, ...optionalParams) { + if (logger) { + logger(message, ...optionalParams); + } + } + function error(message, ...optionalParams) { + if (errorLogger) { + errorLogger(message, ...optionalParams); + } + } + + class Event$1 { + constructor(type) { + this.listener = {}; + this.type = type | ''; + } + on(event, fn) { + if (!this.listener[event]) { + this.listener[event] = []; + } + this.listener[event].push(fn); + return true; + } + off(event, fn) { + if (this.listener[event]) { + var index = this.listener[event].indexOf(fn); + if (index > -1) { + this.listener[event].splice(index, 1); + } + return true; + } + return false; + } + offAll() { + this.listener = {}; + } + dispatch(event, data) { + if (this.listener[event]) { + this.listener[event].map(each => { + each.apply(null, [data]); + }); + return true; + } + return false; + } + } + + var bind = function bind(fn, thisArg) { + return function wrap() { + var args = new Array(arguments.length); + for (var i = 0; i < args.length; i++) { + args[i] = arguments[i]; + } + return fn.apply(thisArg, args); + }; + }; + + // utils is a library of generic helper functions non-specific to axios + + var toString = Object.prototype.toString; + + // eslint-disable-next-line func-names + var kindOf = (function(cache) { + // eslint-disable-next-line func-names + return function(thing) { + var str = toString.call(thing); + return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase()); + }; + })(Object.create(null)); + + function kindOfTest(type) { + type = type.toLowerCase(); + return function isKindOf(thing) { + return kindOf(thing) === type; + }; + } + + /** + * Determine if a value is an Array + * + * @param {Object} val The value to test + * @returns {boolean} True if value is an Array, otherwise false + */ + function isArray(val) { + return Array.isArray(val); + } + + /** + * Determine if a value is undefined + * + * @param {Object} val The value to test + * @returns {boolean} True if the value is undefined, otherwise false + */ + function isUndefined(val) { + return typeof val === 'undefined'; + } + + /** + * Determine if a value is a Buffer + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a Buffer, otherwise false + */ + function isBuffer(val) { + return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor) + && typeof val.constructor.isBuffer === 'function' && val.constructor.isBuffer(val); + } + + /** + * Determine if a value is an ArrayBuffer + * + * @function + * @param {Object} val The value to test + * @returns {boolean} True if value is an ArrayBuffer, otherwise false + */ + var isArrayBuffer = kindOfTest('ArrayBuffer'); + + + /** + * Determine if a value is a view on an ArrayBuffer + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a view on an ArrayBuffer, otherwise false + */ + function isArrayBufferView(val) { + var result; + if ((typeof ArrayBuffer !== 'undefined') && (ArrayBuffer.isView)) { + result = ArrayBuffer.isView(val); + } else { + result = (val) && (val.buffer) && (isArrayBuffer(val.buffer)); + } + return result; + } + + /** + * Determine if a value is a String + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a String, otherwise false + */ + function isString(val) { + return typeof val === 'string'; + } + + /** + * Determine if a value is a Number + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a Number, otherwise false + */ + function isNumber(val) { + return typeof val === 'number'; + } + + /** + * Determine if a value is an Object + * + * @param {Object} val The value to test + * @returns {boolean} True if value is an Object, otherwise false + */ + function isObject(val) { + return val !== null && typeof val === 'object'; + } + + /** + * Determine if a value is a plain Object + * + * @param {Object} val The value to test + * @return {boolean} True if value is a plain Object, otherwise false + */ + function isPlainObject(val) { + if (kindOf(val) !== 'object') { + return false; + } + + var prototype = Object.getPrototypeOf(val); + return prototype === null || prototype === Object.prototype; + } + + /** + * Determine if a value is a Date + * + * @function + * @param {Object} val The value to test + * @returns {boolean} True if value is a Date, otherwise false + */ + var isDate = kindOfTest('Date'); + + /** + * Determine if a value is a File + * + * @function + * @param {Object} val The value to test + * @returns {boolean} True if value is a File, otherwise false + */ + var isFile = kindOfTest('File'); + + /** + * Determine if a value is a Blob + * + * @function + * @param {Object} val The value to test + * @returns {boolean} True if value is a Blob, otherwise false + */ + var isBlob = kindOfTest('Blob'); + + /** + * Determine if a value is a FileList + * + * @function + * @param {Object} val The value to test + * @returns {boolean} True if value is a File, otherwise false + */ + var isFileList = kindOfTest('FileList'); + + /** + * Determine if a value is a Function + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a Function, otherwise false + */ + function isFunction(val) { + return toString.call(val) === '[object Function]'; + } + + /** + * Determine if a value is a Stream + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a Stream, otherwise false + */ + function isStream(val) { + return isObject(val) && isFunction(val.pipe); + } + + /** + * Determine if a value is a FormData + * + * @param {Object} thing The value to test + * @returns {boolean} True if value is an FormData, otherwise false + */ + function isFormData(thing) { + var pattern = '[object FormData]'; + return thing && ( + (typeof FormData === 'function' && thing instanceof FormData) || + toString.call(thing) === pattern || + (isFunction(thing.toString) && thing.toString() === pattern) + ); + } + + /** + * Determine if a value is a URLSearchParams object + * @function + * @param {Object} val The value to test + * @returns {boolean} True if value is a URLSearchParams object, otherwise false + */ + var isURLSearchParams = kindOfTest('URLSearchParams'); + + /** + * Trim excess whitespace off the beginning and end of a string + * + * @param {String} str The String to trim + * @returns {String} The String freed of excess whitespace + */ + function trim(str) { + return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, ''); + } + + /** + * Determine if we're running in a standard browser environment + * + * This allows axios to run in a web worker, and react-native. + * Both environments support XMLHttpRequest, but not fully standard globals. + * + * web workers: + * typeof window -> undefined + * typeof document -> undefined + * + * react-native: + * navigator.product -> 'ReactNative' + * nativescript + * navigator.product -> 'NativeScript' or 'NS' + */ + function isStandardBrowserEnv() { + if (typeof navigator !== 'undefined' && (navigator.product === 'ReactNative' || + navigator.product === 'NativeScript' || + navigator.product === 'NS')) { + return false; + } + return ( + typeof window !== 'undefined' && + typeof document !== 'undefined' + ); + } + + /** + * Iterate over an Array or an Object invoking a function for each item. + * + * If `obj` is an Array callback will be called passing + * the value, index, and complete array for each item. + * + * If 'obj' is an Object callback will be called passing + * the value, key, and complete object for each property. + * + * @param {Object|Array} obj The object to iterate + * @param {Function} fn The callback to invoke for each item + */ + function forEach(obj, fn) { + // Don't bother if no value provided + if (obj === null || typeof obj === 'undefined') { + return; + } + + // Force an array if not already something iterable + if (typeof obj !== 'object') { + /*eslint no-param-reassign:0*/ + obj = [obj]; + } + + if (isArray(obj)) { + // Iterate over array values + for (var i = 0, l = obj.length; i < l; i++) { + fn.call(null, obj[i], i, obj); + } + } else { + // Iterate over object keys + for (var key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + fn.call(null, obj[key], key, obj); + } + } + } + } + + /** + * Accepts varargs expecting each argument to be an object, then + * immutably merges the properties of each object and returns result. + * + * When multiple objects contain the same key the later object in + * the arguments list will take precedence. + * + * Example: + * + * ```js + * var result = merge({foo: 123}, {foo: 456}); + * console.log(result.foo); // outputs 456 + * ``` + * + * @param {Object} obj1 Object to merge + * @returns {Object} Result of all merge properties + */ + function merge(/* obj1, obj2, obj3, ... */) { + var result = {}; + function assignValue(val, key) { + if (isPlainObject(result[key]) && isPlainObject(val)) { + result[key] = merge(result[key], val); + } else if (isPlainObject(val)) { + result[key] = merge({}, val); + } else if (isArray(val)) { + result[key] = val.slice(); + } else { + result[key] = val; + } + } + + for (var i = 0, l = arguments.length; i < l; i++) { + forEach(arguments[i], assignValue); + } + return result; + } + + /** + * Extends object a by mutably adding to it the properties of object b. + * + * @param {Object} a The object to be extended + * @param {Object} b The object to copy properties from + * @param {Object} thisArg The object to bind function to + * @return {Object} The resulting value of object a + */ + function extend(a, b, thisArg) { + forEach(b, function assignValue(val, key) { + if (thisArg && typeof val === 'function') { + a[key] = bind(val, thisArg); + } else { + a[key] = val; + } + }); + return a; + } + + /** + * Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) + * + * @param {string} content with BOM + * @return {string} content value without BOM + */ + function stripBOM(content) { + if (content.charCodeAt(0) === 0xFEFF) { + content = content.slice(1); + } + return content; + } + + /** + * Inherit the prototype methods from one constructor into another + * @param {function} constructor + * @param {function} superConstructor + * @param {object} [props] + * @param {object} [descriptors] + */ + + function inherits(constructor, superConstructor, props, descriptors) { + constructor.prototype = Object.create(superConstructor.prototype, descriptors); + constructor.prototype.constructor = constructor; + props && Object.assign(constructor.prototype, props); + } + + /** + * Resolve object with deep prototype chain to a flat object + * @param {Object} sourceObj source object + * @param {Object} [destObj] + * @param {Function} [filter] + * @returns {Object} + */ + + function toFlatObject(sourceObj, destObj, filter) { + var props; + var i; + var prop; + var merged = {}; + + destObj = destObj || {}; + + do { + props = Object.getOwnPropertyNames(sourceObj); + i = props.length; + while (i-- > 0) { + prop = props[i]; + if (!merged[prop]) { + destObj[prop] = sourceObj[prop]; + merged[prop] = true; + } + } + sourceObj = Object.getPrototypeOf(sourceObj); + } while (sourceObj && (!filter || filter(sourceObj, destObj)) && sourceObj !== Object.prototype); + + return destObj; + } + + /* + * determines whether a string ends with the characters of a specified string + * @param {String} str + * @param {String} searchString + * @param {Number} [position= 0] + * @returns {boolean} + */ + function endsWith(str, searchString, position) { + str = String(str); + if (position === undefined || position > str.length) { + position = str.length; + } + position -= searchString.length; + var lastIndex = str.indexOf(searchString, position); + return lastIndex !== -1 && lastIndex === position; + } + + + /** + * Returns new array from array like object + * @param {*} [thing] + * @returns {Array} + */ + function toArray(thing) { + if (!thing) return null; + var i = thing.length; + if (isUndefined(i)) return null; + var arr = new Array(i); + while (i-- > 0) { + arr[i] = thing[i]; + } + return arr; + } + + // eslint-disable-next-line func-names + var isTypedArray = (function(TypedArray) { + // eslint-disable-next-line func-names + return function(thing) { + return TypedArray && thing instanceof TypedArray; + }; + })(typeof Uint8Array !== 'undefined' && Object.getPrototypeOf(Uint8Array)); + + var utils = { + isArray: isArray, + isArrayBuffer: isArrayBuffer, + isBuffer: isBuffer, + isFormData: isFormData, + isArrayBufferView: isArrayBufferView, + isString: isString, + isNumber: isNumber, + isObject: isObject, + isPlainObject: isPlainObject, + isUndefined: isUndefined, + isDate: isDate, + isFile: isFile, + isBlob: isBlob, + isFunction: isFunction, + isStream: isStream, + isURLSearchParams: isURLSearchParams, + isStandardBrowserEnv: isStandardBrowserEnv, + forEach: forEach, + merge: merge, + extend: extend, + trim: trim, + stripBOM: stripBOM, + inherits: inherits, + toFlatObject: toFlatObject, + kindOf: kindOf, + kindOfTest: kindOfTest, + endsWith: endsWith, + toArray: toArray, + isTypedArray: isTypedArray, + isFileList: isFileList + }; + + function encode(val) { + return encodeURIComponent(val). + replace(/%3A/gi, ':'). + replace(/%24/g, '$'). + replace(/%2C/gi, ','). + replace(/%20/g, '+'). + replace(/%5B/gi, '['). + replace(/%5D/gi, ']'); + } + + /** + * Build a URL by appending params to the end + * + * @param {string} url The base of the url (e.g., http://www.google.com) + * @param {object} [params] The params to be appended + * @returns {string} The formatted url + */ + var buildURL = function buildURL(url, params, paramsSerializer) { + /*eslint no-param-reassign:0*/ + if (!params) { + return url; + } + + var serializedParams; + if (paramsSerializer) { + serializedParams = paramsSerializer(params); + } else if (utils.isURLSearchParams(params)) { + serializedParams = params.toString(); + } else { + var parts = []; + + utils.forEach(params, function serialize(val, key) { + if (val === null || typeof val === 'undefined') { + return; + } + + if (utils.isArray(val)) { + key = key + '[]'; + } else { + val = [val]; + } + + utils.forEach(val, function parseValue(v) { + if (utils.isDate(v)) { + v = v.toISOString(); + } else if (utils.isObject(v)) { + v = JSON.stringify(v); + } + parts.push(encode(key) + '=' + encode(v)); + }); + }); + + serializedParams = parts.join('&'); + } + + if (serializedParams) { + var hashmarkIndex = url.indexOf('#'); + if (hashmarkIndex !== -1) { + url = url.slice(0, hashmarkIndex); + } + + url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams; + } + + return url; + }; + + function InterceptorManager() { + this.handlers = []; + } + + /** + * Add a new interceptor to the stack + * + * @param {Function} fulfilled The function to handle `then` for a `Promise` + * @param {Function} rejected The function to handle `reject` for a `Promise` + * + * @return {Number} An ID used to remove interceptor later + */ + InterceptorManager.prototype.use = function use(fulfilled, rejected, options) { + this.handlers.push({ + fulfilled: fulfilled, + rejected: rejected, + synchronous: options ? options.synchronous : false, + runWhen: options ? options.runWhen : null + }); + return this.handlers.length - 1; + }; + + /** + * Remove an interceptor from the stack + * + * @param {Number} id The ID that was returned by `use` + */ + InterceptorManager.prototype.eject = function eject(id) { + if (this.handlers[id]) { + this.handlers[id] = null; + } + }; + + /** + * Iterate over all the registered interceptors + * + * This method is particularly useful for skipping over any + * interceptors that may have become `null` calling `eject`. + * + * @param {Function} fn The function to call for each interceptor + */ + InterceptorManager.prototype.forEach = function forEach(fn) { + utils.forEach(this.handlers, function forEachHandler(h) { + if (h !== null) { + fn(h); + } + }); + }; + + var InterceptorManager_1 = InterceptorManager; + + var normalizeHeaderName = function normalizeHeaderName(headers, normalizedName) { + utils.forEach(headers, function processHeader(value, name) { + if (name !== normalizedName && name.toUpperCase() === normalizedName.toUpperCase()) { + headers[normalizedName] = value; + delete headers[name]; + } + }); + }; + + /** + * Create an Error with the specified message, config, error code, request and response. + * + * @param {string} message The error message. + * @param {string} [code] The error code (for example, 'ECONNABORTED'). + * @param {Object} [config] The config. + * @param {Object} [request] The request. + * @param {Object} [response] The response. + * @returns {Error} The created error. + */ + function AxiosError(message, code, config, request, response) { + Error.call(this); + this.message = message; + this.name = 'AxiosError'; + code && (this.code = code); + config && (this.config = config); + request && (this.request = request); + response && (this.response = response); + } + + utils.inherits(AxiosError, Error, { + toJSON: function toJSON() { + return { + // Standard + message: this.message, + name: this.name, + // Microsoft + description: this.description, + number: this.number, + // Mozilla + fileName: this.fileName, + lineNumber: this.lineNumber, + columnNumber: this.columnNumber, + stack: this.stack, + // Axios + config: this.config, + code: this.code, + status: this.response && this.response.status ? this.response.status : null + }; + } + }); + + var prototype = AxiosError.prototype; + var descriptors = {}; + + [ + 'ERR_BAD_OPTION_VALUE', + 'ERR_BAD_OPTION', + 'ECONNABORTED', + 'ETIMEDOUT', + 'ERR_NETWORK', + 'ERR_FR_TOO_MANY_REDIRECTS', + 'ERR_DEPRECATED', + 'ERR_BAD_RESPONSE', + 'ERR_BAD_REQUEST', + 'ERR_CANCELED' + // eslint-disable-next-line func-names + ].forEach(function(code) { + descriptors[code] = {value: code}; + }); + + Object.defineProperties(AxiosError, descriptors); + Object.defineProperty(prototype, 'isAxiosError', {value: true}); + + // eslint-disable-next-line func-names + AxiosError.from = function(error, code, config, request, response, customProps) { + var axiosError = Object.create(prototype); + + utils.toFlatObject(error, axiosError, function filter(obj) { + return obj !== Error.prototype; + }); + + AxiosError.call(axiosError, error.message, code, config, request, response); + + axiosError.name = error.name; + + customProps && Object.assign(axiosError, customProps); + + return axiosError; + }; + + var AxiosError_1 = AxiosError; + + var transitional = { + silentJSONParsing: true, + forcedJSONParsing: true, + clarifyTimeoutError: false + }; + + /** + * Convert a data object to FormData + * @param {Object} obj + * @param {?Object} [formData] + * @returns {Object} + **/ + + function toFormData(obj, formData) { + // eslint-disable-next-line no-param-reassign + formData = formData || new FormData(); + + var stack = []; + + function convertValue(value) { + if (value === null) return ''; + + if (utils.isDate(value)) { + return value.toISOString(); + } + + if (utils.isArrayBuffer(value) || utils.isTypedArray(value)) { + return typeof Blob === 'function' ? new Blob([value]) : Buffer.from(value); + } + + return value; + } + + function build(data, parentKey) { + if (utils.isPlainObject(data) || utils.isArray(data)) { + if (stack.indexOf(data) !== -1) { + throw Error('Circular reference detected in ' + parentKey); + } + + stack.push(data); + + utils.forEach(data, function each(value, key) { + if (utils.isUndefined(value)) return; + var fullKey = parentKey ? parentKey + '.' + key : key; + var arr; + + if (value && !parentKey && typeof value === 'object') { + if (utils.endsWith(key, '{}')) { + // eslint-disable-next-line no-param-reassign + value = JSON.stringify(value); + } else if (utils.endsWith(key, '[]') && (arr = utils.toArray(value))) { + // eslint-disable-next-line func-names + arr.forEach(function(el) { + !utils.isUndefined(el) && formData.append(fullKey, convertValue(el)); + }); + return; + } + } + + build(value, fullKey); + }); + + stack.pop(); + } else { + formData.append(parentKey, convertValue(data)); + } + } + + build(obj); + + return formData; + } + + var toFormData_1 = toFormData; + + /** + * Resolve or reject a Promise based on response status. + * + * @param {Function} resolve A function that resolves the promise. + * @param {Function} reject A function that rejects the promise. + * @param {object} response The response. + */ + var settle = function settle(resolve, reject, response) { + var validateStatus = response.config.validateStatus; + if (!response.status || !validateStatus || validateStatus(response.status)) { + resolve(response); + } else { + reject(new AxiosError_1( + '请求失败, 状态码: ' + response.status, + [AxiosError_1.ERR_BAD_REQUEST, AxiosError_1.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4], + response.config, + response.request, + response + )); + } + }; + + var cookies = ( + utils.isStandardBrowserEnv() ? + + // Standard browser envs support document.cookie + (function standardBrowserEnv() { + return { + write: function write(name, value, expires, path, domain, secure) { + var cookie = []; + cookie.push(name + '=' + encodeURIComponent(value)); + + if (utils.isNumber(expires)) { + cookie.push('expires=' + new Date(expires).toGMTString()); + } + + if (utils.isString(path)) { + cookie.push('path=' + path); + } + + if (utils.isString(domain)) { + cookie.push('domain=' + domain); + } + + if (secure === true) { + cookie.push('secure'); + } + + document.cookie = cookie.join('; '); + }, + + read: function read(name) { + var match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)')); + return (match ? decodeURIComponent(match[3]) : null); + }, + + remove: function remove(name) { + this.write(name, '', Date.now() - 86400000); + } + }; + })() : + + // Non standard browser env (web workers, react-native) lack needed support. + (function nonStandardBrowserEnv() { + return { + write: function write() {}, + read: function read() { return null; }, + remove: function remove() {} + }; + })() + ); + + /** + * Determines whether the specified URL is absolute + * + * @param {string} url The URL to test + * @returns {boolean} True if the specified URL is absolute, otherwise false + */ + var isAbsoluteURL = function isAbsoluteURL(url) { + // A URL is considered absolute if it begins with "://" or "//" (protocol-relative URL). + // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed + // by any combination of letters, digits, plus, period, or hyphen. + return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url); + }; + + /** + * Creates a new URL by combining the specified URLs + * + * @param {string} baseURL The base URL + * @param {string} relativeURL The relative URL + * @returns {string} The combined URL + */ + var combineURLs = function combineURLs(baseURL, relativeURL) { + return relativeURL + ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '') + : baseURL; + }; + + /** + * Creates a new URL by combining the baseURL with the requestedURL, + * only when the requestedURL is not already an absolute URL. + * If the requestURL is absolute, this function returns the requestedURL untouched. + * + * @param {string} baseURL The base URL + * @param {string} requestedURL Absolute or relative URL to combine + * @returns {string} The combined full path + */ + var buildFullPath = function buildFullPath(baseURL, requestedURL) { + if (baseURL && !isAbsoluteURL(requestedURL)) { + return combineURLs(baseURL, requestedURL); + } + return requestedURL; + }; + + // Headers whose duplicates are ignored by node + // c.f. https://nodejs.org/api/http.html#http_message_headers + var ignoreDuplicateOf = [ + 'age', 'authorization', 'content-length', 'content-type', 'etag', + 'expires', 'from', 'host', 'if-modified-since', 'if-unmodified-since', + 'last-modified', 'location', 'max-forwards', 'proxy-authorization', + 'referer', 'retry-after', 'user-agent' + ]; + + /** + * Parse headers into an object + * + * ``` + * Date: Wed, 27 Aug 2014 08:58:49 GMT + * Content-Type: application/json + * Connection: keep-alive + * Transfer-Encoding: chunked + * ``` + * + * @param {String} headers Headers needing to be parsed + * @returns {Object} Headers parsed into an object + */ + var parseHeaders = function parseHeaders(headers) { + var parsed = {}; + var key; + var val; + var i; + + if (!headers) { return parsed; } + + utils.forEach(headers.split('\n'), function parser(line) { + i = line.indexOf(':'); + key = utils.trim(line.substr(0, i)).toLowerCase(); + val = utils.trim(line.substr(i + 1)); + + if (key) { + if (parsed[key] && ignoreDuplicateOf.indexOf(key) >= 0) { + return; + } + if (key === 'set-cookie') { + parsed[key] = (parsed[key] ? parsed[key] : []).concat([val]); + } else { + parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; + } + } + }); + + return parsed; + }; + + var isURLSameOrigin = ( + utils.isStandardBrowserEnv() ? + + // Standard browser envs have full support of the APIs needed to test + // whether the request URL is of the same origin as current location. + (function standardBrowserEnv() { + var msie = /(msie|trident)/i.test(navigator.userAgent); + var urlParsingNode = document.createElement('a'); + var originURL; + + /** + * Parse a URL to discover it's components + * + * @param {String} url The URL to be parsed + * @returns {Object} + */ + function resolveURL(url) { + var href = url; + + if (msie) { + // IE needs attribute set twice to normalize properties + urlParsingNode.setAttribute('href', href); + href = urlParsingNode.href; + } + + urlParsingNode.setAttribute('href', href); + + // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils + return { + href: urlParsingNode.href, + protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', + host: urlParsingNode.host, + search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', + hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', + hostname: urlParsingNode.hostname, + port: urlParsingNode.port, + pathname: (urlParsingNode.pathname.charAt(0) === '/') ? + urlParsingNode.pathname : + '/' + urlParsingNode.pathname + }; + } + + originURL = resolveURL(window.location.href); + + /** + * Determine if a URL shares the same origin as the current location + * + * @param {String} requestURL The URL to test + * @returns {boolean} True if URL shares the same origin, otherwise false + */ + return function isURLSameOrigin(requestURL) { + var parsed = (utils.isString(requestURL)) ? resolveURL(requestURL) : requestURL; + return (parsed.protocol === originURL.protocol && + parsed.host === originURL.host); + }; + })() : + + // Non standard browser envs (web workers, react-native) lack needed support. + (function nonStandardBrowserEnv() { + return function isURLSameOrigin() { + return true; + }; + })() + ); + + /** + * A `CanceledError` is an object that is thrown when an operation is canceled. + * + * @class + * @param {string=} message The message. + */ + function CanceledError(message) { + // eslint-disable-next-line no-eq-null,eqeqeq + AxiosError_1.call(this, message == null ? 'canceled' : message, AxiosError_1.ERR_CANCELED); + this.name = 'CanceledError'; + } + + utils.inherits(CanceledError, AxiosError_1, { + __CANCEL__: true + }); + + var CanceledError_1 = CanceledError; + + var parseProtocol = function parseProtocol(url) { + var match = /^([-+\w]{1,25})(:?\/\/|:)/.exec(url); + return match && match[1] || ''; + }; + + var xhr = function xhrAdapter(config) { + return new Promise(function dispatchXhrRequest(resolve, reject) { + var requestData = config.data; + var requestHeaders = config.headers; + var responseType = config.responseType; + var onCanceled; + function done() { + if (config.cancelToken) { + config.cancelToken.unsubscribe(onCanceled); + } + + if (config.signal) { + config.signal.removeEventListener('abort', onCanceled); + } + } + + if (utils.isFormData(requestData) && utils.isStandardBrowserEnv()) { + delete requestHeaders['Content-Type']; // Let the browser set it + } + + var request = new XMLHttpRequest(); + + // HTTP basic authentication + if (config.auth) { + var username = config.auth.username || ''; + var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : ''; + requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password); + } + + var fullPath = buildFullPath(config.baseURL, config.url); + + request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true); + + // Set the request timeout in MS + request.timeout = config.timeout; + + function onloadend() { + if (!request) { + return; + } + // Prepare the response + var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null; + var responseData = !responseType || responseType === 'text' || responseType === 'json' ? + request.responseText : request.response; + var response = { + data: responseData, + status: request.status, + statusText: request.statusText, + headers: responseHeaders, + config: config, + request: request + }; + + settle(function _resolve(value) { + resolve(value); + done(); + }, function _reject(err) { + reject(err); + done(); + }, response); + + // Clean up request + request = null; + } + + if ('onloadend' in request) { + // Use onloadend if available + request.onloadend = onloadend; + } else { + // Listen for ready state to emulate onloadend + request.onreadystatechange = function handleLoad() { + if (!request || request.readyState !== 4) { + return; + } + + // The request errored out and we didn't get a response, this will be + // handled by onerror instead + // With one exception: request that using file: protocol, most browsers + // will return status as 0 even though it's a successful request + if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) { + return; + } + // readystate handler is calling before onerror or ontimeout handlers, + // so we should call onloadend on the next 'tick' + setTimeout(onloadend); + }; + } + + // Handle browser request cancellation (as opposed to a manual cancellation) + request.onabort = function handleAbort() { + if (!request) { + return; + } + + reject(new AxiosError_1('Request aborted', AxiosError_1.ECONNABORTED, config, request)); + + // Clean up request + request = null; + }; + + // Handle low level network errors + request.onerror = function handleError() { + // Real errors are hidden from us by the browser + // onerror should only fire if it's a network error + reject(new AxiosError_1('Network Error', AxiosError_1.ERR_NETWORK, config, request, request)); + + // Clean up request + request = null; + }; + + // Handle timeout + request.ontimeout = function handleTimeout() { + var timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded'; + var transitional$1 = config.transitional || transitional; + if (config.timeoutErrorMessage) { + timeoutErrorMessage = config.timeoutErrorMessage; + } + reject(new AxiosError_1( + timeoutErrorMessage, + transitional$1.clarifyTimeoutError ? AxiosError_1.ETIMEDOUT : AxiosError_1.ECONNABORTED, + config, + request)); + + // Clean up request + request = null; + }; + + // Add xsrf header + // This is only done if running in a standard browser environment. + // Specifically not if we're in a web worker, or react-native. + if (utils.isStandardBrowserEnv()) { + // Add xsrf header + var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ? + cookies.read(config.xsrfCookieName) : + undefined; + + if (xsrfValue) { + requestHeaders[config.xsrfHeaderName] = xsrfValue; + } + } + + // Add headers to the request + if ('setRequestHeader' in request) { + utils.forEach(requestHeaders, function setRequestHeader(val, key) { + if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') { + // Remove Content-Type if data is undefined + delete requestHeaders[key]; + } else { + // Otherwise add header to the request + request.setRequestHeader(key, val); + } + }); + } + + // Add withCredentials to request if needed + if (!utils.isUndefined(config.withCredentials)) { + request.withCredentials = !!config.withCredentials; + } + + // Add responseType to request if needed + if (responseType && responseType !== 'json') { + request.responseType = config.responseType; + } + + // Handle progress if needed + if (typeof config.onDownloadProgress === 'function') { + request.addEventListener('progress', config.onDownloadProgress); + } + + // Not all browsers support upload events + if (typeof config.onUploadProgress === 'function' && request.upload) { + request.upload.addEventListener('progress', config.onUploadProgress); + } + + if (config.cancelToken || config.signal) { + // Handle cancellation + // eslint-disable-next-line func-names + onCanceled = function(cancel) { + if (!request) { + return; + } + reject(!cancel || (cancel && cancel.type) ? new CanceledError_1() : cancel); + request.abort(); + request = null; + }; + + config.cancelToken && config.cancelToken.subscribe(onCanceled); + if (config.signal) { + config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled); + } + } + + if (!requestData) { + requestData = null; + } + + var protocol = parseProtocol(fullPath); + + if (protocol && [ 'http', 'https', 'file' ].indexOf(protocol) === -1) { + reject(new AxiosError_1('Unsupported protocol ' + protocol + ':', AxiosError_1.ERR_BAD_REQUEST, config)); + return; + } + + + // Send the request + request.send(requestData); + }); + }; + + // eslint-disable-next-line strict + var _null = null; + + var DEFAULT_CONTENT_TYPE = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + function setContentTypeIfUnset(headers, value) { + if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) { + headers['Content-Type'] = value; + } + } + + function getDefaultAdapter() { + var adapter; + if (typeof XMLHttpRequest !== 'undefined') { + // For browsers use XHR adapter + adapter = xhr; + } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { + // For node use HTTP adapter + adapter = xhr; + } + return adapter; + } + + function stringifySafely(rawValue, parser, encoder) { + if (utils.isString(rawValue)) { + try { + (parser || JSON.parse)(rawValue); + return utils.trim(rawValue); + } catch (e) { + if (e.name !== 'SyntaxError') { + throw e; + } + } + } + + return (encoder || JSON.stringify)(rawValue); + } + + var defaults = { + + transitional: transitional, + + adapter: getDefaultAdapter(), + + transformRequest: [function transformRequest(data, headers) { + normalizeHeaderName(headers, 'Accept'); + normalizeHeaderName(headers, 'Content-Type'); + + if (utils.isFormData(data) || + utils.isArrayBuffer(data) || + utils.isBuffer(data) || + utils.isStream(data) || + utils.isFile(data) || + utils.isBlob(data) + ) { + return data; + } + if (utils.isArrayBufferView(data)) { + return data.buffer; + } + if (utils.isURLSearchParams(data)) { + setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8'); + return data.toString(); + } + + var isObjectPayload = utils.isObject(data); + var contentType = headers && headers['Content-Type']; + + var isFileList; + + if ((isFileList = utils.isFileList(data)) || (isObjectPayload && contentType === 'multipart/form-data')) { + var _FormData = this.env && this.env.FormData; + return toFormData_1(isFileList ? {'files[]': data} : data, _FormData && new _FormData()); + } else if (isObjectPayload || contentType === 'application/json') { + setContentTypeIfUnset(headers, 'application/json'); + return stringifySafely(data); + } + + return data; + }], + + transformResponse: [function transformResponse(data) { + var transitional = this.transitional || defaults.transitional; + var silentJSONParsing = transitional && transitional.silentJSONParsing; + var forcedJSONParsing = transitional && transitional.forcedJSONParsing; + var strictJSONParsing = !silentJSONParsing && this.responseType === 'json'; + + if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) { + try { + return JSON.parse(data); + } catch (e) { + if (strictJSONParsing) { + if (e.name === 'SyntaxError') { + throw AxiosError_1.from(e, AxiosError_1.ERR_BAD_RESPONSE, this, null, this.response); + } + throw e; + } + } + } + + return data; + }], + + /** + * A timeout in milliseconds to abort a request. If set to 0 (default) a + * timeout is not created. + */ + timeout: 0, + + xsrfCookieName: 'XSRF-TOKEN', + xsrfHeaderName: 'X-XSRF-TOKEN', + + maxContentLength: -1, + maxBodyLength: -1, + + env: { + FormData: _null + }, + + validateStatus: function validateStatus(status) { + return status >= 200 && status < 300; + }, + + headers: { + common: { + 'Accept': 'application/json, text/plain, */*' + } + } + }; + + utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) { + defaults.headers[method] = {}; + }); + + utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) { + defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE); + }); + + var defaults_1 = defaults; + + /** + * Transform the data for a request or a response + * + * @param {Object|String} data The data to be transformed + * @param {Array} headers The headers for the request or response + * @param {Array|Function} fns A single function or Array of functions + * @returns {*} The resulting transformed data + */ + var transformData = function transformData(data, headers, fns) { + var context = this || defaults_1; + /*eslint no-param-reassign:0*/ + utils.forEach(fns, function transform(fn) { + data = fn.call(context, data, headers); + }); + + return data; + }; + + var isCancel = function isCancel(value) { + return !!(value && value.__CANCEL__); + }; + + /** + * Throws a `CanceledError` if cancellation has been requested. + */ + function throwIfCancellationRequested(config) { + if (config.cancelToken) { + config.cancelToken.throwIfRequested(); + } + + if (config.signal && config.signal.aborted) { + throw new CanceledError_1(); + } + } + + /** + * Dispatch a request to the server using the configured adapter. + * + * @param {object} config The config that is to be used for the request + * @returns {Promise} The Promise to be fulfilled + */ + var dispatchRequest = function dispatchRequest(config) { + throwIfCancellationRequested(config); + + // Ensure headers exist + config.headers = config.headers || {}; + + // Transform request data + config.data = transformData.call( + config, + config.data, + config.headers, + config.transformRequest + ); + + // Flatten headers + config.headers = utils.merge( + config.headers.common || {}, + config.headers[config.method] || {}, + config.headers + ); + + utils.forEach( + ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'], + function cleanHeaderConfig(method) { + delete config.headers[method]; + } + ); + + var adapter = config.adapter || defaults_1.adapter; + + return adapter(config).then(function onAdapterResolution(response) { + throwIfCancellationRequested(config); + + // Transform response data + response.data = transformData.call( + config, + response.data, + response.headers, + config.transformResponse + ); + + return response; + }, function onAdapterRejection(reason) { + if (!isCancel(reason)) { + throwIfCancellationRequested(config); + + // Transform response data + if (reason && reason.response) { + reason.response.data = transformData.call( + config, + reason.response.data, + reason.response.headers, + config.transformResponse + ); + } + } + + return Promise.reject(reason); + }); + }; + + /** + * Config-specific merge-function which creates a new config-object + * by merging two configuration objects together. + * + * @param {Object} config1 + * @param {Object} config2 + * @returns {Object} New object resulting from merging config2 to config1 + */ + var mergeConfig = function mergeConfig(config1, config2) { + // eslint-disable-next-line no-param-reassign + config2 = config2 || {}; + var config = {}; + + function getMergedValue(target, source) { + if (utils.isPlainObject(target) && utils.isPlainObject(source)) { + return utils.merge(target, source); + } else if (utils.isPlainObject(source)) { + return utils.merge({}, source); + } else if (utils.isArray(source)) { + return source.slice(); + } + return source; + } + + // eslint-disable-next-line consistent-return + function mergeDeepProperties(prop) { + if (!utils.isUndefined(config2[prop])) { + return getMergedValue(config1[prop], config2[prop]); + } else if (!utils.isUndefined(config1[prop])) { + return getMergedValue(undefined, config1[prop]); + } + } + + // eslint-disable-next-line consistent-return + function valueFromConfig2(prop) { + if (!utils.isUndefined(config2[prop])) { + return getMergedValue(undefined, config2[prop]); + } + } + + // eslint-disable-next-line consistent-return + function defaultToConfig2(prop) { + if (!utils.isUndefined(config2[prop])) { + return getMergedValue(undefined, config2[prop]); + } else if (!utils.isUndefined(config1[prop])) { + return getMergedValue(undefined, config1[prop]); + } + } + + // eslint-disable-next-line consistent-return + function mergeDirectKeys(prop) { + if (prop in config2) { + return getMergedValue(config1[prop], config2[prop]); + } else if (prop in config1) { + return getMergedValue(undefined, config1[prop]); + } + } + + var mergeMap = { + 'url': valueFromConfig2, + 'method': valueFromConfig2, + 'data': valueFromConfig2, + 'baseURL': defaultToConfig2, + 'transformRequest': defaultToConfig2, + 'transformResponse': defaultToConfig2, + 'paramsSerializer': defaultToConfig2, + 'timeout': defaultToConfig2, + 'timeoutMessage': defaultToConfig2, + 'withCredentials': defaultToConfig2, + 'adapter': defaultToConfig2, + 'responseType': defaultToConfig2, + 'xsrfCookieName': defaultToConfig2, + 'xsrfHeaderName': defaultToConfig2, + 'onUploadProgress': defaultToConfig2, + 'onDownloadProgress': defaultToConfig2, + 'decompress': defaultToConfig2, + 'maxContentLength': defaultToConfig2, + 'maxBodyLength': defaultToConfig2, + 'beforeRedirect': defaultToConfig2, + 'transport': defaultToConfig2, + 'httpAgent': defaultToConfig2, + 'httpsAgent': defaultToConfig2, + 'cancelToken': defaultToConfig2, + 'socketPath': defaultToConfig2, + 'responseEncoding': defaultToConfig2, + 'validateStatus': mergeDirectKeys + }; + + utils.forEach(Object.keys(config1).concat(Object.keys(config2)), function computeConfigValue(prop) { + var merge = mergeMap[prop] || mergeDeepProperties; + var configValue = merge(prop); + (utils.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue); + }); + + return config; + }; + + var data = { + "version": "0.27.2" + }; + + var VERSION = data.version; + + + var validators$1 = {}; + + // eslint-disable-next-line func-names + ['object', 'boolean', 'number', 'function', 'string', 'symbol'].forEach(function(type, i) { + validators$1[type] = function validator(thing) { + return typeof thing === type || 'a' + (i < 1 ? 'n ' : ' ') + type; + }; + }); + + var deprecatedWarnings = {}; + + /** + * Transitional option validator + * @param {function|boolean?} validator - set to false if the transitional option has been removed + * @param {string?} version - deprecated version / removed since version + * @param {string?} message - some message with additional info + * @returns {function} + */ + validators$1.transitional = function transitional(validator, version, message) { + function formatMessage(opt, desc) { + return '[Axios v' + VERSION + '] Transitional option \'' + opt + '\'' + desc + (message ? '. ' + message : ''); + } + + // eslint-disable-next-line func-names + return function(value, opt, opts) { + if (validator === false) { + throw new AxiosError_1( + formatMessage(opt, ' has been removed' + (version ? ' in ' + version : '')), + AxiosError_1.ERR_DEPRECATED + ); + } + + if (version && !deprecatedWarnings[opt]) { + deprecatedWarnings[opt] = true; + // eslint-disable-next-line no-console + console.warn( + formatMessage( + opt, + ' has been deprecated since v' + version + ' and will be removed in the near future' + ) + ); + } + + return validator ? validator(value, opt, opts) : true; + }; + }; + + /** + * Assert object's properties type + * @param {object} options + * @param {object} schema + * @param {boolean?} allowUnknown + */ + + function assertOptions(options, schema, allowUnknown) { + if (typeof options !== 'object') { + throw new AxiosError_1('options must be an object', AxiosError_1.ERR_BAD_OPTION_VALUE); + } + var keys = Object.keys(options); + var i = keys.length; + while (i-- > 0) { + var opt = keys[i]; + var validator = schema[opt]; + if (validator) { + var value = options[opt]; + var result = value === undefined || validator(value, opt, options); + if (result !== true) { + throw new AxiosError_1('option ' + opt + ' must be ' + result, AxiosError_1.ERR_BAD_OPTION_VALUE); + } + continue; + } + if (allowUnknown !== true) { + throw new AxiosError_1('Unknown option ' + opt, AxiosError_1.ERR_BAD_OPTION); + } + } + } + + var validator = { + assertOptions: assertOptions, + validators: validators$1 + }; + + var validators = validator.validators; + /** + * Create a new instance of Axios + * + * @param {Object} instanceConfig The default config for the instance + */ + function Axios(instanceConfig) { + this.defaults = instanceConfig; + this.interceptors = { + request: new InterceptorManager_1(), + response: new InterceptorManager_1() + }; + } + + /** + * Dispatch a request + * + * @param {Object} config The config specific for this request (merged with this.defaults) + */ + Axios.prototype.request = function request(configOrUrl, config) { + /*eslint no-param-reassign:0*/ + // Allow for axios('example/url'[, config]) a la fetch API + if (typeof configOrUrl === 'string') { + config = config || {}; + config.url = configOrUrl; + } else { + config = configOrUrl || {}; + } + + config = mergeConfig(this.defaults, config); + + // Set config.method + if (config.method) { + config.method = config.method.toLowerCase(); + } else if (this.defaults.method) { + config.method = this.defaults.method.toLowerCase(); + } else { + config.method = 'get'; + } + + var transitional = config.transitional; + + if (transitional !== undefined) { + validator.assertOptions(transitional, { + silentJSONParsing: validators.transitional(validators.boolean), + forcedJSONParsing: validators.transitional(validators.boolean), + clarifyTimeoutError: validators.transitional(validators.boolean) + }, false); + } + + // filter out skipped interceptors + var requestInterceptorChain = []; + var synchronousRequestInterceptors = true; + this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { + if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) { + return; + } + + synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous; + + requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected); + }); + + var responseInterceptorChain = []; + this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { + responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); + }); + + var promise; + + if (!synchronousRequestInterceptors) { + var chain = [dispatchRequest, undefined]; + + Array.prototype.unshift.apply(chain, requestInterceptorChain); + chain = chain.concat(responseInterceptorChain); + + promise = Promise.resolve(config); + while (chain.length) { + promise = promise.then(chain.shift(), chain.shift()); + } + + return promise; + } + + + var newConfig = config; + while (requestInterceptorChain.length) { + var onFulfilled = requestInterceptorChain.shift(); + var onRejected = requestInterceptorChain.shift(); + try { + newConfig = onFulfilled(newConfig); + } catch (error) { + onRejected(error); + break; + } + } + + try { + promise = dispatchRequest(newConfig); + } catch (error) { + return Promise.reject(error); + } + + while (responseInterceptorChain.length) { + promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift()); + } + + return promise; + }; + + Axios.prototype.getUri = function getUri(config) { + config = mergeConfig(this.defaults, config); + var fullPath = buildFullPath(config.baseURL, config.url); + return buildURL(fullPath, config.params, config.paramsSerializer); + }; + + // Provide aliases for supported request methods + utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) { + /*eslint func-names:0*/ + Axios.prototype[method] = function(url, config) { + return this.request(mergeConfig(config || {}, { + method: method, + url: url, + data: (config || {}).data + })); + }; + }); + + utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) { + /*eslint func-names:0*/ + + function generateHTTPMethod(isForm) { + return function httpMethod(url, data, config) { + return this.request(mergeConfig(config || {}, { + method: method, + headers: isForm ? { + 'Content-Type': 'multipart/form-data' + } : {}, + url: url, + data: data + })); + }; + } + + Axios.prototype[method] = generateHTTPMethod(); + + Axios.prototype[method + 'Form'] = generateHTTPMethod(true); + }); + + var Axios_1 = Axios; + + /** + * A `CancelToken` is an object that can be used to request cancellation of an operation. + * + * @class + * @param {Function} executor The executor function. + */ + function CancelToken(executor) { + if (typeof executor !== 'function') { + throw new TypeError('executor must be a function.'); + } + + var resolvePromise; + + this.promise = new Promise(function promiseExecutor(resolve) { + resolvePromise = resolve; + }); + + var token = this; + + // eslint-disable-next-line func-names + this.promise.then(function(cancel) { + if (!token._listeners) return; + + var i; + var l = token._listeners.length; + + for (i = 0; i < l; i++) { + token._listeners[i](cancel); + } + token._listeners = null; + }); + + // eslint-disable-next-line func-names + this.promise.then = function(onfulfilled) { + var _resolve; + // eslint-disable-next-line func-names + var promise = new Promise(function(resolve) { + token.subscribe(resolve); + _resolve = resolve; + }).then(onfulfilled); + + promise.cancel = function reject() { + token.unsubscribe(_resolve); + }; + + return promise; + }; + + executor(function cancel(message) { + if (token.reason) { + // Cancellation has already been requested + return; + } + + token.reason = new CanceledError_1(message); + resolvePromise(token.reason); + }); + } + + /** + * Throws a `CanceledError` if cancellation has been requested. + */ + CancelToken.prototype.throwIfRequested = function throwIfRequested() { + if (this.reason) { + throw this.reason; + } + }; + + /** + * Subscribe to the cancel signal + */ + + CancelToken.prototype.subscribe = function subscribe(listener) { + if (this.reason) { + listener(this.reason); + return; + } + + if (this._listeners) { + this._listeners.push(listener); + } else { + this._listeners = [listener]; + } + }; + + /** + * Unsubscribe from the cancel signal + */ + + CancelToken.prototype.unsubscribe = function unsubscribe(listener) { + if (!this._listeners) { + return; + } + var index = this._listeners.indexOf(listener); + if (index !== -1) { + this._listeners.splice(index, 1); + } + }; + + /** + * Returns an object that contains a new `CancelToken` and a function that, when called, + * cancels the `CancelToken`. + */ + CancelToken.source = function source() { + var cancel; + var token = new CancelToken(function executor(c) { + cancel = c; + }); + return { + token: token, + cancel: cancel + }; + }; + + var CancelToken_1 = CancelToken; + + /** + * Syntactic sugar for invoking a function and expanding an array for arguments. + * + * Common use case would be to use `Function.prototype.apply`. + * + * ```js + * function f(x, y, z) {} + * var args = [1, 2, 3]; + * f.apply(null, args); + * ``` + * + * With `spread` this example can be re-written. + * + * ```js + * spread(function(x, y, z) {})([1, 2, 3]); + * ``` + * + * @param {Function} callback + * @returns {Function} + */ + var spread = function spread(callback) { + return function wrap(arr) { + return callback.apply(null, arr); + }; + }; + + /** + * Determines whether the payload is an error thrown by Axios + * + * @param {*} payload The value to test + * @returns {boolean} True if the payload is an error thrown by Axios, otherwise false + */ + var isAxiosError = function isAxiosError(payload) { + return utils.isObject(payload) && (payload.isAxiosError === true); + }; + + /** + * Create an instance of Axios + * + * @param {Object} defaultConfig The default config for the instance + * @return {Axios} A new instance of Axios + */ + function createInstance(defaultConfig) { + var context = new Axios_1(defaultConfig); + var instance = bind(Axios_1.prototype.request, context); + + // Copy axios.prototype to instance + utils.extend(instance, Axios_1.prototype, context); + + // Copy context to instance + utils.extend(instance, context); + + // Factory for creating new instances + instance.create = function create(instanceConfig) { + return createInstance(mergeConfig(defaultConfig, instanceConfig)); + }; + + return instance; + } + + // Create the default instance to be exported + var axios$1 = createInstance(defaults_1); + + // Expose Axios class to allow class inheritance + axios$1.Axios = Axios_1; + + // Expose Cancel & CancelToken + axios$1.CanceledError = CanceledError_1; + axios$1.CancelToken = CancelToken_1; + axios$1.isCancel = isCancel; + axios$1.VERSION = data.version; + axios$1.toFormData = toFormData_1; + + // Expose AxiosError class + axios$1.AxiosError = AxiosError_1; + + // alias for CanceledError for backward compatibility + axios$1.Cancel = axios$1.CanceledError; + + // Expose all/spread + axios$1.all = function all(promises) { + return Promise.all(promises); + }; + axios$1.spread = spread; + + // Expose isAxiosError + axios$1.isAxiosError = isAxiosError; + + var axios_1 = axios$1; + + // Allow use of default import syntax in TypeScript + var _default = axios$1; + axios_1.default = _default; + + var axios = axios_1; + + class RTCEndpoint extends Event$1 { + constructor(options) { + super('RTCPusherPlayer'); + this.TAG = '[RTCPusherPlayer]'; + let defaults = { + element: '', + // html video element + debug: false, + // if output debug log + zlmsdpUrl: '', + simulcast: false, + useCamera: true, + audioEnable: true, + videoEnable: true, + recvOnly: false, + resolution: { + w: 0, + h: 0 + }, + usedatachannel: false + }; + this.options = Object.assign({}, defaults, options); + if (this.options.debug) { + setLogger(); + } + this.e = { + onicecandidate: this._onIceCandidate.bind(this), + ontrack: this._onTrack.bind(this), + onicecandidateerror: this._onIceCandidateError.bind(this), + onconnectionstatechange: this._onconnectionstatechange.bind(this), + ondatachannelopen: this._onDataChannelOpen.bind(this), + ondatachannelmsg: this._onDataChannelMsg.bind(this), + ondatachannelerr: this._onDataChannelErr.bind(this), + ondatachannelclose: this._onDataChannelClose.bind(this) + }; + this._remoteStream = null; + this._localStream = null; + this._tracks = []; + this.pc = new RTCPeerConnection(null); + this.pc.onicecandidate = this.e.onicecandidate; + this.pc.onicecandidateerror = this.e.onicecandidateerror; + this.pc.ontrack = this.e.ontrack; + this.pc.onconnectionstatechange = this.e.onconnectionstatechange; + this.datachannel = null; + if (this.options.usedatachannel) { + this.datachannel = this.pc.createDataChannel('chat'); + this.datachannel.onclose = this.e.ondatachannelclose; + this.datachannel.onerror = this.e.ondatachannelerr; + this.datachannel.onmessage = this.e.ondatachannelmsg; + this.datachannel.onopen = this.e.ondatachannelopen; + } + if (!this.options.recvOnly && (this.options.audioEnable || this.options.videoEnable)) this.start();else this.receive(); + } + receive() { + + //debug.error(this.TAG,'this not implement'); + const AudioTransceiverInit = { + direction: 'recvonly', + sendEncodings: [] + }; + const VideoTransceiverInit = { + direction: 'recvonly', + sendEncodings: [] + }; + if (this.options.videoEnable) { + this.pc.addTransceiver('video', VideoTransceiverInit); + } + if (this.options.audioEnable) { + this.pc.addTransceiver('audio', AudioTransceiverInit); + } + this.pc.createOffer().then(desc => { + log(this.TAG, 'offer:', desc.sdp); + this.pc.setLocalDescription(desc).then(() => { + axios({ + method: 'post', + url: this.options.zlmsdpUrl, + responseType: 'json', + data: desc.sdp, + headers: { + 'Content-Type': 'text/plain;charset=utf-8' + } + }).then(response => { + let ret = response.data; //JSON.parse(response.data); + if (ret.code != 0) { + // mean failed for offer/anwser exchange + this.dispatch(Events$1.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, ret); + return; + } + let anwser = {}; + anwser.sdp = ret.sdp; + anwser.type = 'answer'; + log(this.TAG, 'answer:', ret.sdp); + this.pc.setRemoteDescription(anwser).then(() => { + log(this.TAG, 'set remote sucess'); + }).catch(e => { + error(this.TAG, e); + }); + }); + }); + }).catch(e => { + error(this.TAG, e); + }); + } + start() { + let videoConstraints = false; + let audioConstraints = false; + if (this.options.useCamera) { + if (this.options.videoEnable) videoConstraints = new VideoTrackConstraints(VideoSourceInfo.CAMERA); + if (this.options.audioEnable) audioConstraints = new AudioTrackConstraints(AudioSourceInfo.MIC); + } else { + if (this.options.videoEnable) { + videoConstraints = new VideoTrackConstraints(VideoSourceInfo.SCREENCAST); + if (this.options.audioEnable) audioConstraints = new AudioTrackConstraints(AudioSourceInfo.SCREENCAST); + } else { + if (this.options.audioEnable) audioConstraints = new AudioTrackConstraints(AudioSourceInfo.MIC);else { + // error shared display media not only audio + error(this.TAG, 'error paramter'); + } + } + } + if (this.options.resolution.w != 0 && this.options.resolution.h != 0 && typeof videoConstraints == 'object') { + videoConstraints.resolution = new Resolution(this.options.resolution.w, this.options.resolution.h); + } + MediaStreamFactory.createMediaStream(new StreamConstraints(audioConstraints, videoConstraints)).then(stream => { + this._localStream = stream; + this.dispatch(Events$1.WEBRTC_ON_LOCAL_STREAM, stream); + const AudioTransceiverInit = { + direction: 'sendrecv', + sendEncodings: [] + }; + const VideoTransceiverInit = { + direction: 'sendrecv', + sendEncodings: [] + }; + if (this.options.simulcast && stream.getVideoTracks().length > 0) { + VideoTransceiverInit.sendEncodings = [{ + rid: 'h', + active: true, + maxBitrate: 1000000 + }, { + rid: 'm', + active: true, + maxBitrate: 500000, + scaleResolutionDownBy: 2 + }, { + rid: 'l', + active: true, + maxBitrate: 200000, + scaleResolutionDownBy: 4 + }]; + } + if (this.options.audioEnable) { + if (stream.getAudioTracks().length > 0) { + this.pc.addTransceiver(stream.getAudioTracks()[0], AudioTransceiverInit); + } else { + AudioTransceiverInit.direction = 'recvonly'; + this.pc.addTransceiver('audio', AudioTransceiverInit); + } + } + if (this.options.videoEnable) { + if (stream.getVideoTracks().length > 0) { + this.pc.addTransceiver(stream.getVideoTracks()[0], VideoTransceiverInit); + } else { + VideoTransceiverInit.direction = 'recvonly'; + this.pc.addTransceiver('video', VideoTransceiverInit); + } + } + + /* + stream.getTracks().forEach((track,idx)=>{ + debug.log(this.TAG,track); + this.pc.addTrack(track); + }); + */ + this.pc.createOffer().then(desc => { + log(this.TAG, 'offer:', desc.sdp); + this.pc.setLocalDescription(desc).then(() => { + axios({ + method: 'post', + url: this.options.zlmsdpUrl, + responseType: 'json', + data: desc.sdp, + headers: { + 'Content-Type': 'text/plain;charset=utf-8' + } + }).then(response => { + let ret = response.data; //JSON.parse(response.data); + if (ret.code != 0) { + // mean failed for offer/anwser exchange + this.dispatch(Events$1.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, ret); + return; + } + let anwser = {}; + anwser.sdp = ret.sdp; + anwser.type = 'answer'; + log(this.TAG, 'answer:', ret.sdp); + this.pc.setRemoteDescription(anwser).then(() => { + log(this.TAG, 'set remote sucess'); + }).catch(e => { + error(this.TAG, e); + }); + }); + }); + }).catch(e => { + error(this.TAG, e); + }); + }).catch(e => { + this.dispatch(Events$1.CAPTURE_STREAM_FAILED); + //debug.error(this.TAG,e); + }); + + //const offerOptions = {}; + /* + if (typeof this.pc.addTransceiver === 'function') { + // |direction| seems not working on Safari. + this.pc.addTransceiver('audio', { direction: 'recvonly' }); + this.pc.addTransceiver('video', { direction: 'recvonly' }); + } else { + offerOptions.offerToReceiveAudio = true; + offerOptions.offerToReceiveVideo = true; + } + */ + } + + _onIceCandidate(event) { + if (event.candidate) { + log(this.TAG, 'Remote ICE candidate: \n ' + event.candidate.candidate); + // Send the candidate to the remote peer + } + } + _onTrack(event) { + this._tracks.push(event.track); + if (this.options.element && event.streams && event.streams.length > 0) { + this.options.element.srcObject = event.streams[0]; + this._remoteStream = event.streams[0]; + this.dispatch(Events$1.WEBRTC_ON_REMOTE_STREAMS, event); + } else { + if (this.pc.getReceivers().length == this._tracks.length) { + log(this.TAG, 'play remote stream '); + this._remoteStream = new MediaStream(this._tracks); + this.options.element.srcObject = this._remoteStream; + } else { + error(this.TAG, 'wait stream track finish'); + } + } + } + _onIceCandidateError(event) { + this.dispatch(Events$1.WEBRTC_ICE_CANDIDATE_ERROR, event); + } + _onconnectionstatechange(event) { + this.dispatch(Events$1.WEBRTC_ON_CONNECTION_STATE_CHANGE, this.pc.connectionState); + } + _onDataChannelOpen(event) { + log(this.TAG, 'ondatachannel open:', event); + this.dispatch(Events$1.WEBRTC_ON_DATA_CHANNEL_OPEN, event); + } + _onDataChannelMsg(event) { + log(this.TAG, 'ondatachannel msg:', event); + this.dispatch(Events$1.WEBRTC_ON_DATA_CHANNEL_MSG, event); + } + _onDataChannelErr(event) { + log(this.TAG, 'ondatachannel err:', event); + this.dispatch(Events$1.WEBRTC_ON_DATA_CHANNEL_ERR, event); + } + _onDataChannelClose(event) { + log(this.TAG, 'ondatachannel close:', event); + this.dispatch(Events$1.WEBRTC_ON_DATA_CHANNEL_CLOSE, event); + } + sendMsg(data) { + if (this.datachannel != null) { + this.datachannel.send(data); + } else { + error(this.TAG, 'data channel is null'); + } + } + closeDataChannel() { + if (this.datachannel) { + this.datachannel.close(); + this.datachannel = null; + } + } + close() { + this.closeDataChannel(); + if (this.pc) { + this.pc.close(); + this.pc = null; + } + if (this.options) { + this.options = null; + } + if (this._localStream) { + this._localStream.getTracks().forEach((track, idx) => { + track.stop(); + }); + } + if (this._remoteStream) { + this._remoteStream.getTracks().forEach((track, idx) => { + track.stop(); + }); + } + this._tracks.forEach((track, idx) => { + track.stop(); + }); + this._tracks = []; + } + get remoteStream() { + return this._remoteStream; + } + get localStream() { + return this._localStream; + } + } + + const quickScan = [{ + 'label': '4K(UHD)', + 'width': 3840, + 'height': 2160 + }, { + 'label': '1080p(FHD)', + 'width': 1920, + 'height': 1080 + }, { + 'label': 'UXGA', + 'width': 1600, + 'height': 1200, + 'ratio': '4:3' + }, { + 'label': '720p(HD)', + 'width': 1280, + 'height': 720 + }, { + 'label': 'SVGA', + 'width': 800, + 'height': 600 + }, { + 'label': 'VGA', + 'width': 640, + 'height': 480 + }, { + 'label': '360p(nHD)', + 'width': 640, + 'height': 360 + }, { + 'label': 'CIF', + 'width': 352, + 'height': 288 + }, { + 'label': 'QVGA', + 'width': 320, + 'height': 240 + }, { + 'label': 'QCIF', + 'width': 176, + 'height': 144 + }, { + 'label': 'QQVGA', + 'width': 160, + 'height': 120 + }]; + function GetSupportCameraResolutions$1() { + return new Promise(function (resolve, reject) { + let resolutions = []; + let ok = 0; + let err = 0; + for (let i = 0; i < quickScan.length; ++i) { + let videoConstraints = new VideoTrackConstraints(VideoSourceInfo.CAMERA); + videoConstraints.resolution = new Resolution(quickScan[i].width, quickScan[i].height); + MediaStreamFactory.createMediaStream(new StreamConstraints(false, videoConstraints)).then(stream => { + resolutions.push(quickScan[i]); + ok++; + if (ok + err == quickScan.length) { + resolve(resolutions); + } + }).catch(e => { + err++; + if (ok + err == quickScan.length) { + resolve(resolutions); + } + }); + } + }); + } + function GetAllScanResolution$1() { + return quickScan; + } + function isSupportResolution$1(w, h) { + return new Promise(function (resolve, reject) { + let videoConstraints = new VideoTrackConstraints(VideoSourceInfo.CAMERA); + videoConstraints.resolution = new Resolution(w, h); + MediaStreamFactory.createMediaStream(new StreamConstraints(false, videoConstraints)).then(stream => { + resolve(); + }).catch(e => { + reject(e); + }); + }); + } + + console.log('build date:', BUILD_DATE); + console.log('version:', VERSION$1); + const Events = Events$1; + const Media = media; + const Endpoint = RTCEndpoint; + const GetSupportCameraResolutions = GetSupportCameraResolutions$1; + const GetAllScanResolution = GetAllScanResolution$1; + const isSupportResolution = isSupportResolution$1; + + exports.Endpoint = Endpoint; + exports.Events = Events; + exports.GetAllScanResolution = GetAllScanResolution; + exports.GetSupportCameraResolutions = GetSupportCameraResolutions; + exports.Media = Media; + exports.isSupportResolution = isSupportResolution; + + Object.defineProperty(exports, '__esModule', { value: true }); + + return exports; + +})({}); +//# sourceMappingURL=ZLMRTCClient.js.map diff --git a/web/public/static/js/config.js b/web/public/static/js/config.js new file mode 100644 index 0000000..43329f7 --- /dev/null +++ b/web/public/static/js/config.js @@ -0,0 +1,22 @@ + +window.baseUrl = "" + +// map组件全局参数, 注释此内容可以关闭地图功能 +window.mapParam = { + // 开启/关闭地图功能 + enable: true, + // 坐标系 GCJ02 WGS84, + coordinateSystem: "GCJ02", + // 地图瓦片地址 + tilesUrl: "http://webrd0{1-4}.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8", + // 瓦片大小 + tileSize: 256, + // 默认层级 + zoom:10, + // 默认地图中心点 + center:[116.41020, 39.915119], + // 地图最大层级 + maxZoom:18, + // 地图最小层级 + minZoom: 3 +} diff --git a/web/public/static/js/h265web/h265webjs-v20221106.js b/web/public/static/js/h265web/h265webjs-v20221106.js new file mode 100644 index 0000000..3dd4880 --- /dev/null +++ b/web/public/static/js/h265web/h265webjs-v20221106.js @@ -0,0 +1,428 @@ +!function e(t,i,n){function r(s,o){if(!i[s]){if(!t[s]){var u="function"==typeof require&&require;if(!o&&u)return u(s,!0);if(a)return a(s,!0);var l=new Error("Cannot find module '"+s+"'");throw l.code="MODULE_NOT_FOUND",l}var h=i[s]={exports:{}};t[s][0].call(h.exports,(function(e){return r(t[s][1][e]||e)}),h,h.exports,e,t,i,n)}return i[s].exports}for(var a="function"==typeof require&&require,s=0;sh&&(u-=h,u-=h,u-=c(2))}return Number(u)};i.numberToBytes=function(e,t){var i=(void 0===t?{}:t).le,n=void 0!==i&&i;("bigint"!=typeof e&&"number"!=typeof e||"number"==typeof e&&e!=e)&&(e=0),e=c(e);for(var r=s(e),a=new Uint8Array(new ArrayBuffer(r)),o=0;o=t.length&&u.call(t,(function(t,i){return t===(o[i]?o[i]&e[a+i]:e[a+i])}))};i.sliceBytes=function(e,t,i){return Uint8Array.prototype.slice?Uint8Array.prototype.slice.call(e,t,i):new Uint8Array(Array.prototype.slice.call(e,t,i))};i.reverseBytes=function(e){return e.reverse?e.reverse():Array.prototype.reverse.call(e)}},{"@babel/runtime/helpers/interopRequireDefault":6,"global/window":34}],10:[function(e,t,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0}),i.getHvcCodec=i.getAvcCodec=i.getAv1Codec=void 0;var n=e("./byte-helpers.js");i.getAv1Codec=function(e){var t,i="",r=e[1]>>>3,a=31&e[1],s=e[2]>>>7,o=(64&e[2])>>6,u=(32&e[2])>>5,l=(16&e[2])>>4,h=(8&e[2])>>3,d=(4&e[2])>>2,c=3&e[2];return i+=r+"."+(0,n.padStart)(a,2,"0"),0===s?i+="M":1===s&&(i+="H"),t=2===r&&o?u?12:10:o?10:8,i+="."+(0,n.padStart)(t,2,"0"),i+="."+l,i+="."+h+d+c};i.getAvcCodec=function(e){return""+(0,n.toHexString)(e[1])+(0,n.toHexString)(252&e[2])+(0,n.toHexString)(e[3])};i.getHvcCodec=function(e){var t="",i=e[1]>>6,r=31&e[1],a=(32&e[1])>>5,s=e.subarray(2,6),o=e.subarray(6,12),u=e[12];1===i?t+="A":2===i?t+="B":3===i&&(t+="C"),t+=r+".";var l=parseInt((0,n.toBinaryString)(s).split("").reverse().join(""),2);l>255&&(l=parseInt((0,n.toBinaryString)(s),2)),t+=l.toString(16)+".",t+=0===a?"L":"H",t+=u;for(var h="",d=0;d=1)return 71===e[0];for(var t=0;t+1880}},{"./byte-helpers.js":9,"./ebml-helpers.js":14,"./id3-helpers.js":15,"./mp4-helpers.js":17,"./nal-helpers.js":18}],13:[function(e,t,i){(function(n){"use strict";var r=e("@babel/runtime/helpers/interopRequireDefault");Object.defineProperty(i,"__esModule",{value:!0}),i.default=function(e){for(var t=(s=e,a.default.atob?a.default.atob(s):n.from(s,"base64").toString("binary")),i=new Uint8Array(t.length),r=0;r=i.length)return i.length;var a=o(i,r,!1);if((0,n.bytesMatch)(t.bytes,a.bytes))return r;var s=o(i,r+a.length);return e(t,i,r+s.length+s.value+a.length)},h=function e(t,i){i=function(e){return Array.isArray(e)?e.map((function(e){return u(e)})):[u(e)]}(i),t=(0,n.toUint8)(t);var r=[];if(!i.length)return r;for(var a=0;at.length?t.length:d+h.value,f=t.subarray(d,c);(0,n.bytesMatch)(i[0],s.bytes)&&(1===i.length?r.push(f):r=r.concat(e(f,i.slice(1)))),a+=s.length+h.length+f.length}return r};i.findEbml=h;var d=function(e,t,i,r){var s;"group"===t&&((s=h(e,[a.BlockDuration])[0])&&(s=1/i*(s=(0,n.bytesToNumber)(s))*i/1e3),e=h(e,[a.Block])[0],t="block");var u=new DataView(e.buffer,e.byteOffset,e.byteLength),l=o(e,0),d=u.getInt16(l.length,!1),c=e[l.length+2],f=e.subarray(l.length+3),p=1/i*(r+d)*i/1e3,m={duration:s,trackNumber:l.value,keyframe:"simple"===t&&c>>7==1,invisible:(8&c)>>3==1,lacing:(6&c)>>1,discardable:"simple"===t&&1==(1&c),frames:[],pts:p,dts:p,timestamp:d};if(!m.lacing)return m.frames.push(f),m;var _=f[0]+1,g=[],v=1;if(2===m.lacing)for(var y=(f.length-v)/_,b=0;b<_;b++)g.push(y);if(1===m.lacing)for(var S=0;S<_-1;S++){var T=0;do{T+=f[v],v++}while(255===f[v-1]);g.push(T)}if(3===m.lacing)for(var E=0,w=0;w<_-1;w++){var A=0===w?o(f,v):o(f,v,!0,!0);E+=A.value,g.push(E),v+=A.length}return g.forEach((function(e){m.frames.push(f.subarray(v,v+e)),v+=e})),m};i.decodeBlock=d;var c=function(e){e=(0,n.toUint8)(e);var t=[],i=h(e,[a.Segment,a.Tracks,a.Track]);return i.length||(i=h(e,[a.Tracks,a.Track])),i.length||(i=h(e,[a.Track])),i.length?(i.forEach((function(e){var i=h(e,a.TrackType)[0];if(i&&i.length){if(1===i[0])i="video";else if(2===i[0])i="audio";else{if(17!==i[0])return;i="subtitle"}var s={rawCodec:(0,n.bytesToString)(h(e,[a.CodecID])[0]),type:i,codecPrivate:h(e,[a.CodecPrivate])[0],number:(0,n.bytesToNumber)(h(e,[a.TrackNumber])[0]),defaultDuration:(0,n.bytesToNumber)(h(e,[a.DefaultDuration])[0]),default:h(e,[a.FlagDefault])[0],rawData:e},o="";if(/V_MPEG4\/ISO\/AVC/.test(s.rawCodec))o="avc1."+(0,r.getAvcCodec)(s.codecPrivate);else if(/V_MPEGH\/ISO\/HEVC/.test(s.rawCodec))o="hev1."+(0,r.getHvcCodec)(s.codecPrivate);else if(/V_MPEG4\/ISO\/ASP/.test(s.rawCodec))o=s.codecPrivate?"mp4v.20."+s.codecPrivate[4].toString():"mp4v.20.9";else if(/^V_THEORA/.test(s.rawCodec))o="theora";else if(/^V_VP8/.test(s.rawCodec))o="vp8";else if(/^V_VP9/.test(s.rawCodec))if(s.codecPrivate){var u=function(e){for(var t=0,i={};t>>3).toString():"mp4a.40.2":/^A_AC3/.test(s.rawCodec)?o="ac-3":/^A_PCM/.test(s.rawCodec)?o="pcm":/^A_MS\/ACM/.test(s.rawCodec)?o="speex":/^A_EAC3/.test(s.rawCodec)?o="ec-3":/^A_VORBIS/.test(s.rawCodec)?o="vorbis":/^A_FLAC/.test(s.rawCodec)?o="flac":/^A_OPUS/.test(s.rawCodec)&&(o="opus");s.codec=o,t.push(s)}})),t.sort((function(e,t){return e.number-t.number}))):t};i.parseTracks=c;i.parseData=function(e,t){var i=[],r=h(e,[a.Segment])[0],s=h(r,[a.SegmentInfo,a.TimestampScale])[0];s=s&&s.length?(0,n.bytesToNumber)(s):1e6;var o=h(r,[a.Cluster]);return t||(t=c(r)),o.forEach((function(e,t){var r=h(e,[a.SimpleBlock]).map((function(e){return{type:"simple",data:e}})),o=h(e,[a.BlockGroup]).map((function(e){return{type:"group",data:e}})),u=h(e,[a.Timestamp])[0]||0;u&&u.length&&(u=(0,n.bytesToNumber)(u)),r.concat(o).sort((function(e,t){return e.data.byteOffset-t.data.byteOffset})).forEach((function(e,t){var n=d(e.data,e.type,s,u);i.push(n)}))})),{tracks:t,blocks:i}}},{"./byte-helpers":9,"./codec-helpers.js":10}],15:[function(e,t,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0}),i.getId3Offset=i.getId3Size=void 0;var n=e("./byte-helpers.js"),r=(0,n.toUint8)([73,68,51]),a=function(e,t){void 0===t&&(t=0);var i=(e=(0,n.toUint8)(e))[t+5],r=e[t+6]<<21|e[t+7]<<14|e[t+8]<<7|e[t+9];return(16&i)>>4?r+20:r+10};i.getId3Size=a;i.getId3Offset=function e(t,i){return void 0===i&&(i=0),(t=(0,n.toUint8)(t)).length-i<10||!(0,n.bytesMatch)(t,r,{offset:i})?i:e(t,i+=a(t,i))}},{"./byte-helpers.js":9}],16:[function(e,t,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0}),i.simpleTypeFromSourceType=void 0;var n=/^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i,r=/^application\/dash\+xml/i;i.simpleTypeFromSourceType=function(e){return n.test(e)?"hls":r.test(e)?"dash":"application/vnd.videojs.vhs+json"===e?"vhs-json":null}},{}],17:[function(e,t,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0}),i.parseMediaInfo=i.parseTracks=i.addSampleDescription=i.buildFrameTable=i.findNamedBox=i.findBox=i.parseDescriptors=void 0;var n,r=e("./byte-helpers.js"),a=e("./codec-helpers.js"),s=e("./opus-helpers.js"),o=function(e){return"string"==typeof e?(0,r.stringToBytes)(e):e},u=function(e){e=(0,r.toUint8)(e);for(var t=[],i=0;e.length>i;){var a=e[i],s=0,o=0,u=e[++o];for(o++;128&u;)s=(127&u)<<7,u=e[o],o++;s+=127&u;for(var l=0;l>>0,l=t.subarray(s+4,s+8);if(0===u)break;var h=s+u;if(h>t.length){if(n)break;h=t.length}var d=t.subarray(s+8,h);(0,r.bytesMatch)(l,i[0])&&(1===i.length?a.push(d):a.push.apply(a,e(d,i.slice(1),n))),s=h}return a};i.findBox=l;var h=function(e,t){if(!(t=o(t)).length)return e.subarray(e.length);for(var i=0;i>>0,a=n>1?i+n:e.byteLength;return e.subarray(i+4,a)}i++}return e.subarray(e.length)};i.findNamedBox=h;var d=function(e,t,i){void 0===t&&(t=4),void 0===i&&(i=function(e){return(0,r.bytesToNumber)(e)});var n=[];if(!e||!e.length)return n;for(var a=(0,r.bytesToNumber)(e.subarray(4,8)),s=8;a;s+=t,a--)n.push(i(e.subarray(s,s+t)));return n},c=function(e,t){for(var i=d(l(e,["stss"])[0]),n=d(l(e,["stco"])[0]),a=d(l(e,["stts"])[0],8,(function(e){return{sampleCount:(0,r.bytesToNumber)(e.subarray(0,4)),sampleDelta:(0,r.bytesToNumber)(e.subarray(4,8))}})),s=d(l(e,["stsc"])[0],12,(function(e){return{firstChunk:(0,r.bytesToNumber)(e.subarray(0,4)),samplesPerChunk:(0,r.bytesToNumber)(e.subarray(4,8)),sampleDescriptionIndex:(0,r.bytesToNumber)(e.subarray(8,12))}})),o=l(e,["stsz"])[0],u=d(o&&o.length&&o.subarray(4)||null),h=[],c=0;c=m.firstChunk&&(p+1>=s.length||c+1>3).toString():32===d.oti?i+="."+d.descriptors[0].bytes[4].toString():221===d.oti&&(i="vorbis")):"audio"===e.type?i+=".40.2":i+=".20.9"}else if("av01"===i)i+="."+(0,a.getAv1Codec)(h(t,"av1C"));else if("vp09"===i){var c=h(t,"vpcC"),f=c[0],p=c[1],m=c[2]>>4,_=(15&c[2])>>1,g=(15&c[2])>>3,v=c[3],y=c[4],b=c[5];i+="."+(0,r.padStart)(f,2,"0"),i+="."+(0,r.padStart)(p,2,"0"),i+="."+(0,r.padStart)(m,2,"0"),i+="."+(0,r.padStart)(_,2,"0"),i+="."+(0,r.padStart)(v,2,"0"),i+="."+(0,r.padStart)(y,2,"0"),i+="."+(0,r.padStart)(b,2,"0"),i+="."+(0,r.padStart)(g,2,"0")}else if("theo"===i)i="theora";else if("spex"===i)i="speex";else if(".mp3"===i)i="mp4a.40.34";else if("msVo"===i)i="vorbis";else if("Opus"===i){i="opus";var S=h(t,"dOps");e.info.opus=(0,s.parseOpusHead)(S),e.info.codecDelay=65e5}else i=i.toLowerCase();e.codec=i};i.addSampleDescription=f;i.parseTracks=function(e,t){void 0===t&&(t=!0),e=(0,r.toUint8)(e);var i=l(e,["moov","trak"],!0),n=[];return i.forEach((function(e){var i={bytes:e},a=l(e,["mdia"])[0],s=l(a,["hdlr"])[0],o=(0,r.bytesToString)(s.subarray(8,12));i.type="soun"===o?"audio":"vide"===o?"video":o;var u=l(e,["tkhd"])[0];if(u){var h=new DataView(u.buffer,u.byteOffset,u.byteLength),d=h.getUint8(0);i.number=0===d?h.getUint32(12):h.getUint32(20)}var p=l(a,["mdhd"])[0];if(p){var m=0===p[0]?12:20;i.timescale=(p[m]<<24|p[m+1]<<16|p[m+2]<<8|p[m+3])>>>0}for(var _=l(a,["minf","stbl"])[0],g=l(_,["stsd"])[0],v=(0,r.bytesToNumber)(g.subarray(4,8)),y=8;v--;){var b=(0,r.bytesToNumber)(g.subarray(y,y+4)),S=g.subarray(y+4,y+4+b);f(i,S),y+=4+b}t&&(i.frameTable=c(_,i.timescale)),n.push(i)})),n};i.parseMediaInfo=function(e){var t=l(e,["moov","mvhd"],!0)[0];if(t&&t.length){var i={};return 1===t[0]?(i.timestampScale=(0,r.bytesToNumber)(t.subarray(20,24)),i.duration=(0,r.bytesToNumber)(t.subarray(24,32))):(i.timestampScale=(0,r.bytesToNumber)(t.subarray(12,16)),i.duration=(0,r.bytesToNumber)(t.subarray(16,20))),i.bytes=t,i}}},{"./byte-helpers.js":9,"./codec-helpers.js":10,"./opus-helpers.js":19}],18:[function(e,t,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0}),i.findH265Nal=i.findH264Nal=i.findNal=i.discardEmulationPreventionBytes=i.EMULATION_PREVENTION=i.NAL_TYPE_TWO=i.NAL_TYPE_ONE=void 0;var n=e("./byte-helpers.js"),r=(0,n.toUint8)([0,0,0,1]);i.NAL_TYPE_ONE=r;var a=(0,n.toUint8)([0,0,1]);i.NAL_TYPE_TWO=a;var s=(0,n.toUint8)([0,0,3]);i.EMULATION_PREVENTION=s;var o=function(e){for(var t=[],i=1;i>1&63),-1!==i.indexOf(c)&&(u=l+d),l+=d+("h264"===t?1:2)}else l++}return e.subarray(0,0)};i.findNal=u;i.findH264Nal=function(e,t,i){return u(e,"h264",t,i)};i.findH265Nal=function(e,t,i){return u(e,"h265",t,i)}},{"./byte-helpers.js":9}],19:[function(e,t,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0}),i.setOpusHead=i.parseOpusHead=i.OPUS_HEAD=void 0;var n=new Uint8Array([79,112,117,115,72,101,97,100]);i.OPUS_HEAD=n;i.parseOpusHead=function(e){var t=new DataView(e.buffer,e.byteOffset,e.byteLength),i=t.getUint8(0),n=0!==i,r={version:i,channels:t.getUint8(1),preSkip:t.getUint16(2,n),sampleRate:t.getUint32(4,n),outputGain:t.getUint16(8,n),channelMappingFamily:t.getUint8(10)};if(r.channelMappingFamily>0&&e.length>10){r.streamCount=t.getUint8(11),r.twoChannelStreamCount=t.getUint8(12),r.channelMapping=[];for(var a=0;a0&&(i.setUint8(11,e.streamCount),e.channelMapping.foreach((function(e,t){i.setUint8(12+t,e)}))),new Uint8Array(i.buffer)}},{}],20:[function(e,t,i){"use strict";var n=e("@babel/runtime/helpers/interopRequireDefault");Object.defineProperty(i,"__esModule",{value:!0}),i.default=void 0;var r=n(e("url-toolkit")),a=n(e("global/window")),s=function(e,t){if(/^[a-z]+:/i.test(t))return t;/^data:/.test(e)&&(e=a.default.location&&a.default.location.href||"");var i="function"==typeof a.default.URL,n=/^\/\//.test(e),s=!a.default.location&&!/\/\//i.test(e);if(i?e=new a.default.URL(e,a.default.location||"http://example.com"):/\/\//i.test(e)||(e=r.default.buildAbsoluteURL(a.default.location&&a.default.location.href||"",e)),i){var o=new URL(t,e);return s?o.href.slice("http://example.com".length):n?o.href.slice(o.protocol.length):o.href}return r.default.buildAbsoluteURL(e,t)};i.default=s,t.exports=i.default},{"@babel/runtime/helpers/interopRequireDefault":6,"global/window":34,"url-toolkit":46}],21:[function(e,t,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0}),i.default=void 0;var n=function(){function e(){this.listeners={}}var t=e.prototype;return t.on=function(e,t){this.listeners[e]||(this.listeners[e]=[]),this.listeners[e].push(t)},t.off=function(e,t){if(!this.listeners[e])return!1;var i=this.listeners[e].indexOf(t);return this.listeners[e]=this.listeners[e].slice(0),this.listeners[e].splice(i,1),i>-1},t.trigger=function(e){var t=this.listeners[e];if(t)if(2===arguments.length)for(var i=t.length,n=0;n=400&&r.statusCode<=599){var s=a;if(t)if(n.TextDecoder){var o=function(e){void 0===e&&(e="");return e.toLowerCase().split(";").reduce((function(e,t){var i=t.split("="),n=i[0],r=i[1];return"charset"===n.trim()?r.trim():e}),"utf-8")}(r.headers&&r.headers["content-type"]);try{s=new TextDecoder(o).decode(a)}catch(e){}}else s=String.fromCharCode.apply(null,new Uint8Array(a));e({cause:s})}else e(null,a)}}},{"global/window":34}],23:[function(e,t,i){"use strict";var n=e("global/window"),r=e("@babel/runtime/helpers/extends"),a=e("is-function");o.httpHandler=e("./http-handler.js");function s(e,t,i){var n=e;return a(t)?(i=t,"string"==typeof e&&(n={uri:e})):n=r({},t,{uri:e}),n.callback=i,n}function o(e,t,i){return u(t=s(e,t,i))}function u(e){if(void 0===e.callback)throw new Error("callback argument missing");var t=!1,i=function(i,n,r){t||(t=!0,e.callback(i,n,r))};function n(){var e=void 0;if(e=l.response?l.response:l.responseText||function(e){try{if("document"===e.responseType)return e.responseXML;var t=e.responseXML&&"parsererror"===e.responseXML.documentElement.nodeName;if(""===e.responseType&&!t)return e.responseXML}catch(e){}return null}(l),_)try{e=JSON.parse(e)}catch(e){}return e}function r(e){return clearTimeout(h),e instanceof Error||(e=new Error(""+(e||"Unknown XMLHttpRequest Error"))),e.statusCode=0,i(e,g)}function a(){if(!u){var t;clearTimeout(h),t=e.useXDR&&void 0===l.status?200:1223===l.status?204:l.status;var r=g,a=null;return 0!==t?(r={body:n(),statusCode:t,method:c,headers:{},url:d,rawRequest:l},l.getAllResponseHeaders&&(r.headers=function(e){var t={};return e?(e.trim().split("\n").forEach((function(e){var i=e.indexOf(":"),n=e.slice(0,i).trim().toLowerCase(),r=e.slice(i+1).trim();void 0===t[n]?t[n]=r:Array.isArray(t[n])?t[n].push(r):t[n]=[t[n],r]})),t):t}(l.getAllResponseHeaders()))):a=new Error("Internal XMLHttpRequest Error"),i(a,r,r.body)}}var s,u,l=e.xhr||null;l||(l=e.cors||e.useXDR?new o.XDomainRequest:new o.XMLHttpRequest);var h,d=l.url=e.uri||e.url,c=l.method=e.method||"GET",f=e.body||e.data,p=l.headers=e.headers||{},m=!!e.sync,_=!1,g={body:void 0,headers:{},statusCode:0,method:c,url:d,rawRequest:l};if("json"in e&&!1!==e.json&&(_=!0,p.accept||p.Accept||(p.Accept="application/json"),"GET"!==c&&"HEAD"!==c&&(p["content-type"]||p["Content-Type"]||(p["Content-Type"]="application/json"),f=JSON.stringify(!0===e.json?f:e.json))),l.onreadystatechange=function(){4===l.readyState&&setTimeout(a,0)},l.onload=a,l.onerror=r,l.onprogress=function(){},l.onabort=function(){u=!0},l.ontimeout=r,l.open(c,d,!m,e.username,e.password),m||(l.withCredentials=!!e.withCredentials),!m&&e.timeout>0&&(h=setTimeout((function(){if(!u){u=!0,l.abort("timeout");var e=new Error("XMLHttpRequest timeout");e.code="ETIMEDOUT",r(e)}}),e.timeout)),l.setRequestHeader)for(s in p)p.hasOwnProperty(s)&&l.setRequestHeader(s,p[s]);else if(e.headers&&!function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0}(e.headers))throw new Error("Headers cannot be set on an XDomainRequest object");return"responseType"in e&&(l.responseType=e.responseType),"beforeSend"in e&&"function"==typeof e.beforeSend&&e.beforeSend(l),l.send(f||null),l}t.exports=o,t.exports.default=o,o.XMLHttpRequest=n.XMLHttpRequest||function(){},o.XDomainRequest="withCredentials"in new o.XMLHttpRequest?o.XMLHttpRequest:n.XDomainRequest,function(e,t){for(var i=0;i=t+i||t?new java.lang.String(e,t,i)+"":e}function _(e,t){e.currentElement?e.currentElement.appendChild(t):e.doc.appendChild(t)}d.prototype.parseFromString=function(e,t){var i=this.options,n=new h,r=i.domBuilder||new c,s=i.errorHandler,o=i.locator,l=i.xmlns||{},d=/\/x?html?$/.test(t),f=d?a.HTML_ENTITIES:a.XML_ENTITIES;return o&&r.setDocumentLocator(o),n.errorHandler=function(e,t,i){if(!e){if(t instanceof c)return t;e=t}var n={},r=e instanceof Function;function a(t){var a=e[t];!a&&r&&(a=2==e.length?function(i){e(t,i)}:e),n[t]=a&&function(e){a("[xmldom "+t+"]\t"+e+p(i))}||function(){}}return i=i||{},a("warning"),a("error"),a("fatalError"),n}(s,r,o),n.domBuilder=i.domBuilder||r,d&&(l[""]=u.HTML),l.xml=l.xml||u.XML,e&&"string"==typeof e?n.parse(e,l,f):n.errorHandler.error("invalid doc source"),r.doc},c.prototype={startDocument:function(){this.doc=(new o).createDocument(null,null,null),this.locator&&(this.doc.documentURI=this.locator.systemId)},startElement:function(e,t,i,n){var r=this.doc,a=r.createElementNS(e,i||t),s=n.length;_(this,a),this.currentElement=a,this.locator&&f(this.locator,a);for(var o=0;o=0))throw k(A,new Error(e.tagName+"@"+i));for(var r=t.length-1;n"==e&&">")||"&"==e&&"&"||'"'==e&&"""||"&#"+e.charCodeAt()+";"}function B(e,t){if(t(e))return!0;if(e=e.firstChild)do{if(B(e,t))return!0}while(e=e.nextSibling)}function N(){}function j(e,t,i,r){e&&e._inc++,i.namespaceURI===n.XMLNS&&delete t._nsMap[i.prefix?i.localName:""]}function V(e,t,i){if(e&&e._inc){e._inc++;var n=t.childNodes;if(i)n[n.length++]=i;else{for(var r=t.firstChild,a=0;r;)n[a++]=r,r=r.nextSibling;n.length=a}}}function H(e,t){var i=t.previousSibling,n=t.nextSibling;return i?i.nextSibling=n:e.firstChild=n,n?n.previousSibling=i:e.lastChild=i,V(e.ownerDocument,e),t}function z(e,t,i){var n=t.parentNode;if(n&&n.removeChild(t),t.nodeType===b){var r=t.firstChild;if(null==r)return t;var a=t.lastChild}else r=a=t;var s=i?i.previousSibling:e.lastChild;r.previousSibling=s,a.nextSibling=i,s?s.nextSibling=r:e.firstChild=r,null==i?e.lastChild=a:i.previousSibling=a;do{r.parentNode=e}while(r!==a&&(r=r.nextSibling));return V(e.ownerDocument||e,e),t.nodeType==b&&(t.firstChild=t.lastChild=null),t}function G(){this._nsMap={}}function W(){}function Y(){}function q(){}function K(){}function X(){}function Q(){}function $(){}function J(){}function Z(){}function ee(){}function te(){}function ie(){}function ne(e,t){var i=[],n=9==this.nodeType&&this.documentElement||this,r=n.prefix,a=n.namespaceURI;if(a&&null==r&&null==(r=n.lookupPrefix(a)))var s=[{namespace:a,prefix:null}];return se(this,i,e,t,s),i.join("")}function re(e,t,i){var r=e.prefix||"",a=e.namespaceURI;if(!a)return!1;if("xml"===r&&a===n.XML||a===n.XMLNS)return!1;for(var s=i.length;s--;){var o=i[s];if(o.prefix===r)return o.namespace!==a}return!0}function ae(e,t,i){e.push(" ",t,'="',i.replace(/[<&"]/g,F),'"')}function se(e,t,i,r,a){if(a||(a=[]),r){if(!(e=r(e)))return;if("string"==typeof e)return void t.push(e)}switch(e.nodeType){case h:var s=e.attributes,o=s.length,u=e.firstChild,l=e.tagName,m=l;if(!(i=n.isHTML(e.namespaceURI)||i)&&!e.prefix&&e.namespaceURI){for(var S,T=0;T=0;E--){if(""===(w=a[E]).prefix&&w.namespace===e.namespaceURI){S=w.namespace;break}}if(S!==e.namespaceURI)for(E=a.length-1;E>=0;E--){var w;if((w=a[E]).namespace===e.namespaceURI){w.prefix&&(m=w.prefix+":"+l);break}}}t.push("<",m);for(var A=0;A"),i&&/^script$/i.test(l))for(;u;)u.data?t.push(u.data):se(u,t,i,r,a.slice()),u=u.nextSibling;else for(;u;)se(u,t,i,r,a.slice()),u=u.nextSibling;t.push("")}else t.push("/>");return;case v:case b:for(u=e.firstChild;u;)se(u,t,i,r,a.slice()),u=u.nextSibling;return;case d:return ae(t,e.name,e.value);case c:return t.push(e.data.replace(/[<&]/g,F).replace(/]]>/g,"]]>"));case f:return t.push("");case g:return t.push("\x3c!--",e.data,"--\x3e");case y:var I=e.publicId,L=e.systemId;if(t.push("");else if(L&&"."!=L)t.push(" SYSTEM ",L,">");else{var x=e.internalSubset;x&&t.push(" [",x,"]"),t.push(">")}return;case _:return t.push("");case p:return t.push("&",e.nodeName,";");default:t.push("??",e.nodeName)}}function oe(e,t,i){e[t]=i}k.prototype=Error.prototype,o(T,k),P.prototype={length:0,item:function(e){return this[e]||null},toString:function(e,t){for(var i=[],n=0;n0},lookupPrefix:function(e){for(var t=this;t;){var i=t._nsMap;if(i)for(var n in i)if(i[n]==e)return n;t=t.nodeType==d?t.ownerDocument:t.parentNode}return null},lookupNamespaceURI:function(e){for(var t=this;t;){var i=t._nsMap;if(i&&e in i)return i[e];t=t.nodeType==d?t.ownerDocument:t.parentNode}return null},isDefaultNamespace:function(e){return null==this.lookupPrefix(e)}},o(l,M),o(l,M.prototype),N.prototype={nodeName:"#document",nodeType:v,doctype:null,documentElement:null,_inc:1,insertBefore:function(e,t){if(e.nodeType==b){for(var i=e.firstChild;i;){var n=i.nextSibling;this.insertBefore(i,t),i=n}return e}return null==this.documentElement&&e.nodeType==h&&(this.documentElement=e),z(this,e,t),e.ownerDocument=this,e},removeChild:function(e){return this.documentElement==e&&(this.documentElement=null),H(this,e)},importNode:function(e,t){return function e(t,i,n){var r;switch(i.nodeType){case h:(r=i.cloneNode(!1)).ownerDocument=t;case b:break;case d:n=!0}r||(r=i.cloneNode(!1));if(r.ownerDocument=t,r.parentNode=null,n)for(var a=i.firstChild;a;)r.appendChild(e(t,a,n)),a=a.nextSibling;return r}(this,e,t)},getElementById:function(e){var t=null;return B(this.documentElement,(function(i){if(i.nodeType==h&&i.getAttribute("id")==e)return t=i,!0})),t},getElementsByClassName:function(e){var t=s(e);return new I(this,(function(i){var n=[];return t.length>0&&B(i.documentElement,(function(r){if(r!==i&&r.nodeType===h){var a=r.getAttribute("class");if(a){var o=e===a;if(!o){var u=s(a);o=t.every((l=u,function(e){return l&&-1!==l.indexOf(e)}))}o&&n.push(r)}}var l})),n}))},createElement:function(e){var t=new G;return t.ownerDocument=this,t.nodeName=e,t.tagName=e,t.localName=e,t.childNodes=new P,(t.attributes=new x)._ownerElement=t,t},createDocumentFragment:function(){var e=new ee;return e.ownerDocument=this,e.childNodes=new P,e},createTextNode:function(e){var t=new q;return t.ownerDocument=this,t.appendData(e),t},createComment:function(e){var t=new K;return t.ownerDocument=this,t.appendData(e),t},createCDATASection:function(e){var t=new X;return t.ownerDocument=this,t.appendData(e),t},createProcessingInstruction:function(e,t){var i=new te;return i.ownerDocument=this,i.tagName=i.target=e,i.nodeValue=i.data=t,i},createAttribute:function(e){var t=new W;return t.ownerDocument=this,t.name=e,t.nodeName=e,t.localName=e,t.specified=!0,t},createEntityReference:function(e){var t=new Z;return t.ownerDocument=this,t.nodeName=e,t},createElementNS:function(e,t){var i=new G,n=t.split(":"),r=i.attributes=new x;return i.childNodes=new P,i.ownerDocument=this,i.nodeName=t,i.tagName=t,i.namespaceURI=e,2==n.length?(i.prefix=n[0],i.localName=n[1]):i.localName=t,r._ownerElement=i,i},createAttributeNS:function(e,t){var i=new W,n=t.split(":");return i.ownerDocument=this,i.nodeName=t,i.name=t,i.namespaceURI=e,i.specified=!0,2==n.length?(i.prefix=n[0],i.localName=n[1]):i.localName=t,i}},u(N,M),G.prototype={nodeType:h,hasAttribute:function(e){return null!=this.getAttributeNode(e)},getAttribute:function(e){var t=this.getAttributeNode(e);return t&&t.value||""},getAttributeNode:function(e){return this.attributes.getNamedItem(e)},setAttribute:function(e,t){var i=this.ownerDocument.createAttribute(e);i.value=i.nodeValue=""+t,this.setAttributeNode(i)},removeAttribute:function(e){var t=this.getAttributeNode(e);t&&this.removeAttributeNode(t)},appendChild:function(e){return e.nodeType===b?this.insertBefore(e,null):function(e,t){var i=t.parentNode;if(i){var n=e.lastChild;i.removeChild(t);n=e.lastChild}return n=e.lastChild,t.parentNode=e,t.previousSibling=n,t.nextSibling=null,n?n.nextSibling=t:e.firstChild=t,e.lastChild=t,V(e.ownerDocument,e,t),t}(this,e)},setAttributeNode:function(e){return this.attributes.setNamedItem(e)},setAttributeNodeNS:function(e){return this.attributes.setNamedItemNS(e)},removeAttributeNode:function(e){return this.attributes.removeNamedItem(e.nodeName)},removeAttributeNS:function(e,t){var i=this.getAttributeNodeNS(e,t);i&&this.removeAttributeNode(i)},hasAttributeNS:function(e,t){return null!=this.getAttributeNodeNS(e,t)},getAttributeNS:function(e,t){var i=this.getAttributeNodeNS(e,t);return i&&i.value||""},setAttributeNS:function(e,t,i){var n=this.ownerDocument.createAttributeNS(e,t);n.value=n.nodeValue=""+i,this.setAttributeNode(n)},getAttributeNodeNS:function(e,t){return this.attributes.getNamedItemNS(e,t)},getElementsByTagName:function(e){return new I(this,(function(t){var i=[];return B(t,(function(n){n===t||n.nodeType!=h||"*"!==e&&n.tagName!=e||i.push(n)})),i}))},getElementsByTagNameNS:function(e,t){return new I(this,(function(i){var n=[];return B(i,(function(r){r===i||r.nodeType!==h||"*"!==e&&r.namespaceURI!==e||"*"!==t&&r.localName!=t||n.push(r)})),n}))}},N.prototype.getElementsByTagName=G.prototype.getElementsByTagName,N.prototype.getElementsByTagNameNS=G.prototype.getElementsByTagNameNS,u(G,M),W.prototype.nodeType=d,u(W,M),Y.prototype={data:"",substringData:function(e,t){return this.data.substring(e,e+t)},appendData:function(e){e=this.data+e,this.nodeValue=this.data=e,this.length=e.length},insertData:function(e,t){this.replaceData(e,0,t)},appendChild:function(e){throw new Error(E[w])},deleteData:function(e,t){this.replaceData(e,t,"")},replaceData:function(e,t,i){i=this.data.substring(0,e)+i+this.data.substring(e+t),this.nodeValue=this.data=i,this.length=i.length}},u(Y,M),q.prototype={nodeName:"#text",nodeType:c,splitText:function(e){var t=this.data,i=t.substring(e);t=t.substring(0,e),this.data=this.nodeValue=t,this.length=t.length;var n=this.ownerDocument.createTextNode(i);return this.parentNode&&this.parentNode.insertBefore(n,this.nextSibling),n}},u(q,Y),K.prototype={nodeName:"#comment",nodeType:g},u(K,Y),X.prototype={nodeName:"#cdata-section",nodeType:f},u(X,Y),Q.prototype.nodeType=y,u(Q,M),$.prototype.nodeType=S,u($,M),J.prototype.nodeType=m,u(J,M),Z.prototype.nodeType=p,u(Z,M),ee.prototype.nodeName="#document-fragment",ee.prototype.nodeType=b,u(ee,M),te.prototype.nodeType=_,u(te,M),ie.prototype.serializeToString=function(e,t,i){return ne.call(e,t,i)},M.prototype.toString=ne;try{if(Object.defineProperty){Object.defineProperty(I.prototype,"length",{get:function(){return L(this),this.$$length}}),Object.defineProperty(M.prototype,"textContent",{get:function(){return function e(t){switch(t.nodeType){case h:case b:var i=[];for(t=t.firstChild;t;)7!==t.nodeType&&8!==t.nodeType&&i.push(e(t)),t=t.nextSibling;return i.join("");default:return t.nodeValue}}(this)},set:function(e){switch(this.nodeType){case h:case b:for(;this.firstChild;)this.removeChild(this.firstChild);(e||String(e))&&this.appendChild(this.ownerDocument.createTextNode(e));break;default:this.data=e,this.value=e,this.nodeValue=e}}}),oe=function(e,t,i){e["$$"+t]=i}}}catch(e){}i.DocumentType=Q,i.DOMException=k,i.DOMImplementation=U,i.Element=G,i.Node=M,i.NodeList=P,i.XMLSerializer=ie},{"./conventions":24}],27:[function(e,t,i){var n=e("./conventions").freeze;i.XML_ENTITIES=n({amp:"&",apos:"'",gt:">",lt:"<",quot:'"'}),i.HTML_ENTITIES=n({lt:"<",gt:">",amp:"&",quot:'"',apos:"'",Agrave:"À",Aacute:"Á",Acirc:"Â",Atilde:"Ã",Auml:"Ä",Aring:"Å",AElig:"Æ",Ccedil:"Ç",Egrave:"È",Eacute:"É",Ecirc:"Ê",Euml:"Ë",Igrave:"Ì",Iacute:"Í",Icirc:"Î",Iuml:"Ï",ETH:"Ð",Ntilde:"Ñ",Ograve:"Ò",Oacute:"Ó",Ocirc:"Ô",Otilde:"Õ",Ouml:"Ö",Oslash:"Ø",Ugrave:"Ù",Uacute:"Ú",Ucirc:"Û",Uuml:"Ü",Yacute:"Ý",THORN:"Þ",szlig:"ß",agrave:"à",aacute:"á",acirc:"â",atilde:"ã",auml:"ä",aring:"å",aelig:"æ",ccedil:"ç",egrave:"è",eacute:"é",ecirc:"ê",euml:"ë",igrave:"ì",iacute:"í",icirc:"î",iuml:"ï",eth:"ð",ntilde:"ñ",ograve:"ò",oacute:"ó",ocirc:"ô",otilde:"õ",ouml:"ö",oslash:"ø",ugrave:"ù",uacute:"ú",ucirc:"û",uuml:"ü",yacute:"ý",thorn:"þ",yuml:"ÿ",nbsp:" ",iexcl:"¡",cent:"¢",pound:"£",curren:"¤",yen:"¥",brvbar:"¦",sect:"§",uml:"¨",copy:"©",ordf:"ª",laquo:"«",not:"¬",shy:"­­",reg:"®",macr:"¯",deg:"°",plusmn:"±",sup2:"²",sup3:"³",acute:"´",micro:"µ",para:"¶",middot:"·",cedil:"¸",sup1:"¹",ordm:"º",raquo:"»",frac14:"¼",frac12:"½",frac34:"¾",iquest:"¿",times:"×",divide:"÷",forall:"∀",part:"∂",exist:"∃",empty:"∅",nabla:"∇",isin:"∈",notin:"∉",ni:"∋",prod:"∏",sum:"∑",minus:"−",lowast:"∗",radic:"√",prop:"∝",infin:"∞",ang:"∠",and:"∧",or:"∨",cap:"∩",cup:"∪",int:"∫",there4:"∴",sim:"∼",cong:"≅",asymp:"≈",ne:"≠",equiv:"≡",le:"≤",ge:"≥",sub:"⊂",sup:"⊃",nsub:"⊄",sube:"⊆",supe:"⊇",oplus:"⊕",otimes:"⊗",perp:"⊥",sdot:"⋅",Alpha:"Α",Beta:"Β",Gamma:"Γ",Delta:"Δ",Epsilon:"Ε",Zeta:"Ζ",Eta:"Η",Theta:"Θ",Iota:"Ι",Kappa:"Κ",Lambda:"Λ",Mu:"Μ",Nu:"Ν",Xi:"Ξ",Omicron:"Ο",Pi:"Π",Rho:"Ρ",Sigma:"Σ",Tau:"Τ",Upsilon:"Υ",Phi:"Φ",Chi:"Χ",Psi:"Ψ",Omega:"Ω",alpha:"α",beta:"β",gamma:"γ",delta:"δ",epsilon:"ε",zeta:"ζ",eta:"η",theta:"θ",iota:"ι",kappa:"κ",lambda:"λ",mu:"μ",nu:"ν",xi:"ξ",omicron:"ο",pi:"π",rho:"ρ",sigmaf:"ς",sigma:"σ",tau:"τ",upsilon:"υ",phi:"φ",chi:"χ",psi:"ψ",omega:"ω",thetasym:"ϑ",upsih:"ϒ",piv:"ϖ",OElig:"Œ",oelig:"œ",Scaron:"Š",scaron:"š",Yuml:"Ÿ",fnof:"ƒ",circ:"ˆ",tilde:"˜",ensp:" ",emsp:" ",thinsp:" ",zwnj:"‌",zwj:"‍",lrm:"‎",rlm:"‏",ndash:"–",mdash:"—",lsquo:"‘",rsquo:"’",sbquo:"‚",ldquo:"“",rdquo:"”",bdquo:"„",dagger:"†",Dagger:"‡",bull:"•",hellip:"…",permil:"‰",prime:"′",Prime:"″",lsaquo:"‹",rsaquo:"›",oline:"‾",euro:"€",trade:"™",larr:"←",uarr:"↑",rarr:"→",darr:"↓",harr:"↔",crarr:"↵",lceil:"⌈",rceil:"⌉",lfloor:"⌊",rfloor:"⌋",loz:"◊",spades:"♠",clubs:"♣",hearts:"♥",diams:"♦"}),i.entityMap=i.HTML_ENTITIES},{"./conventions":24}],28:[function(e,t,i){var n=e("./dom");i.DOMImplementation=n.DOMImplementation,i.XMLSerializer=n.XMLSerializer,i.DOMParser=e("./dom-parser").DOMParser},{"./dom":26,"./dom-parser":25}],29:[function(e,t,i){var n=e("./conventions").NAMESPACE,r=/[A-Z_a-z\xC0-\xD6\xD8-\xF6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/,a=new RegExp("[\\-\\.0-9"+r.source.slice(1,-1)+"\\u00B7\\u0300-\\u036F\\u203F-\\u2040]"),s=new RegExp("^"+r.source+a.source+"*(?::"+r.source+a.source+"*)?$");function o(e,t){this.message=e,this.locator=t,Error.captureStackTrace&&Error.captureStackTrace(this,o)}function u(){}function l(e,t){return t.lineNumber=e.lineNumber,t.columnNumber=e.columnNumber,t}function h(e,t,i,r,a,s){function o(e,t,n){i.attributeNames.hasOwnProperty(e)&&s.fatalError("Attribute "+e+" redefined"),i.addValue(e,t,n)}for(var u,l=++t,h=0;;){var d=e.charAt(l);switch(d){case"=":if(1===h)u=e.slice(t,l),h=3;else{if(2!==h)throw new Error("attribute equal must after attrName");h=3}break;case"'":case'"':if(3===h||1===h){if(1===h&&(s.warning('attribute value must after "="'),u=e.slice(t,l)),t=l+1,!((l=e.indexOf(d,t))>0))throw new Error("attribute value no end '"+d+"' match");o(u,c=e.slice(t,l).replace(/&#?\w+;/g,a),t-1),h=5}else{if(4!=h)throw new Error('attribute value must after "="');o(u,c=e.slice(t,l).replace(/&#?\w+;/g,a),t),s.warning('attribute "'+u+'" missed start quot('+d+")!!"),t=l+1,h=5}break;case"/":switch(h){case 0:i.setTagName(e.slice(t,l));case 5:case 6:case 7:h=7,i.closed=!0;case 4:case 1:case 2:break;default:throw new Error("attribute invalid close char('/')")}break;case"":return s.error("unexpected end of input"),0==h&&i.setTagName(e.slice(t,l)),l;case">":switch(h){case 0:i.setTagName(e.slice(t,l));case 5:case 6:case 7:break;case 4:case 1:"/"===(c=e.slice(t,l)).slice(-1)&&(i.closed=!0,c=c.slice(0,-1));case 2:2===h&&(c=u),4==h?(s.warning('attribute "'+c+'" missed quot(")!'),o(u,c.replace(/&#?\w+;/g,a),t)):(n.isHTML(r[""])&&c.match(/^(?:disabled|checked|selected)$/i)||s.warning('attribute "'+c+'" missed value!! "'+c+'" instead!!'),o(c,c,t));break;case 3:throw new Error("attribute value missed!!")}return l;case"€":d=" ";default:if(d<=" ")switch(h){case 0:i.setTagName(e.slice(t,l)),h=6;break;case 1:u=e.slice(t,l),h=2;break;case 4:var c=e.slice(t,l).replace(/&#?\w+;/g,a);s.warning('attribute "'+c+'" missed quot(")!!'),o(u,c,t);case 5:h=6}else switch(h){case 2:i.tagName;n.isHTML(r[""])&&u.match(/^(?:disabled|checked|selected)$/i)||s.warning('attribute "'+u+'" missed value!! "'+u+'" instead2!!'),o(u,u,t),t=l,h=1;break;case 5:s.warning('attribute space is required"'+u+'"!!');case 6:h=1,t=l;break;case 3:h=4,t=l;break;case 7:throw new Error("elements closed character '/' and '>' must be connected to")}}l++}}function d(e,t,i){for(var r=e.tagName,a=null,s=e.length;s--;){var o=e[s],u=o.qName,l=o.value;if((f=u.indexOf(":"))>0)var h=o.prefix=u.slice(0,f),d=u.slice(f+1),c="xmlns"===h&&d;else d=u,h=null,c="xmlns"===u&&"";o.localName=d,!1!==c&&(null==a&&(a={},p(i,i={})),i[c]=a[c]=l,o.uri=n.XMLNS,t.startPrefixMapping(c,l))}for(s=e.length;s--;){(h=(o=e[s]).prefix)&&("xml"===h&&(o.uri=n.XML),"xmlns"!==h&&(o.uri=i[h||""]))}var f;(f=r.indexOf(":"))>0?(h=e.prefix=r.slice(0,f),d=e.localName=r.slice(f+1)):(h=null,d=e.localName=r);var m=e.uri=i[h||""];if(t.startElement(m,d,r,e),!e.closed)return e.currentNSMap=i,e.localNSMap=a,!0;if(t.endElement(m,d,r),a)for(h in a)t.endPrefixMapping(h)}function c(e,t,i,n,r){if(/^(?:script|textarea)$/i.test(i)){var a=e.indexOf("",t),s=e.substring(t+1,a);if(/[&<]/.test(s))return/^script$/i.test(i)?(r.characters(s,0,s.length),a):(s=s.replace(/&#?\w+;/g,n),r.characters(s,0,s.length),a)}return t+1}function f(e,t,i,n){var r=n[i];return null==r&&((r=e.lastIndexOf(""))t?(i.comment(e,t+4,r-t-4),r+3):(n.error("Unclosed comment"),-1):-1;default:if("CDATA["==e.substr(t+3,6)){var r=e.indexOf("]]>",t+9);return i.startCDATA(),i.characters(e,t+9,r-t-9),i.endCDATA(),r+3}var a=function(e,t){var i,n=[],r=/'[^']+'|"[^"]+"|[^\s<>\/=]+=?|(\/?\s*>|<)/g;r.lastIndex=t,r.exec(e);for(;i=r.exec(e);)if(n.push(i),i[1])return n}(e,t),s=a.length;if(s>1&&/!doctype/i.test(a[0][0])){var o=a[1][0],u=!1,l=!1;s>3&&(/^public$/i.test(a[2][0])?(u=a[3][0],l=s>4&&a[4][0]):/^system$/i.test(a[2][0])&&(l=a[3][0]));var h=a[s-1];return i.startDTD(o,u,l),i.endDTD(),h.index+h[0].length}}return-1}function _(e,t,i){var n=e.indexOf("?>",t);if(n){var r=e.substring(t,n).match(/^<\?(\S*)\s*([\s\S]*?)\s*$/);if(r){r[0].length;return i.processingInstruction(r[1],r[2]),n+2}return-1}return-1}function g(){this.attributeNames={}}o.prototype=new Error,o.prototype.name=o.name,u.prototype={parse:function(e,t,i){var r=this.domBuilder;r.startDocument(),p(t,t={}),function(e,t,i,r,a){function s(e){var t=e.slice(1,-1);return t in i?i[t]:"#"===t.charAt(0)?function(e){if(e>65535){var t=55296+((e-=65536)>>10),i=56320+(1023&e);return String.fromCharCode(t,i)}return String.fromCharCode(e)}(parseInt(t.substr(1).replace("x","0x"))):(a.error("entity not found:"+e),e)}function u(t){if(t>w){var i=e.substring(w,t).replace(/&#?\w+;/g,s);S&&p(w),r.characters(i,0,t-w),w=t}}function p(t,i){for(;t>=y&&(i=b.exec(e));)v=i.index,y=v+i[0].length,S.lineNumber++;S.columnNumber=t-v+1}var v=0,y=0,b=/.*(?:\r\n?|\n)|.*$/g,S=r.locator,T=[{currentNSMap:t}],E={},w=0;for(;;){try{var A=e.indexOf("<",w);if(A<0){if(!e.substr(w).match(/^\s*$/)){var C=r.doc,k=C.createTextNode(e.substr(w));C.appendChild(k),r.currentElement=k}return}switch(A>w&&u(A),e.charAt(A+1)){case"/":var P=e.indexOf(">",A+3),I=e.substring(A+2,P).replace(/[ \t\n\r]+$/g,""),L=T.pop();P<0?(I=e.substring(A+2).replace(/[\s<].*/,""),a.error("end tag name: "+I+" is not complete:"+L.tagName),P=A+1+I.length):I.match(/\sw?w=P:u(Math.max(A,w)+1)}}(e,t,i,r,this.errorHandler),r.endDocument()}},g.prototype={setTagName:function(e){if(!s.test(e))throw new Error("invalid tagName:"+e);this.tagName=e},addValue:function(e,t,i){if(!s.test(e))throw new Error("invalid attribute:"+e);this.attributeNames[e]=this.length,this[this.length++]={qName:e,value:t,offset:i}},length:0,getLocalName:function(e){return this[e].localName},getLocator:function(e){return this[e].locator},getQName:function(e){return this[e].qName},getURI:function(e){return this[e].uri},getValue:function(e){return this[e].value}},i.XMLReader=u,i.ParseError=o},{"./conventions":24}],30:[function(e,t,i){"use strict";i.byteLength=function(e){var t=l(e),i=t[0],n=t[1];return 3*(i+n)/4-n},i.toByteArray=function(e){var t,i,n=l(e),s=n[0],o=n[1],u=new a(function(e,t,i){return 3*(t+i)/4-i}(0,s,o)),h=0,d=o>0?s-4:s;for(i=0;i>16&255,u[h++]=t>>8&255,u[h++]=255&t;2===o&&(t=r[e.charCodeAt(i)]<<2|r[e.charCodeAt(i+1)]>>4,u[h++]=255&t);1===o&&(t=r[e.charCodeAt(i)]<<10|r[e.charCodeAt(i+1)]<<4|r[e.charCodeAt(i+2)]>>2,u[h++]=t>>8&255,u[h++]=255&t);return u},i.fromByteArray=function(e){for(var t,i=e.length,r=i%3,a=[],s=0,o=i-r;so?o:s+16383));1===r?(t=e[i-1],a.push(n[t>>2]+n[t<<4&63]+"==")):2===r&&(t=(e[i-2]<<8)+e[i-1],a.push(n[t>>10]+n[t>>4&63]+n[t<<2&63]+"="));return a.join("")};for(var n=[],r=[],a="undefined"!=typeof Uint8Array?Uint8Array:Array,s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",o=0,u=s.length;o0)throw new Error("Invalid string. Length must be a multiple of 4");var i=e.indexOf("=");return-1===i&&(i=t),[i,i===t?0:4-i%4]}function h(e,t,i){for(var r,a,s=[],o=t;o>18&63]+n[a>>12&63]+n[a>>6&63]+n[63&a]);return s.join("")}r["-".charCodeAt(0)]=62,r["_".charCodeAt(0)]=63},{}],31:[function(e,t,i){},{}],32:[function(e,t,i){(function(t){ +/*! + * The buffer module from node.js, for the browser. + * + * @author Feross Aboukhadijeh + * @license MIT + */ +"use strict";var n=e("base64-js"),r=e("ieee754");i.Buffer=t,i.SlowBuffer=function(e){+e!=e&&(e=0);return t.alloc(+e)},i.INSPECT_MAX_BYTES=50;function a(e){if(e>2147483647)throw new RangeError('The value "'+e+'" is invalid for option "size"');var i=new Uint8Array(e);return i.__proto__=t.prototype,i}function t(e,t,i){if("number"==typeof e){if("string"==typeof t)throw new TypeError('The "string" argument must be of type string. Received type number');return u(e)}return s(e,t,i)}function s(e,i,n){if("string"==typeof e)return function(e,i){"string"==typeof i&&""!==i||(i="utf8");if(!t.isEncoding(i))throw new TypeError("Unknown encoding: "+i);var n=0|d(e,i),r=a(n),s=r.write(e,i);s!==n&&(r=r.slice(0,s));return r}(e,i);if(ArrayBuffer.isView(e))return l(e);if(null==e)throw TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof e);if(B(e,ArrayBuffer)||e&&B(e.buffer,ArrayBuffer))return function(e,i,n){if(i<0||e.byteLength=2147483647)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+2147483647..toString(16)+" bytes");return 0|e}function d(e,i){if(t.isBuffer(e))return e.length;if(ArrayBuffer.isView(e)||B(e,ArrayBuffer))return e.byteLength;if("string"!=typeof e)throw new TypeError('The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof e);var n=e.length,r=arguments.length>2&&!0===arguments[2];if(!r&&0===n)return 0;for(var a=!1;;)switch(i){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":return U(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return M(e).length;default:if(a)return r?-1:U(e).length;i=(""+i).toLowerCase(),a=!0}}function c(e,t,i){var n=!1;if((void 0===t||t<0)&&(t=0),t>this.length)return"";if((void 0===i||i>this.length)&&(i=this.length),i<=0)return"";if((i>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return C(this,t,i);case"utf8":case"utf-8":return E(this,t,i);case"ascii":return w(this,t,i);case"latin1":case"binary":return A(this,t,i);case"base64":return T(this,t,i);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return k(this,t,i);default:if(n)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),n=!0}}function f(e,t,i){var n=e[t];e[t]=e[i],e[i]=n}function p(e,i,n,r,a){if(0===e.length)return-1;if("string"==typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),N(n=+n)&&(n=a?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(a)return-1;n=e.length-1}else if(n<0){if(!a)return-1;n=0}if("string"==typeof i&&(i=t.from(i,r)),t.isBuffer(i))return 0===i.length?-1:m(e,i,n,r,a);if("number"==typeof i)return i&=255,"function"==typeof Uint8Array.prototype.indexOf?a?Uint8Array.prototype.indexOf.call(e,i,n):Uint8Array.prototype.lastIndexOf.call(e,i,n):m(e,[i],n,r,a);throw new TypeError("val must be string, number or Buffer")}function m(e,t,i,n,r){var a,s=1,o=e.length,u=t.length;if(void 0!==n&&("ucs2"===(n=String(n).toLowerCase())||"ucs-2"===n||"utf16le"===n||"utf-16le"===n)){if(e.length<2||t.length<2)return-1;s=2,o/=2,u/=2,i/=2}function l(e,t){return 1===s?e[t]:e.readUInt16BE(t*s)}if(r){var h=-1;for(a=i;ao&&(i=o-u),a=i;a>=0;a--){for(var d=!0,c=0;cr&&(n=r):n=r;var a=t.length;n>a/2&&(n=a/2);for(var s=0;s>8,r=i%256,a.push(r),a.push(n);return a}(t,e.length-i),e,i,n)}function T(e,t,i){return 0===t&&i===e.length?n.fromByteArray(e):n.fromByteArray(e.slice(t,i))}function E(e,t,i){i=Math.min(e.length,i);for(var n=[],r=t;r239?4:l>223?3:l>191?2:1;if(r+d<=i)switch(d){case 1:l<128&&(h=l);break;case 2:128==(192&(a=e[r+1]))&&(u=(31&l)<<6|63&a)>127&&(h=u);break;case 3:a=e[r+1],s=e[r+2],128==(192&a)&&128==(192&s)&&(u=(15&l)<<12|(63&a)<<6|63&s)>2047&&(u<55296||u>57343)&&(h=u);break;case 4:a=e[r+1],s=e[r+2],o=e[r+3],128==(192&a)&&128==(192&s)&&128==(192&o)&&(u=(15&l)<<18|(63&a)<<12|(63&s)<<6|63&o)>65535&&u<1114112&&(h=u)}null===h?(h=65533,d=1):h>65535&&(h-=65536,n.push(h>>>10&1023|55296),h=56320|1023&h),n.push(h),r+=d}return function(e){var t=e.length;if(t<=4096)return String.fromCharCode.apply(String,e);var i="",n=0;for(;nt&&(e+=" ... "),""},t.prototype.compare=function(e,i,n,r,a){if(B(e,Uint8Array)&&(e=t.from(e,e.offset,e.byteLength)),!t.isBuffer(e))throw new TypeError('The "target" argument must be one of type Buffer or Uint8Array. Received type '+typeof e);if(void 0===i&&(i=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===a&&(a=this.length),i<0||n>e.length||r<0||a>this.length)throw new RangeError("out of range index");if(r>=a&&i>=n)return 0;if(r>=a)return-1;if(i>=n)return 1;if(this===e)return 0;for(var s=(a>>>=0)-(r>>>=0),o=(n>>>=0)-(i>>>=0),u=Math.min(s,o),l=this.slice(r,a),h=e.slice(i,n),d=0;d>>=0,isFinite(i)?(i>>>=0,void 0===n&&(n="utf8")):(n=i,i=void 0)}var r=this.length-t;if((void 0===i||i>r)&&(i=r),e.length>0&&(i<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");n||(n="utf8");for(var a=!1;;)switch(n){case"hex":return _(this,e,t,i);case"utf8":case"utf-8":return g(this,e,t,i);case"ascii":return v(this,e,t,i);case"latin1":case"binary":return y(this,e,t,i);case"base64":return b(this,e,t,i);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return S(this,e,t,i);default:if(a)throw new TypeError("Unknown encoding: "+n);n=(""+n).toLowerCase(),a=!0}},t.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};function w(e,t,i){var n="";i=Math.min(e.length,i);for(var r=t;rn)&&(i=n);for(var r="",a=t;ai)throw new RangeError("Trying to access beyond buffer length")}function I(e,i,n,r,a,s){if(!t.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(i>a||ie.length)throw new RangeError("Index out of range")}function L(e,t,i,n,r,a){if(i+n>e.length)throw new RangeError("Index out of range");if(i<0)throw new RangeError("Index out of range")}function x(e,t,i,n,a){return t=+t,i>>>=0,a||L(e,0,i,4),r.write(e,t,i,n,23,4),i+4}function R(e,t,i,n,a){return t=+t,i>>>=0,a||L(e,0,i,8),r.write(e,t,i,n,52,8),i+8}t.prototype.slice=function(e,i){var n=this.length;(e=~~e)<0?(e+=n)<0&&(e=0):e>n&&(e=n),(i=void 0===i?n:~~i)<0?(i+=n)<0&&(i=0):i>n&&(i=n),i>>=0,t>>>=0,i||P(e,t,this.length);for(var n=this[e],r=1,a=0;++a>>=0,t>>>=0,i||P(e,t,this.length);for(var n=this[e+--t],r=1;t>0&&(r*=256);)n+=this[e+--t]*r;return n},t.prototype.readUInt8=function(e,t){return e>>>=0,t||P(e,1,this.length),this[e]},t.prototype.readUInt16LE=function(e,t){return e>>>=0,t||P(e,2,this.length),this[e]|this[e+1]<<8},t.prototype.readUInt16BE=function(e,t){return e>>>=0,t||P(e,2,this.length),this[e]<<8|this[e+1]},t.prototype.readUInt32LE=function(e,t){return e>>>=0,t||P(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},t.prototype.readUInt32BE=function(e,t){return e>>>=0,t||P(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},t.prototype.readIntLE=function(e,t,i){e>>>=0,t>>>=0,i||P(e,t,this.length);for(var n=this[e],r=1,a=0;++a=(r*=128)&&(n-=Math.pow(2,8*t)),n},t.prototype.readIntBE=function(e,t,i){e>>>=0,t>>>=0,i||P(e,t,this.length);for(var n=t,r=1,a=this[e+--n];n>0&&(r*=256);)a+=this[e+--n]*r;return a>=(r*=128)&&(a-=Math.pow(2,8*t)),a},t.prototype.readInt8=function(e,t){return e>>>=0,t||P(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},t.prototype.readInt16LE=function(e,t){e>>>=0,t||P(e,2,this.length);var i=this[e]|this[e+1]<<8;return 32768&i?4294901760|i:i},t.prototype.readInt16BE=function(e,t){e>>>=0,t||P(e,2,this.length);var i=this[e+1]|this[e]<<8;return 32768&i?4294901760|i:i},t.prototype.readInt32LE=function(e,t){return e>>>=0,t||P(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},t.prototype.readInt32BE=function(e,t){return e>>>=0,t||P(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},t.prototype.readFloatLE=function(e,t){return e>>>=0,t||P(e,4,this.length),r.read(this,e,!0,23,4)},t.prototype.readFloatBE=function(e,t){return e>>>=0,t||P(e,4,this.length),r.read(this,e,!1,23,4)},t.prototype.readDoubleLE=function(e,t){return e>>>=0,t||P(e,8,this.length),r.read(this,e,!0,52,8)},t.prototype.readDoubleBE=function(e,t){return e>>>=0,t||P(e,8,this.length),r.read(this,e,!1,52,8)},t.prototype.writeUIntLE=function(e,t,i,n){(e=+e,t>>>=0,i>>>=0,n)||I(this,e,t,i,Math.pow(2,8*i)-1,0);var r=1,a=0;for(this[t]=255&e;++a>>=0,i>>>=0,n)||I(this,e,t,i,Math.pow(2,8*i)-1,0);var r=i-1,a=1;for(this[t+r]=255&e;--r>=0&&(a*=256);)this[t+r]=e/a&255;return t+i},t.prototype.writeUInt8=function(e,t,i){return e=+e,t>>>=0,i||I(this,e,t,1,255,0),this[t]=255&e,t+1},t.prototype.writeUInt16LE=function(e,t,i){return e=+e,t>>>=0,i||I(this,e,t,2,65535,0),this[t]=255&e,this[t+1]=e>>>8,t+2},t.prototype.writeUInt16BE=function(e,t,i){return e=+e,t>>>=0,i||I(this,e,t,2,65535,0),this[t]=e>>>8,this[t+1]=255&e,t+2},t.prototype.writeUInt32LE=function(e,t,i){return e=+e,t>>>=0,i||I(this,e,t,4,4294967295,0),this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e,t+4},t.prototype.writeUInt32BE=function(e,t,i){return e=+e,t>>>=0,i||I(this,e,t,4,4294967295,0),this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e,t+4},t.prototype.writeIntLE=function(e,t,i,n){if(e=+e,t>>>=0,!n){var r=Math.pow(2,8*i-1);I(this,e,t,i,r-1,-r)}var a=0,s=1,o=0;for(this[t]=255&e;++a>0)-o&255;return t+i},t.prototype.writeIntBE=function(e,t,i,n){if(e=+e,t>>>=0,!n){var r=Math.pow(2,8*i-1);I(this,e,t,i,r-1,-r)}var a=i-1,s=1,o=0;for(this[t+a]=255&e;--a>=0&&(s*=256);)e<0&&0===o&&0!==this[t+a+1]&&(o=1),this[t+a]=(e/s>>0)-o&255;return t+i},t.prototype.writeInt8=function(e,t,i){return e=+e,t>>>=0,i||I(this,e,t,1,127,-128),e<0&&(e=255+e+1),this[t]=255&e,t+1},t.prototype.writeInt16LE=function(e,t,i){return e=+e,t>>>=0,i||I(this,e,t,2,32767,-32768),this[t]=255&e,this[t+1]=e>>>8,t+2},t.prototype.writeInt16BE=function(e,t,i){return e=+e,t>>>=0,i||I(this,e,t,2,32767,-32768),this[t]=e>>>8,this[t+1]=255&e,t+2},t.prototype.writeInt32LE=function(e,t,i){return e=+e,t>>>=0,i||I(this,e,t,4,2147483647,-2147483648),this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24,t+4},t.prototype.writeInt32BE=function(e,t,i){return e=+e,t>>>=0,i||I(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e,t+4},t.prototype.writeFloatLE=function(e,t,i){return x(this,e,t,!0,i)},t.prototype.writeFloatBE=function(e,t,i){return x(this,e,t,!1,i)},t.prototype.writeDoubleLE=function(e,t,i){return R(this,e,t,!0,i)},t.prototype.writeDoubleBE=function(e,t,i){return R(this,e,t,!1,i)},t.prototype.copy=function(e,i,n,r){if(!t.isBuffer(e))throw new TypeError("argument should be a Buffer");if(n||(n=0),r||0===r||(r=this.length),i>=e.length&&(i=e.length),i||(i=0),r>0&&r=this.length)throw new RangeError("Index out of range");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-i=0;--s)e[s+i]=this[s+n];else Uint8Array.prototype.set.call(e,this.subarray(n,r),i);return a},t.prototype.fill=function(e,i,n,r){if("string"==typeof e){if("string"==typeof i?(r=i,i=0,n=this.length):"string"==typeof n&&(r=n,n=this.length),void 0!==r&&"string"!=typeof r)throw new TypeError("encoding must be a string");if("string"==typeof r&&!t.isEncoding(r))throw new TypeError("Unknown encoding: "+r);if(1===e.length){var a=e.charCodeAt(0);("utf8"===r&&a<128||"latin1"===r)&&(e=a)}}else"number"==typeof e&&(e&=255);if(i<0||this.length>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"==typeof e)for(s=i;s55295&&i<57344){if(!r){if(i>56319){(t-=3)>-1&&a.push(239,191,189);continue}if(s+1===n){(t-=3)>-1&&a.push(239,191,189);continue}r=i;continue}if(i<56320){(t-=3)>-1&&a.push(239,191,189),r=i;continue}i=65536+(r-55296<<10|i-56320)}else r&&(t-=3)>-1&&a.push(239,191,189);if(r=null,i<128){if((t-=1)<0)break;a.push(i)}else if(i<2048){if((t-=2)<0)break;a.push(i>>6|192,63&i|128)}else if(i<65536){if((t-=3)<0)break;a.push(i>>12|224,i>>6&63|128,63&i|128)}else{if(!(i<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;a.push(i>>18|240,i>>12&63|128,i>>6&63|128,63&i|128)}}return a}function M(e){return n.toByteArray(function(e){if((e=(e=e.split("=")[0]).trim().replace(D,"")).length<2)return"";for(;e.length%4!=0;)e+="=";return e}(e))}function F(e,t,i,n){for(var r=0;r=t.length||r>=e.length);++r)t[r+i]=e[r];return r}function B(e,t){return e instanceof t||null!=e&&null!=e.constructor&&null!=e.constructor.name&&e.constructor.name===t.name}function N(e){return e!=e}}).call(this,e("buffer").Buffer)},{"base64-js":30,buffer:32,ieee754:35}],33:[function(e,t,i){(function(i){var n,r=void 0!==i?i:"undefined"!=typeof window?window:{},a=e("min-document");"undefined"!=typeof document?n=document:(n=r["__GLOBAL_DOCUMENT_CACHE@4"])||(n=r["__GLOBAL_DOCUMENT_CACHE@4"]=a),t.exports=n}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"min-document":31}],34:[function(e,t,i){(function(e){var i;i="undefined"!=typeof window?window:void 0!==e?e:"undefined"!=typeof self?self:{},t.exports=i}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],35:[function(e,t,i){i.read=function(e,t,i,n,r){var a,s,o=8*r-n-1,u=(1<>1,h=-7,d=i?r-1:0,c=i?-1:1,f=e[t+d];for(d+=c,a=f&(1<<-h)-1,f>>=-h,h+=o;h>0;a=256*a+e[t+d],d+=c,h-=8);for(s=a&(1<<-h)-1,a>>=-h,h+=n;h>0;s=256*s+e[t+d],d+=c,h-=8);if(0===a)a=1-l;else{if(a===u)return s?NaN:1/0*(f?-1:1);s+=Math.pow(2,n),a-=l}return(f?-1:1)*s*Math.pow(2,a-n)},i.write=function(e,t,i,n,r,a){var s,o,u,l=8*a-r-1,h=(1<>1,c=23===r?Math.pow(2,-24)-Math.pow(2,-77):0,f=n?0:a-1,p=n?1:-1,m=t<0||0===t&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(o=isNaN(t)?1:0,s=h):(s=Math.floor(Math.log(t)/Math.LN2),t*(u=Math.pow(2,-s))<1&&(s--,u*=2),(t+=s+d>=1?c/u:c*Math.pow(2,1-d))*u>=2&&(s++,u/=2),s+d>=h?(o=0,s=h):s+d>=1?(o=(t*u-1)*Math.pow(2,r),s+=d):(o=t*Math.pow(2,d-1)*Math.pow(2,r),s=0));r>=8;e[i+f]=255&o,f+=p,o/=256,r-=8);for(s=s<0;e[i+f]=255&s,f+=p,s/=256,l-=8);e[i+f-p]|=128*m}},{}],36:[function(e,t,i){t.exports=function(e){if(!e)return!1;var t=n.call(e);return"[object Function]"===t||"function"==typeof e&&"[object RegExp]"!==t||"undefined"!=typeof window&&(e===window.setTimeout||e===window.alert||e===window.confirm||e===window.prompt)};var n=Object.prototype.toString},{}],37:[function(e,t,i){function n(e){if(e&&"object"==typeof e){var t=e.which||e.keyCode||e.charCode;t&&(e=t)}if("number"==typeof e)return o[e];var i,n=String(e);return(i=r[n.toLowerCase()])?i:(i=a[n.toLowerCase()])||(1===n.length?n.charCodeAt(0):void 0)}n.isEventKey=function(e,t){if(e&&"object"==typeof e){var i=e.which||e.keyCode||e.charCode;if(null==i)return!1;if("string"==typeof t){var n;if(n=r[t.toLowerCase()])return n===i;if(n=a[t.toLowerCase()])return n===i}else if("number"==typeof t)return t===i;return!1}};var r=(i=t.exports=n).code=i.codes={backspace:8,tab:9,enter:13,shift:16,ctrl:17,alt:18,"pause/break":19,"caps lock":20,esc:27,space:32,"page up":33,"page down":34,end:35,home:36,left:37,up:38,right:39,down:40,insert:45,delete:46,command:91,"left command":91,"right command":93,"numpad *":106,"numpad +":107,"numpad -":109,"numpad .":110,"numpad /":111,"num lock":144,"scroll lock":145,"my computer":182,"my calculator":183,";":186,"=":187,",":188,"-":189,".":190,"/":191,"`":192,"[":219,"\\":220,"]":221,"'":222},a=i.aliases={windows:91,"⇧":16,"⌥":18,"⌃":17,"⌘":91,ctl:17,control:17,option:18,pause:19,break:19,caps:20,return:13,escape:27,spc:32,spacebar:32,pgup:33,pgdn:34,ins:45,del:46,cmd:91}; +/*! + * Programatically add the following + */ +for(s=97;s<123;s++)r[String.fromCharCode(s)]=s-32;for(var s=48;s<58;s++)r[s-48]=s;for(s=1;s<13;s++)r["f"+s]=s+111;for(s=0;s<10;s++)r["numpad "+s]=s+96;var o=i.names=i.title={};for(s in r)o[r[s]]=s;for(var u in a)r[u]=a[u]},{}],38:[function(e,t,i){ +/*! @name m3u8-parser @version 4.7.0 @license Apache-2.0 */ +"use strict";Object.defineProperty(i,"__esModule",{value:!0});var n=e("@babel/runtime/helpers/inheritsLoose"),r=e("@videojs/vhs-utils/cjs/stream.js"),a=e("@babel/runtime/helpers/extends"),s=e("@babel/runtime/helpers/assertThisInitialized"),o=e("@videojs/vhs-utils/cjs/decode-b64-to-uint8-array.js");function u(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var l=u(n),h=u(r),d=u(a),c=u(s),f=u(o),p=function(e){function t(){var t;return(t=e.call(this)||this).buffer="",t}return l.default(t,e),t.prototype.push=function(e){var t;for(this.buffer+=e,t=this.buffer.indexOf("\n");t>-1;t=this.buffer.indexOf("\n"))this.trigger("data",this.buffer.substring(0,t)),this.buffer=this.buffer.substring(t+1)},t}(h.default),m=String.fromCharCode(9),_=function(e){var t=/([0-9.]*)?@?([0-9.]*)?/.exec(e||""),i={};return t[1]&&(i.length=parseInt(t[1],10)),t[2]&&(i.offset=parseInt(t[2],10)),i},g=function(e){for(var t,i=e.split(new RegExp('(?:^|,)((?:[^=]*)=(?:"[^"]*"|[^,]*))')),n={},r=i.length;r--;)""!==i[r]&&((t=/([^=]*)=(.*)/.exec(i[r]).slice(1))[0]=t[0].replace(/^\s+|\s+$/g,""),t[1]=t[1].replace(/^\s+|\s+$/g,""),t[1]=t[1].replace(/^['"](.*)['"]$/g,"$1"),n[t[0]]=t[1]);return n},v=function(e){function t(){var t;return(t=e.call(this)||this).customParsers=[],t.tagMappers=[],t}l.default(t,e);var i=t.prototype;return i.push=function(e){var t,i,n=this;0!==(e=e.trim()).length&&("#"===e[0]?this.tagMappers.reduce((function(t,i){var n=i(e);return n===e?t:t.concat([n])}),[e]).forEach((function(e){for(var r=0;r0&&(s.duration=e.duration),0===e.duration&&(s.duration=.01,this.trigger("info",{message:"updating zero segment duration to a small value"})),this.manifest.segments=a},key:function(){if(e.attributes)if("NONE"!==e.attributes.METHOD)if(e.attributes.URI){if("com.apple.streamingkeydelivery"===e.attributes.KEYFORMAT)return this.manifest.contentProtection=this.manifest.contentProtection||{},void(this.manifest.contentProtection["com.apple.fps.1_0"]={attributes:e.attributes});if("urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"===e.attributes.KEYFORMAT){return-1===["SAMPLE-AES","SAMPLE-AES-CTR","SAMPLE-AES-CENC"].indexOf(e.attributes.METHOD)?void this.trigger("warn",{message:"invalid key method provided for Widevine"}):("SAMPLE-AES-CENC"===e.attributes.METHOD&&this.trigger("warn",{message:"SAMPLE-AES-CENC is deprecated, please use SAMPLE-AES-CTR instead"}),"data:text/plain;base64,"!==e.attributes.URI.substring(0,23)?void this.trigger("warn",{message:"invalid key URI provided for Widevine"}):e.attributes.KEYID&&"0x"===e.attributes.KEYID.substring(0,2)?(this.manifest.contentProtection=this.manifest.contentProtection||{},void(this.manifest.contentProtection["com.widevine.alpha"]={attributes:{schemeIdUri:e.attributes.KEYFORMAT,keyId:e.attributes.KEYID.substring(2)},pssh:f.default(e.attributes.URI.split(",")[1])})):void this.trigger("warn",{message:"invalid key ID provided for Widevine"}))}e.attributes.METHOD||this.trigger("warn",{message:"defaulting key method to AES-128"}),n={method:e.attributes.METHOD||"AES-128",uri:e.attributes.URI},void 0!==e.attributes.IV&&(n.iv=e.attributes.IV)}else this.trigger("warn",{message:"ignoring key declaration without URI"});else n=null;else this.trigger("warn",{message:"ignoring key declaration without attribute list"})},"media-sequence":function(){isFinite(e.number)?this.manifest.mediaSequence=e.number:this.trigger("warn",{message:"ignoring invalid media sequence: "+e.number})},"discontinuity-sequence":function(){isFinite(e.number)?(this.manifest.discontinuitySequence=e.number,h=e.number):this.trigger("warn",{message:"ignoring invalid discontinuity sequence: "+e.number})},"playlist-type":function(){/VOD|EVENT/.test(e.playlistType)?this.manifest.playlistType=e.playlistType:this.trigger("warn",{message:"ignoring unknown playlist type: "+e.playlist})},map:function(){i={},e.uri&&(i.uri=e.uri),e.byterange&&(i.byterange=e.byterange),n&&(i.key=n)},"stream-inf":function(){this.manifest.playlists=a,this.manifest.mediaGroups=this.manifest.mediaGroups||l,e.attributes?(s.attributes||(s.attributes={}),d.default(s.attributes,e.attributes)):this.trigger("warn",{message:"ignoring empty stream-inf attributes"})},media:function(){if(this.manifest.mediaGroups=this.manifest.mediaGroups||l,e.attributes&&e.attributes.TYPE&&e.attributes["GROUP-ID"]&&e.attributes.NAME){var i=this.manifest.mediaGroups[e.attributes.TYPE];i[e.attributes["GROUP-ID"]]=i[e.attributes["GROUP-ID"]]||{},t=i[e.attributes["GROUP-ID"]],(c={default:/yes/i.test(e.attributes.DEFAULT)}).default?c.autoselect=!0:c.autoselect=/yes/i.test(e.attributes.AUTOSELECT),e.attributes.LANGUAGE&&(c.language=e.attributes.LANGUAGE),e.attributes.URI&&(c.uri=e.attributes.URI),e.attributes["INSTREAM-ID"]&&(c.instreamId=e.attributes["INSTREAM-ID"]),e.attributes.CHARACTERISTICS&&(c.characteristics=e.attributes.CHARACTERISTICS),e.attributes.FORCED&&(c.forced=/yes/i.test(e.attributes.FORCED)),t[e.attributes.NAME]=c}else this.trigger("warn",{message:"ignoring incomplete or missing media group"})},discontinuity:function(){h+=1,s.discontinuity=!0,this.manifest.discontinuityStarts.push(a.length)},"program-date-time":function(){void 0===this.manifest.dateTimeString&&(this.manifest.dateTimeString=e.dateTimeString,this.manifest.dateTimeObject=e.dateTimeObject),s.dateTimeString=e.dateTimeString,s.dateTimeObject=e.dateTimeObject},targetduration:function(){!isFinite(e.duration)||e.duration<0?this.trigger("warn",{message:"ignoring invalid target duration: "+e.duration}):(this.manifest.targetDuration=e.duration,b.call(this,this.manifest))},start:function(){e.attributes&&!isNaN(e.attributes["TIME-OFFSET"])?this.manifest.start={timeOffset:e.attributes["TIME-OFFSET"],precise:e.attributes.PRECISE}:this.trigger("warn",{message:"ignoring start declaration without appropriate attribute list"})},"cue-out":function(){s.cueOut=e.data},"cue-out-cont":function(){s.cueOutCont=e.data},"cue-in":function(){s.cueIn=e.data},skip:function(){this.manifest.skip=y(e.attributes),this.warnOnMissingAttributes_("#EXT-X-SKIP",e.attributes,["SKIPPED-SEGMENTS"])},part:function(){var t=this;o=!0;var i=this.manifest.segments.length,n=y(e.attributes);s.parts=s.parts||[],s.parts.push(n),n.byterange&&(n.byterange.hasOwnProperty("offset")||(n.byterange.offset=_),_=n.byterange.offset+n.byterange.length);var r=s.parts.length-1;this.warnOnMissingAttributes_("#EXT-X-PART #"+r+" for segment #"+i,e.attributes,["URI","DURATION"]),this.manifest.renditionReports&&this.manifest.renditionReports.forEach((function(e,i){e.hasOwnProperty("lastPart")||t.trigger("warn",{message:"#EXT-X-RENDITION-REPORT #"+i+" lacks required attribute(s): LAST-PART"})}))},"server-control":function(){var t=this.manifest.serverControl=y(e.attributes);t.hasOwnProperty("canBlockReload")||(t.canBlockReload=!1,this.trigger("info",{message:"#EXT-X-SERVER-CONTROL defaulting CAN-BLOCK-RELOAD to false"})),b.call(this,this.manifest),t.canSkipDateranges&&!t.hasOwnProperty("canSkipUntil")&&this.trigger("warn",{message:"#EXT-X-SERVER-CONTROL lacks required attribute CAN-SKIP-UNTIL which is required when CAN-SKIP-DATERANGES is set"})},"preload-hint":function(){var t=this.manifest.segments.length,i=y(e.attributes),n=i.type&&"PART"===i.type;s.preloadHints=s.preloadHints||[],s.preloadHints.push(i),i.byterange&&(i.byterange.hasOwnProperty("offset")||(i.byterange.offset=n?_:0,n&&(_=i.byterange.offset+i.byterange.length)));var r=s.preloadHints.length-1;if(this.warnOnMissingAttributes_("#EXT-X-PRELOAD-HINT #"+r+" for segment #"+t,e.attributes,["TYPE","URI"]),i.type)for(var a=0;a=r&&console.debug("["+a.getDurationString(new Date-n,1e3)+"]","["+e+"]",t)},log:function(e,t){this.debug(e.msg)},info:function(e,t){2>=r&&console.info("["+a.getDurationString(new Date-n,1e3)+"]","["+e+"]",t)},warn:function(e,t){3>=r&&a.getDurationString(new Date-n,1e3)},error:function(e,t){4>=r&&console.error("["+a.getDurationString(new Date-n,1e3)+"]","["+e+"]",t)}});a.getDurationString=function(e,t){var i;function n(e,t){for(var i=(""+e).split(".");i[0].length0){for(var i="",n=0;n0&&(i+=","),i+="["+a.getDurationString(e.start(n))+","+a.getDurationString(e.end(n))+"]";return i}return"(empty)"},void 0!==i&&(i.Log=a);var s=function(e){if(!(e instanceof ArrayBuffer))throw"Needs an array buffer";this.buffer=e,this.dataview=new DataView(e),this.position=0};s.prototype.getPosition=function(){return this.position},s.prototype.getEndPosition=function(){return this.buffer.byteLength},s.prototype.getLength=function(){return this.buffer.byteLength},s.prototype.seek=function(e){var t=Math.max(0,Math.min(this.buffer.byteLength,e));return this.position=isNaN(t)||!isFinite(t)?0:t,!0},s.prototype.isEos=function(){return this.getPosition()>=this.getEndPosition()},s.prototype.readAnyInt=function(e,t){var i=0;if(this.position+e<=this.buffer.byteLength){switch(e){case 1:i=t?this.dataview.getInt8(this.position):this.dataview.getUint8(this.position);break;case 2:i=t?this.dataview.getInt16(this.position):this.dataview.getUint16(this.position);break;case 3:if(t)throw"No method for reading signed 24 bits values";i=this.dataview.getUint8(this.position)<<16,i|=this.dataview.getUint8(this.position)<<8,i|=this.dataview.getUint8(this.position);break;case 4:i=t?this.dataview.getInt32(this.position):this.dataview.getUint32(this.position);break;case 8:if(t)throw"No method for reading signed 64 bits values";i=this.dataview.getUint32(this.position)<<32,i|=this.dataview.getUint32(this.position);break;default:throw"readInt method not implemented for size: "+e}return this.position+=e,i}throw"Not enough bytes in buffer"},s.prototype.readUint8=function(){return this.readAnyInt(1,!1)},s.prototype.readUint16=function(){return this.readAnyInt(2,!1)},s.prototype.readUint24=function(){return this.readAnyInt(3,!1)},s.prototype.readUint32=function(){return this.readAnyInt(4,!1)},s.prototype.readUint64=function(){return this.readAnyInt(8,!1)},s.prototype.readString=function(e){if(this.position+e<=this.buffer.byteLength){for(var t="",i=0;ithis._byteLength&&(this._byteLength=t);else{for(i<1&&(i=1);t>i;)i*=2;var n=new ArrayBuffer(i),r=new Uint8Array(this._buffer);new Uint8Array(n,0,r.length).set(r),this.buffer=n,this._byteLength=t}}},o.prototype._trimAlloc=function(){if(this._byteLength!=this._buffer.byteLength){var e=new ArrayBuffer(this._byteLength),t=new Uint8Array(e),i=new Uint8Array(this._buffer,0,t.length);t.set(i),this.buffer=e}},o.BIG_ENDIAN=!1,o.LITTLE_ENDIAN=!0,o.prototype._byteLength=0,Object.defineProperty(o.prototype,"byteLength",{get:function(){return this._byteLength-this._byteOffset}}),Object.defineProperty(o.prototype,"buffer",{get:function(){return this._trimAlloc(),this._buffer},set:function(e){this._buffer=e,this._dataView=new DataView(this._buffer,this._byteOffset),this._byteLength=this._buffer.byteLength}}),Object.defineProperty(o.prototype,"byteOffset",{get:function(){return this._byteOffset},set:function(e){this._byteOffset=e,this._dataView=new DataView(this._buffer,this._byteOffset),this._byteLength=this._buffer.byteLength}}),Object.defineProperty(o.prototype,"dataView",{get:function(){return this._dataView},set:function(e){this._byteOffset=e.byteOffset,this._buffer=e.buffer,this._dataView=new DataView(this._buffer,this._byteOffset),this._byteLength=this._byteOffset+e.byteLength}}),o.prototype.seek=function(e){var t=Math.max(0,Math.min(this.byteLength,e));this.position=isNaN(t)||!isFinite(t)?0:t},o.prototype.isEof=function(){return this.position>=this._byteLength},o.prototype.mapUint8Array=function(e){this._realloc(1*e);var t=new Uint8Array(this._buffer,this.byteOffset+this.position,e);return this.position+=1*e,t},o.prototype.readInt32Array=function(e,t){e=null==e?this.byteLength-this.position/4:e;var i=new Int32Array(e);return o.memcpy(i.buffer,0,this.buffer,this.byteOffset+this.position,e*i.BYTES_PER_ELEMENT),o.arrayToNative(i,null==t?this.endianness:t),this.position+=i.byteLength,i},o.prototype.readInt16Array=function(e,t){e=null==e?this.byteLength-this.position/2:e;var i=new Int16Array(e);return o.memcpy(i.buffer,0,this.buffer,this.byteOffset+this.position,e*i.BYTES_PER_ELEMENT),o.arrayToNative(i,null==t?this.endianness:t),this.position+=i.byteLength,i},o.prototype.readInt8Array=function(e){e=null==e?this.byteLength-this.position:e;var t=new Int8Array(e);return o.memcpy(t.buffer,0,this.buffer,this.byteOffset+this.position,e*t.BYTES_PER_ELEMENT),this.position+=t.byteLength,t},o.prototype.readUint32Array=function(e,t){e=null==e?this.byteLength-this.position/4:e;var i=new Uint32Array(e);return o.memcpy(i.buffer,0,this.buffer,this.byteOffset+this.position,e*i.BYTES_PER_ELEMENT),o.arrayToNative(i,null==t?this.endianness:t),this.position+=i.byteLength,i},o.prototype.readUint16Array=function(e,t){e=null==e?this.byteLength-this.position/2:e;var i=new Uint16Array(e);return o.memcpy(i.buffer,0,this.buffer,this.byteOffset+this.position,e*i.BYTES_PER_ELEMENT),o.arrayToNative(i,null==t?this.endianness:t),this.position+=i.byteLength,i},o.prototype.readUint8Array=function(e){e=null==e?this.byteLength-this.position:e;var t=new Uint8Array(e);return o.memcpy(t.buffer,0,this.buffer,this.byteOffset+this.position,e*t.BYTES_PER_ELEMENT),this.position+=t.byteLength,t},o.prototype.readFloat64Array=function(e,t){e=null==e?this.byteLength-this.position/8:e;var i=new Float64Array(e);return o.memcpy(i.buffer,0,this.buffer,this.byteOffset+this.position,e*i.BYTES_PER_ELEMENT),o.arrayToNative(i,null==t?this.endianness:t),this.position+=i.byteLength,i},o.prototype.readFloat32Array=function(e,t){e=null==e?this.byteLength-this.position/4:e;var i=new Float32Array(e);return o.memcpy(i.buffer,0,this.buffer,this.byteOffset+this.position,e*i.BYTES_PER_ELEMENT),o.arrayToNative(i,null==t?this.endianness:t),this.position+=i.byteLength,i},o.prototype.readInt32=function(e){var t=this._dataView.getInt32(this.position,null==e?this.endianness:e);return this.position+=4,t},o.prototype.readInt16=function(e){var t=this._dataView.getInt16(this.position,null==e?this.endianness:e);return this.position+=2,t},o.prototype.readInt8=function(){var e=this._dataView.getInt8(this.position);return this.position+=1,e},o.prototype.readUint32=function(e){var t=this._dataView.getUint32(this.position,null==e?this.endianness:e);return this.position+=4,t},o.prototype.readUint16=function(e){var t=this._dataView.getUint16(this.position,null==e?this.endianness:e);return this.position+=2,t},o.prototype.readUint8=function(){var e=this._dataView.getUint8(this.position);return this.position+=1,e},o.prototype.readFloat32=function(e){var t=this._dataView.getFloat32(this.position,null==e?this.endianness:e);return this.position+=4,t},o.prototype.readFloat64=function(e){var t=this._dataView.getFloat64(this.position,null==e?this.endianness:e);return this.position+=8,t},o.endianness=new Int8Array(new Int16Array([1]).buffer)[0]>0,o.memcpy=function(e,t,i,n,r){var a=new Uint8Array(e,t,r),s=new Uint8Array(i,n,r);a.set(s)},o.arrayToNative=function(e,t){return t==this.endianness?e:this.flipArrayEndianness(e)},o.nativeToEndian=function(e,t){return this.endianness==t?e:this.flipArrayEndianness(e)},o.flipArrayEndianness=function(e){for(var t=new Uint8Array(e.buffer,e.byteOffset,e.byteLength),i=0;ir;n--,r++){var a=t[r];t[r]=t[n],t[n]=a}return e},o.prototype.failurePosition=0,String.fromCharCodeUint8=function(e){for(var t=[],i=0;i>16),this.writeUint8((65280&e)>>8),this.writeUint8(255&e)},o.prototype.adjustUint32=function(e,t){var i=this.position;this.seek(e),this.writeUint32(t),this.seek(i)},o.prototype.mapInt32Array=function(e,t){this._realloc(4*e);var i=new Int32Array(this._buffer,this.byteOffset+this.position,e);return o.arrayToNative(i,null==t?this.endianness:t),this.position+=4*e,i},o.prototype.mapInt16Array=function(e,t){this._realloc(2*e);var i=new Int16Array(this._buffer,this.byteOffset+this.position,e);return o.arrayToNative(i,null==t?this.endianness:t),this.position+=2*e,i},o.prototype.mapInt8Array=function(e){this._realloc(1*e);var t=new Int8Array(this._buffer,this.byteOffset+this.position,e);return this.position+=1*e,t},o.prototype.mapUint32Array=function(e,t){this._realloc(4*e);var i=new Uint32Array(this._buffer,this.byteOffset+this.position,e);return o.arrayToNative(i,null==t?this.endianness:t),this.position+=4*e,i},o.prototype.mapUint16Array=function(e,t){this._realloc(2*e);var i=new Uint16Array(this._buffer,this.byteOffset+this.position,e);return o.arrayToNative(i,null==t?this.endianness:t),this.position+=2*e,i},o.prototype.mapFloat64Array=function(e,t){this._realloc(8*e);var i=new Float64Array(this._buffer,this.byteOffset+this.position,e);return o.arrayToNative(i,null==t?this.endianness:t),this.position+=8*e,i},o.prototype.mapFloat32Array=function(e,t){this._realloc(4*e);var i=new Float32Array(this._buffer,this.byteOffset+this.position,e);return o.arrayToNative(i,null==t?this.endianness:t),this.position+=4*e,i};var l=function(e){this.buffers=[],this.bufferIndex=-1,e&&(this.insertBuffer(e),this.bufferIndex=0)};(l.prototype=new o(new ArrayBuffer,0,o.BIG_ENDIAN)).initialized=function(){var e;return this.bufferIndex>-1||(this.buffers.length>0?0===(e=this.buffers[0]).fileStart?(this.buffer=e,this.bufferIndex=0,a.debug("MultiBufferStream","Stream ready for parsing"),!0):(a.warn("MultiBufferStream","The first buffer should have a fileStart of 0"),this.logBufferLevel(),!1):(a.warn("MultiBufferStream","No buffer to start parsing from"),this.logBufferLevel(),!1))},ArrayBuffer.concat=function(e,t){a.debug("ArrayBuffer","Trying to create a new buffer of size: "+(e.byteLength+t.byteLength));var i=new Uint8Array(e.byteLength+t.byteLength);return i.set(new Uint8Array(e),0),i.set(new Uint8Array(t),e.byteLength),i.buffer},l.prototype.reduceBuffer=function(e,t,i){var n;return(n=new Uint8Array(i)).set(new Uint8Array(e,t,i)),n.buffer.fileStart=e.fileStart+t,n.buffer.usedBytes=0,n.buffer},l.prototype.insertBuffer=function(e){for(var t=!0,i=0;in.byteLength){this.buffers.splice(i,1),i--;continue}a.warn("MultiBufferStream","Buffer (fileStart: "+e.fileStart+" - Length: "+e.byteLength+") already appended, ignoring")}else e.fileStart+e.byteLength<=n.fileStart||(e=this.reduceBuffer(e,0,n.fileStart-e.fileStart)),a.debug("MultiBufferStream","Appending new buffer (fileStart: "+e.fileStart+" - Length: "+e.byteLength+")"),this.buffers.splice(i,0,e),0===i&&(this.buffer=e);t=!1;break}if(e.fileStart0)){t=!1;break}e=this.reduceBuffer(e,r,s)}}t&&(a.debug("MultiBufferStream","Appending new buffer (fileStart: "+e.fileStart+" - Length: "+e.byteLength+")"),this.buffers.push(e),0===i&&(this.buffer=e))},l.prototype.logBufferLevel=function(e){var t,i,n,r,s,o=[],u="";for(n=0,r=0,t=0;t0&&(u+=s.end-1+"]");var l=e?a.info:a.debug;0===this.buffers.length?l("MultiBufferStream","No more buffer in memory"):l("MultiBufferStream",this.buffers.length+" stored buffer(s) ("+n+"/"+r+" bytes): "+u)},l.prototype.cleanBuffers=function(){var e,t;for(e=0;e"+this.buffer.byteLength+")"),!0}return!1}return!1},l.prototype.findPosition=function(e,t,i){var n,r=null,s=-1;for(n=!0===e?0:this.bufferIndex;n=t?(a.debug("MultiBufferStream","Found position in existing buffer #"+s),s):-1},l.prototype.findEndContiguousBuf=function(e){var t,i,n,r=void 0!==e?e:this.bufferIndex;if(i=this.buffers[r],this.buffers.length>r+1)for(t=r+1;t>3;return 31===n&&i.data.length>=2&&(n=32+((7&i.data[0])<<3)+((224&i.data[1])>>5)),n}return null},i.DecoderConfigDescriptor=function(e){i.Descriptor.call(this,4,e)},i.DecoderConfigDescriptor.prototype=new i.Descriptor,i.DecoderConfigDescriptor.prototype.parse=function(e){this.oti=e.readUint8(),this.streamType=e.readUint8(),this.bufferSize=e.readUint24(),this.maxBitrate=e.readUint32(),this.avgBitrate=e.readUint32(),this.size-=13,this.parseRemainingDescriptors(e)},i.DecoderSpecificInfo=function(e){i.Descriptor.call(this,5,e)},i.DecoderSpecificInfo.prototype=new i.Descriptor,i.SLConfigDescriptor=function(e){i.Descriptor.call(this,6,e)},i.SLConfigDescriptor.prototype=new i.Descriptor,this};void 0!==i&&(i.MPEG4DescriptorParser=h);var d={ERR_INVALID_DATA:-1,ERR_NOT_ENOUGH_DATA:0,OK:1,BASIC_BOXES:["mdat","idat","free","skip","meco","strk"],FULL_BOXES:["hmhd","nmhd","iods","xml ","bxml","ipro","mere"],CONTAINER_BOXES:[["moov",["trak","pssh"]],["trak"],["edts"],["mdia"],["minf"],["dinf"],["stbl",["sgpd","sbgp"]],["mvex",["trex"]],["moof",["traf"]],["traf",["trun","sgpd","sbgp"]],["vttc"],["tref"],["iref"],["mfra",["tfra"]],["meco"],["hnti"],["hinf"],["strk"],["strd"],["sinf"],["rinf"],["schi"],["trgr"],["udta",["kind"]],["iprp",["ipma"]],["ipco"]],boxCodes:[],fullBoxCodes:[],containerBoxCodes:[],sampleEntryCodes:{},sampleGroupEntryCodes:[],trackGroupTypes:[],UUIDBoxes:{},UUIDs:[],initialize:function(){d.FullBox.prototype=new d.Box,d.ContainerBox.prototype=new d.Box,d.SampleEntry.prototype=new d.Box,d.TrackGroupTypeBox.prototype=new d.FullBox,d.BASIC_BOXES.forEach((function(e){d.createBoxCtor(e)})),d.FULL_BOXES.forEach((function(e){d.createFullBoxCtor(e)})),d.CONTAINER_BOXES.forEach((function(e){d.createContainerBoxCtor(e[0],null,e[1])}))},Box:function(e,t,i){this.type=e,this.size=t,this.uuid=i},FullBox:function(e,t,i){d.Box.call(this,e,t,i),this.flags=0,this.version=0},ContainerBox:function(e,t,i){d.Box.call(this,e,t,i),this.boxes=[]},SampleEntry:function(e,t,i,n){d.ContainerBox.call(this,e,t),this.hdr_size=i,this.start=n},SampleGroupEntry:function(e){this.grouping_type=e},TrackGroupTypeBox:function(e,t){d.FullBox.call(this,e,t)},createBoxCtor:function(e,t){d.boxCodes.push(e),d[e+"Box"]=function(t){d.Box.call(this,e,t)},d[e+"Box"].prototype=new d.Box,t&&(d[e+"Box"].prototype.parse=t)},createFullBoxCtor:function(e,t){d[e+"Box"]=function(t){d.FullBox.call(this,e,t)},d[e+"Box"].prototype=new d.FullBox,d[e+"Box"].prototype.parse=function(e){this.parseFullHeader(e),t&&t.call(this,e)}},addSubBoxArrays:function(e){if(e){this.subBoxNames=e;for(var t=e.length,i=0;ii?(a.error("BoxParser","Box of type '"+h+"' has a size "+l+" greater than its container size "+i),{code:d.ERR_NOT_ENOUGH_DATA,type:h,size:l,hdr_size:u,start:o}):o+l>e.getEndPosition()?(e.seek(o),a.info("BoxParser","Not enough data in stream to parse the entire '"+h+"' box"),{code:d.ERR_NOT_ENOUGH_DATA,type:h,size:l,hdr_size:u,start:o}):t?{code:d.OK,type:h,size:l,hdr_size:u,start:o}:(d[h+"Box"]?n=new d[h+"Box"](l):"uuid"!==h?(a.warn("BoxParser","Unknown box type: '"+h+"'"),(n=new d.Box(h,l)).has_unparsed_data=!0):d.UUIDBoxes[s]?n=new d.UUIDBoxes[s](l):(a.warn("BoxParser","Unknown uuid type: '"+s+"'"),(n=new d.Box(h,l)).uuid=s,n.has_unparsed_data=!0),n.hdr_size=u,n.start=o,n.write===d.Box.prototype.write&&"mdat"!==n.type&&(a.info("BoxParser","'"+c+"' box writing not yet implemented, keeping unparsed data in memory for later write"),n.parseDataAndRewind(e)),n.parse(e),(r=e.getPosition()-(n.start+n.size))<0?(a.warn("BoxParser","Parsing of box '"+c+"' did not read the entire indicated box data size (missing "+-r+" bytes), seeking forward"),e.seek(n.start+n.size)):r>0&&(a.error("BoxParser","Parsing of box '"+c+"' read "+r+" more bytes than the indicated box data size, seeking backwards"),e.seek(n.start+n.size)),{code:d.OK,box:n,size:n.size})},d.Box.prototype.parse=function(e){"mdat"!=this.type?this.data=e.readUint8Array(this.size-this.hdr_size):0===this.size?e.seek(e.getEndPosition()):e.seek(this.start+this.size)},d.Box.prototype.parseDataAndRewind=function(e){this.data=e.readUint8Array(this.size-this.hdr_size),e.position-=this.size-this.hdr_size},d.FullBox.prototype.parseDataAndRewind=function(e){this.parseFullHeader(e),this.data=e.readUint8Array(this.size-this.hdr_size),this.hdr_size-=4,e.position-=this.size-this.hdr_size},d.FullBox.prototype.parseFullHeader=function(e){this.version=e.readUint8(),this.flags=e.readUint24(),this.hdr_size+=4},d.FullBox.prototype.parse=function(e){this.parseFullHeader(e),this.data=e.readUint8Array(this.size-this.hdr_size)},d.ContainerBox.prototype.parse=function(e){for(var t,i;e.getPosition()>10&31,t[1]=this.language>>5&31,t[2]=31&this.language,this.languageString=String.fromCharCode(t[0]+96,t[1]+96,t[2]+96)},d.SAMPLE_ENTRY_TYPE_VISUAL="Visual",d.SAMPLE_ENTRY_TYPE_AUDIO="Audio",d.SAMPLE_ENTRY_TYPE_HINT="Hint",d.SAMPLE_ENTRY_TYPE_METADATA="Metadata",d.SAMPLE_ENTRY_TYPE_SUBTITLE="Subtitle",d.SAMPLE_ENTRY_TYPE_SYSTEM="System",d.SAMPLE_ENTRY_TYPE_TEXT="Text",d.SampleEntry.prototype.parseHeader=function(e){e.readUint8Array(6),this.data_reference_index=e.readUint16(),this.hdr_size+=8},d.SampleEntry.prototype.parse=function(e){this.parseHeader(e),this.data=e.readUint8Array(this.size-this.hdr_size)},d.SampleEntry.prototype.parseDataAndRewind=function(e){this.parseHeader(e),this.data=e.readUint8Array(this.size-this.hdr_size),this.hdr_size-=8,e.position-=this.size-this.hdr_size},d.SampleEntry.prototype.parseFooter=function(e){d.ContainerBox.prototype.parse.call(this,e)},d.createMediaSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_HINT),d.createMediaSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_METADATA),d.createMediaSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_SUBTITLE),d.createMediaSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_SYSTEM),d.createMediaSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_TEXT),d.createMediaSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_VISUAL,(function(e){var t;this.parseHeader(e),e.readUint16(),e.readUint16(),e.readUint32Array(3),this.width=e.readUint16(),this.height=e.readUint16(),this.horizresolution=e.readUint32(),this.vertresolution=e.readUint32(),e.readUint32(),this.frame_count=e.readUint16(),t=Math.min(31,e.readUint8()),this.compressorname=e.readString(t),t<31&&e.readString(31-t),this.depth=e.readUint16(),e.readUint16(),this.parseFooter(e)})),d.createMediaSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_AUDIO,(function(e){this.parseHeader(e),e.readUint32Array(2),this.channel_count=e.readUint16(),this.samplesize=e.readUint16(),e.readUint16(),e.readUint16(),this.samplerate=e.readUint32()/65536,this.parseFooter(e)})),d.createSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_VISUAL,"avc1"),d.createSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_VISUAL,"avc2"),d.createSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_VISUAL,"avc3"),d.createSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_VISUAL,"avc4"),d.createSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_VISUAL,"av01"),d.createSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_VISUAL,"hvc1"),d.createSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_VISUAL,"hev1"),d.createSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_AUDIO,"mp4a"),d.createSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_AUDIO,"ac-3"),d.createSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_AUDIO,"ec-3"),d.createEncryptedSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_VISUAL,"encv"),d.createEncryptedSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_AUDIO,"enca"),d.createEncryptedSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_SUBTITLE,"encu"),d.createEncryptedSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_SYSTEM,"encs"),d.createEncryptedSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_TEXT,"enct"),d.createEncryptedSampleEntryCtor(d.SAMPLE_ENTRY_TYPE_METADATA,"encm"),d.createBoxCtor("av1C",(function(e){var t=e.readUint8();if(t>>7&!1)a.error("av1C marker problem");else if(this.version=127&t,1===this.version)if(t=e.readUint8(),this.seq_profile=t>>5&7,this.seq_level_idx_0=31&t,t=e.readUint8(),this.seq_tier_0=t>>7&1,this.high_bitdepth=t>>6&1,this.twelve_bit=t>>5&1,this.monochrome=t>>4&1,this.chroma_subsampling_x=t>>3&1,this.chroma_subsampling_y=t>>2&1,this.chroma_sample_position=3&t,t=e.readUint8(),this.reserved_1=t>>5&7,0===this.reserved_1){if(this.initial_presentation_delay_present=t>>4&1,1===this.initial_presentation_delay_present)this.initial_presentation_delay_minus_one=15&t;else if(this.reserved_2=15&t,0!==this.reserved_2)return void a.error("av1C reserved_2 parsing problem");var i=this.size-this.hdr_size-4;this.configOBUs=e.readUint8Array(i)}else a.error("av1C reserved_1 parsing problem");else a.error("av1C version "+this.version+" not supported")})),d.createBoxCtor("avcC",(function(e){var t,i;for(this.configurationVersion=e.readUint8(),this.AVCProfileIndication=e.readUint8(),this.profile_compatibility=e.readUint8(),this.AVCLevelIndication=e.readUint8(),this.lengthSizeMinusOne=3&e.readUint8(),this.nb_SPS_nalus=31&e.readUint8(),i=this.size-this.hdr_size-6,this.SPS=[],t=0;t0&&(this.ext=e.readUint8Array(i))})),d.createBoxCtor("btrt",(function(e){this.bufferSizeDB=e.readUint32(),this.maxBitrate=e.readUint32(),this.avgBitrate=e.readUint32()})),d.createBoxCtor("clap",(function(e){this.cleanApertureWidthN=e.readUint32(),this.cleanApertureWidthD=e.readUint32(),this.cleanApertureHeightN=e.readUint32(),this.cleanApertureHeightD=e.readUint32(),this.horizOffN=e.readUint32(),this.horizOffD=e.readUint32(),this.vertOffN=e.readUint32(),this.vertOffD=e.readUint32()})),d.createBoxCtor("clli",(function(e){this.max_content_light_level=e.readUint16(),this.max_pic_average_light_level=e.readUint16()})),d.createFullBoxCtor("co64",(function(e){var t,i;if(t=e.readUint32(),this.chunk_offsets=[],0===this.version)for(i=0;i>7}else("rICC"===this.colour_type||"prof"===this.colour_type)&&(this.ICC_profile=e.readUint8Array(this.size-4))})),d.createFullBoxCtor("cprt",(function(e){this.parseLanguage(e),this.notice=e.readCString()})),d.createFullBoxCtor("cslg",(function(e){0===this.version&&(this.compositionToDTSShift=e.readInt32(),this.leastDecodeToDisplayDelta=e.readInt32(),this.greatestDecodeToDisplayDelta=e.readInt32(),this.compositionStartTime=e.readInt32(),this.compositionEndTime=e.readInt32())})),d.createFullBoxCtor("ctts",(function(e){var t,i;if(t=e.readUint32(),this.sample_counts=[],this.sample_offsets=[],0===this.version)for(i=0;i>6,this.bsid=t>>1&31,this.bsmod=(1&t)<<2|i>>6&3,this.acmod=i>>3&7,this.lfeon=i>>2&1,this.bit_rate_code=3&i|n>>5&7})),d.createBoxCtor("dec3",(function(e){var t=e.readUint16();this.data_rate=t>>3,this.num_ind_sub=7&t,this.ind_subs=[];for(var i=0;i>6,n.bsid=r>>1&31,n.bsmod=(1&r)<<4|a>>4&15,n.acmod=a>>1&7,n.lfeon=1&a,n.num_dep_sub=s>>1&15,n.num_dep_sub>0&&(n.chan_loc=(1&s)<<8|e.readUint8())}})),d.createFullBoxCtor("dfLa",(function(e){var t=[],i=["STREAMINFO","PADDING","APPLICATION","SEEKTABLE","VORBIS_COMMENT","CUESHEET","PICTURE","RESERVED"];for(this.parseFullHeader(e);;){var n=e.readUint8(),r=Math.min(127&n,i.length-1);if(r?e.readUint8Array(e.readUint24()):(e.readUint8Array(13),this.samplerate=e.readUint32()>>12,e.readUint8Array(20)),t.push(i[r]),128&n)break}this.numMetadataBlocks=t.length+" ("+t.join(", ")+")"})),d.createBoxCtor("dimm",(function(e){this.bytessent=e.readUint64()})),d.createBoxCtor("dmax",(function(e){this.time=e.readUint32()})),d.createBoxCtor("dmed",(function(e){this.bytessent=e.readUint64()})),d.createFullBoxCtor("dref",(function(e){var t,i;this.entries=[];for(var n=e.readUint32(),r=0;r=4;)this.compatible_brands[i]=e.readString(4),t-=4,i++})),d.createFullBoxCtor("hdlr",(function(e){0===this.version&&(e.readUint32(),this.handler=e.readString(4),e.readUint32Array(3),this.name=e.readString(this.size-this.hdr_size-20),"\0"===this.name[this.name.length-1]&&(this.name=this.name.slice(0,-1)))})),d.createBoxCtor("hvcC",(function(e){var t,i,n,r;this.configurationVersion=e.readUint8(),r=e.readUint8(),this.general_profile_space=r>>6,this.general_tier_flag=(32&r)>>5,this.general_profile_idc=31&r,this.general_profile_compatibility=e.readUint32(),this.general_constraint_indicator=e.readUint8Array(6),this.general_level_idc=e.readUint8(),this.min_spatial_segmentation_idc=4095&e.readUint16(),this.parallelismType=3&e.readUint8(),this.chroma_format_idc=3&e.readUint8(),this.bit_depth_luma_minus8=7&e.readUint8(),this.bit_depth_chroma_minus8=7&e.readUint8(),this.avgFrameRate=e.readUint16(),r=e.readUint8(),this.constantFrameRate=r>>6,this.numTemporalLayers=(13&r)>>3,this.temporalIdNested=(4&r)>>2,this.lengthSizeMinusOne=3&r,this.nalu_arrays=[];var a=e.readUint8();for(t=0;t>7,s.nalu_type=63&r;var o=e.readUint16();for(i=0;i>4&15,this.length_size=15&t,t=e.readUint8(),this.base_offset_size=t>>4&15,1===this.version||2===this.version?this.index_size=15&t:this.index_size=0,this.items=[];var i=0;if(this.version<2)i=e.readUint16();else{if(2!==this.version)throw"version of iloc box not supported";i=e.readUint32()}for(var n=0;n=2&&(2===this.version?this.item_ID=e.readUint16():3===this.version&&(this.item_ID=e.readUint32()),this.item_protection_index=e.readUint16(),this.item_type=e.readString(4),this.item_name=e.readCString(),"mime"===this.item_type?(this.content_type=e.readCString(),this.content_encoding=e.readCString()):"uri "===this.item_type&&(this.item_uri_type=e.readCString()))})),d.createFullBoxCtor("ipma",(function(e){var t,i;for(entry_count=e.readUint32(),this.associations=[],t=0;t>7==1,1&this.flags?s.property_index=(127&a)<<8|e.readUint8():s.property_index=127&a}}})),d.createFullBoxCtor("iref",(function(e){var t,i;for(this.references=[];e.getPosition()>7,n.assignment_type=127&r,n.assignment_type){case 0:n.grouping_type=e.readString(4);break;case 1:n.grouping_type=e.readString(4),n.grouping_type_parameter=e.readUint32();break;case 2:case 3:break;case 4:n.sub_track_id=e.readUint32();break;default:a.warn("BoxParser","Unknown leva assignement type")}}})),d.createBoxCtor("maxr",(function(e){this.period=e.readUint32(),this.bytes=e.readUint32()})),d.createBoxCtor("mdcv",(function(e){this.display_primaries=[],this.display_primaries[0]={},this.display_primaries[0].x=e.readUint16(),this.display_primaries[0].y=e.readUint16(),this.display_primaries[1]={},this.display_primaries[1].x=e.readUint16(),this.display_primaries[1].y=e.readUint16(),this.display_primaries[2]={},this.display_primaries[2].x=e.readUint16(),this.display_primaries[2].y=e.readUint16(),this.white_point={},this.white_point.x=e.readUint16(),this.white_point.y=e.readUint16(),this.max_display_mastering_luminance=e.readUint32(),this.min_display_mastering_luminance=e.readUint32()})),d.createFullBoxCtor("mdhd",(function(e){1==this.version?(this.creation_time=e.readUint64(),this.modification_time=e.readUint64(),this.timescale=e.readUint32(),this.duration=e.readUint64()):(this.creation_time=e.readUint32(),this.modification_time=e.readUint32(),this.timescale=e.readUint32(),this.duration=e.readUint32()),this.parseLanguage(e),e.readUint16()})),d.createFullBoxCtor("mehd",(function(e){1&this.flags&&(a.warn("BoxParser","mehd box incorrectly uses flags set to 1, converting version to 1"),this.version=1),1==this.version?this.fragment_duration=e.readUint64():this.fragment_duration=e.readUint32()})),d.createFullBoxCtor("meta",(function(e){this.boxes=[],d.ContainerBox.prototype.parse.call(this,e)})),d.createFullBoxCtor("mfhd",(function(e){this.sequence_number=e.readUint32()})),d.createFullBoxCtor("mfro",(function(e){this._size=e.readUint32()})),d.createFullBoxCtor("mvhd",(function(e){1==this.version?(this.creation_time=e.readUint64(),this.modification_time=e.readUint64(),this.timescale=e.readUint32(),this.duration=e.readUint64()):(this.creation_time=e.readUint32(),this.modification_time=e.readUint32(),this.timescale=e.readUint32(),this.duration=e.readUint32()),this.rate=e.readUint32(),this.volume=e.readUint16()>>8,e.readUint16(),e.readUint32Array(2),this.matrix=e.readUint32Array(9),e.readUint32Array(6),this.next_track_id=e.readUint32()})),d.createBoxCtor("npck",(function(e){this.packetssent=e.readUint32()})),d.createBoxCtor("nump",(function(e){this.packetssent=e.readUint64()})),d.createFullBoxCtor("padb",(function(e){var t=e.readUint32();this.padbits=[];for(var i=0;i0){var t=e.readUint32();this.kid=[];for(var i=0;i0&&(this.data=e.readUint8Array(n))})),d.createFullBoxCtor("clef",(function(e){this.width=e.readUint32(),this.height=e.readUint32()})),d.createFullBoxCtor("enof",(function(e){this.width=e.readUint32(),this.height=e.readUint32()})),d.createFullBoxCtor("prof",(function(e){this.width=e.readUint32(),this.height=e.readUint32()})),d.createContainerBoxCtor("tapt",null,["clef","prof","enof"]),d.createBoxCtor("rtp ",(function(e){this.descriptionformat=e.readString(4),this.sdptext=e.readString(this.size-this.hdr_size-4)})),d.createFullBoxCtor("saio",(function(e){1&this.flags&&(this.aux_info_type=e.readUint32(),this.aux_info_type_parameter=e.readUint32());var t=e.readUint32();this.offset=[];for(var i=0;i>7,this.avgRateFlag=t>>6&1,this.durationFlag&&(this.duration=e.readUint32()),this.avgRateFlag&&(this.accurateStatisticsFlag=e.readUint8(),this.avgBitRate=e.readUint16(),this.avgFrameRate=e.readUint16()),this.dependency=[];for(var i=e.readUint8(),n=0;n>7,this.num_leading_samples=127&t})),d.createSampleGroupCtor("rash",(function(e){if(this.operation_point_count=e.readUint16(),this.description_length!==2+(1===this.operation_point_count?2:6*this.operation_point_count)+9)a.warn("BoxParser","Mismatch in "+this.grouping_type+" sample group length"),this.data=e.readUint8Array(this.description_length-2);else{if(1===this.operation_point_count)this.target_rate_share=e.readUint16();else{this.target_rate_share=[],this.available_bitrate=[];for(var t=0;t>4,this.skip_byte_block=15&t,this.isProtected=e.readUint8(),this.Per_Sample_IV_Size=e.readUint8(),this.KID=d.parseHex16(e),this.constant_IV_size=0,this.constant_IV=0,1===this.isProtected&&0===this.Per_Sample_IV_Size&&(this.constant_IV_size=e.readUint8(),this.constant_IV=e.readUint8Array(this.constant_IV_size))})),d.createSampleGroupCtor("stsa",(function(e){a.warn("BoxParser","Sample Group type: "+this.grouping_type+" not fully parsed")})),d.createSampleGroupCtor("sync",(function(e){var t=e.readUint8();this.NAL_unit_type=63&t})),d.createSampleGroupCtor("tele",(function(e){var t=e.readUint8();this.level_independently_decodable=t>>7})),d.createSampleGroupCtor("tsas",(function(e){a.warn("BoxParser","Sample Group type: "+this.grouping_type+" not fully parsed")})),d.createSampleGroupCtor("tscl",(function(e){a.warn("BoxParser","Sample Group type: "+this.grouping_type+" not fully parsed")})),d.createSampleGroupCtor("vipr",(function(e){a.warn("BoxParser","Sample Group type: "+this.grouping_type+" not fully parsed")})),d.createFullBoxCtor("sbgp",(function(e){this.grouping_type=e.readString(4),1===this.version?this.grouping_type_parameter=e.readUint32():this.grouping_type_parameter=0,this.entries=[];for(var t=e.readUint32(),i=0;i>6,this.sample_depends_on[n]=t>>4&3,this.sample_is_depended_on[n]=t>>2&3,this.sample_has_redundancy[n]=3&t})),d.createFullBoxCtor("senc"),d.createFullBoxCtor("sgpd",(function(e){this.grouping_type=e.readString(4),a.debug("BoxParser","Found Sample Groups of type "+this.grouping_type),1===this.version?this.default_length=e.readUint32():this.default_length=0,this.version>=2&&(this.default_group_description_index=e.readUint32()),this.entries=[];for(var t=e.readUint32(),i=0;i>31&1,n.referenced_size=2147483647&r,n.subsegment_duration=e.readUint32(),r=e.readUint32(),n.starts_with_SAP=r>>31&1,n.SAP_type=r>>28&7,n.SAP_delta_time=268435455&r}})),d.SingleItemTypeReferenceBox=function(e,t,i,n){d.Box.call(this,e,t),this.hdr_size=i,this.start=n},d.SingleItemTypeReferenceBox.prototype=new d.Box,d.SingleItemTypeReferenceBox.prototype.parse=function(e){this.from_item_ID=e.readUint16();var t=e.readUint16();this.references=[];for(var i=0;i>4&15,this.sample_sizes[t+1]=15&n}else if(8===this.field_size)for(t=0;t0)for(i=0;i>4&15,this.default_skip_byte_block=15&t}this.default_isProtected=e.readUint8(),this.default_Per_Sample_IV_Size=e.readUint8(),this.default_KID=d.parseHex16(e),1===this.default_isProtected&&0===this.default_Per_Sample_IV_Size&&(this.default_constant_IV_size=e.readUint8(),this.default_constant_IV=e.readUint8Array(this.default_constant_IV_size))})),d.createFullBoxCtor("tfdt",(function(e){1==this.version?this.baseMediaDecodeTime=e.readUint64():this.baseMediaDecodeTime=e.readUint32()})),d.createFullBoxCtor("tfhd",(function(e){var t=0;this.track_id=e.readUint32(),this.size-this.hdr_size>t&&this.flags&d.TFHD_FLAG_BASE_DATA_OFFSET?(this.base_data_offset=e.readUint64(),t+=8):this.base_data_offset=0,this.size-this.hdr_size>t&&this.flags&d.TFHD_FLAG_SAMPLE_DESC?(this.default_sample_description_index=e.readUint32(),t+=4):this.default_sample_description_index=0,this.size-this.hdr_size>t&&this.flags&d.TFHD_FLAG_SAMPLE_DUR?(this.default_sample_duration=e.readUint32(),t+=4):this.default_sample_duration=0,this.size-this.hdr_size>t&&this.flags&d.TFHD_FLAG_SAMPLE_SIZE?(this.default_sample_size=e.readUint32(),t+=4):this.default_sample_size=0,this.size-this.hdr_size>t&&this.flags&d.TFHD_FLAG_SAMPLE_FLAGS?(this.default_sample_flags=e.readUint32(),t+=4):this.default_sample_flags=0})),d.createFullBoxCtor("tfra",(function(e){this.track_ID=e.readUint32(),e.readUint24();var t=e.readUint8();this.length_size_of_traf_num=t>>4&3,this.length_size_of_trun_num=t>>2&3,this.length_size_of_sample_num=3&t,this.entries=[];for(var i=e.readUint32(),n=0;n>8,e.readUint16(),this.matrix=e.readInt32Array(9),this.width=e.readUint32(),this.height=e.readUint32()})),d.createBoxCtor("tmax",(function(e){this.time=e.readUint32()})),d.createBoxCtor("tmin",(function(e){this.time=e.readUint32()})),d.createBoxCtor("totl",(function(e){this.bytessent=e.readUint32()})),d.createBoxCtor("tpay",(function(e){this.bytessent=e.readUint32()})),d.createBoxCtor("tpyl",(function(e){this.bytessent=e.readUint64()})),d.TrackGroupTypeBox.prototype.parse=function(e){this.parseFullHeader(e),this.track_group_id=e.readUint32()},d.createTrackGroupCtor("msrc"),d.TrackReferenceTypeBox=function(e,t,i,n){d.Box.call(this,e,t),this.hdr_size=i,this.start=n},d.TrackReferenceTypeBox.prototype=new d.Box,d.TrackReferenceTypeBox.prototype.parse=function(e){this.track_ids=e.readUint32Array((this.size-this.hdr_size)/4)},d.trefBox.prototype.parse=function(e){for(var t,i;e.getPosition()t&&this.flags&d.TRUN_FLAGS_DATA_OFFSET?(this.data_offset=e.readInt32(),t+=4):this.data_offset=0,this.size-this.hdr_size>t&&this.flags&d.TRUN_FLAGS_FIRST_FLAG?(this.first_sample_flags=e.readUint32(),t+=4):this.first_sample_flags=0,this.sample_duration=[],this.sample_size=[],this.sample_flags=[],this.sample_composition_time_offset=[],this.size-this.hdr_size>t)for(var i=0;i0&&(this.location=e.readCString())})),d.createUUIDBox("a5d40b30e81411ddba2f0800200c9a66",!0,!1,(function(e){this.LiveServerManifest=e.readString(this.size-this.hdr_size).replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")})),d.createUUIDBox("d08a4f1810f34a82b6c832d8aba183d3",!0,!1,(function(e){this.system_id=d.parseHex16(e);var t=e.readUint32();t>0&&(this.data=e.readUint8Array(t))})),d.createUUIDBox("a2394f525a9b4f14a2446c427c648df4",!0,!1),d.createUUIDBox("8974dbce7be74c5184f97148f9882554",!0,!1,(function(e){this.default_AlgorithmID=e.readUint24(),this.default_IV_size=e.readUint8(),this.default_KID=d.parseHex16(e)})),d.createUUIDBox("d4807ef2ca3946958e5426cb9e46a79f",!0,!1,(function(e){this.fragment_count=e.readUint8(),this.entries=[];for(var t=0;t>4,this.chromaSubsampling=t>>1&7,this.videoFullRangeFlag=1&t,this.colourPrimaries=e.readUint8(),this.transferCharacteristics=e.readUint8(),this.matrixCoefficients=e.readUint8(),this.codecIntializationDataSize=e.readUint16(),this.codecIntializationData=e.readUint8Array(this.codecIntializationDataSize)):(this.profile=e.readUint8(),this.level=e.readUint8(),t=e.readUint8(),this.bitDepth=t>>4&15,this.colorSpace=15&t,t=e.readUint8(),this.chromaSubsampling=t>>4&15,this.transferFunction=t>>1&7,this.videoFullRangeFlag=1&t,this.codecIntializationDataSize=e.readUint16(),this.codecIntializationData=e.readUint8Array(this.codecIntializationDataSize))})),d.createBoxCtor("vttC",(function(e){this.text=e.readString(this.size-this.hdr_size)})),d.SampleEntry.prototype.isVideo=function(){return!1},d.SampleEntry.prototype.isAudio=function(){return!1},d.SampleEntry.prototype.isSubtitle=function(){return!1},d.SampleEntry.prototype.isMetadata=function(){return!1},d.SampleEntry.prototype.isHint=function(){return!1},d.SampleEntry.prototype.getCodec=function(){return this.type.replace(".","")},d.SampleEntry.prototype.getWidth=function(){return""},d.SampleEntry.prototype.getHeight=function(){return""},d.SampleEntry.prototype.getChannelCount=function(){return""},d.SampleEntry.prototype.getSampleRate=function(){return""},d.SampleEntry.prototype.getSampleSize=function(){return""},d.VisualSampleEntry.prototype.isVideo=function(){return!0},d.VisualSampleEntry.prototype.getWidth=function(){return this.width},d.VisualSampleEntry.prototype.getHeight=function(){return this.height},d.AudioSampleEntry.prototype.isAudio=function(){return!0},d.AudioSampleEntry.prototype.getChannelCount=function(){return this.channel_count},d.AudioSampleEntry.prototype.getSampleRate=function(){return this.samplerate},d.AudioSampleEntry.prototype.getSampleSize=function(){return this.samplesize},d.SubtitleSampleEntry.prototype.isSubtitle=function(){return!0},d.MetadataSampleEntry.prototype.isMetadata=function(){return!0},d.decimalToHex=function(e,t){var i=Number(e).toString(16);for(t=null==t?t=2:t;i.length>=1;t+=d.decimalToHex(n,0),t+=".",0===this.hvcC.general_tier_flag?t+="L":t+="H",t+=this.hvcC.general_level_idc;var r=!1,a="";for(e=5;e>=0;e--)(this.hvcC.general_constraint_indicator[e]||r)&&(a="."+d.decimalToHex(this.hvcC.general_constraint_indicator[e],0)+a,r=!0);t+=a}return t},d.mp4aSampleEntry.prototype.getCodec=function(){var e=d.SampleEntry.prototype.getCodec.call(this);if(this.esds&&this.esds.esd){var t=this.esds.esd.getOTI(),i=this.esds.esd.getAudioConfig();return e+"."+d.decimalToHex(t)+(i?"."+i:"")}return e},d.stxtSampleEntry.prototype.getCodec=function(){var e=d.SampleEntry.prototype.getCodec.call(this);return this.mime_format?e+"."+this.mime_format:e},d.av01SampleEntry.prototype.getCodec=function(){var e,t=d.SampleEntry.prototype.getCodec.call(this);return 2===this.av1C.seq_profile&&1===this.av1C.high_bitdepth?e=1===this.av1C.twelve_bit?"12":"10":this.av1C.seq_profile<=2&&(e=1===this.av1C.high_bitdepth?"10":"08"),t+"."+this.av1C.seq_profile+"."+this.av1C.seq_level_idx_0+(this.av1C.seq_tier_0?"H":"M")+"."+e},d.Box.prototype.writeHeader=function(e,t){this.size+=8,this.size>u&&(this.size+=8),"uuid"===this.type&&(this.size+=16),a.debug("BoxWriter","Writing box "+this.type+" of size: "+this.size+" at position "+e.getPosition()+(t||"")),this.size>u?e.writeUint32(1):(this.sizePosition=e.getPosition(),e.writeUint32(this.size)),e.writeString(this.type,null,4),"uuid"===this.type&&e.writeUint8Array(this.uuid),this.size>u&&e.writeUint64(this.size)},d.FullBox.prototype.writeHeader=function(e){this.size+=4,d.Box.prototype.writeHeader.call(this,e," v="+this.version+" f="+this.flags),e.writeUint8(this.version),e.writeUint24(this.flags)},d.Box.prototype.write=function(e){"mdat"===this.type?this.data&&(this.size=this.data.length,this.writeHeader(e),e.writeUint8Array(this.data)):(this.size=this.data?this.data.length:0,this.writeHeader(e),this.data&&e.writeUint8Array(this.data))},d.ContainerBox.prototype.write=function(e){this.size=0,this.writeHeader(e);for(var t=0;t=2&&e.writeUint32(this.default_sample_description_index),e.writeUint32(this.entries.length),t=0;t0)for(t=0;t+1-1||e[i]instanceof d.Box||t[i]instanceof d.Box||void 0===e[i]||void 0===t[i]||"function"==typeof e[i]||"function"==typeof t[i]||e.subBoxNames&&e.subBoxNames.indexOf(i.slice(0,4))>-1||t.subBoxNames&&t.subBoxNames.indexOf(i.slice(0,4))>-1||"data"===i||"start"===i||"size"===i||"creation_time"===i||"modification_time"===i||d.DIFF_PRIMITIVE_ARRAY_PROP_NAMES.indexOf(i)>-1||e[i]===t[i]))return!1;return!0},d.boxEqual=function(e,t){if(!d.boxEqualFields(e,t))return!1;for(var i=0;i=t?e:new Array(t-e.length+1).join(i)+e}function r(e){var t=Math.floor(e/3600),i=Math.floor((e-3600*t)/60),r=Math.floor(e-3600*t-60*i),a=Math.floor(1e3*(e-3600*t-60*i-r));return n(t,2)+":"+n(i,2)+":"+n(r,2)+"."+n(a,3)}for(var a=this.parseSample(i),s="",o=0;o1)for(t=1;t-1&&this.fragmentedTracks.splice(t,1)},m.prototype.setExtractionOptions=function(e,t,i){var n=this.getTrackById(e);if(n){var r={};this.extractedTracks.push(r),r.id=e,r.user=t,r.trak=n,n.nextSample=0,r.nb_samples=1e3,r.samples=[],i&&i.nbSamples&&(r.nb_samples=i.nbSamples)}},m.prototype.unsetExtractionOptions=function(e){for(var t=-1,i=0;i-1&&this.extractedTracks.splice(t,1)},m.prototype.parse=function(){var e,t;if(!this.restoreParsePosition||this.restoreParsePosition())for(;;){if(this.hasIncompleteMdat&&this.hasIncompleteMdat()){if(this.processIncompleteMdat())continue;return}if(this.saveParsePosition&&this.saveParsePosition(),(e=d.parseOneBox(this.stream,!1)).code===d.ERR_NOT_ENOUGH_DATA){if(this.processIncompleteBox){if(this.processIncompleteBox(e))continue;return}return}var i;switch(i="uuid"!==(t=e.box).type?t.type:t.uuid,this.boxes.push(t),i){case"mdat":this.mdats.push(t);break;case"moof":this.moofs.push(t);break;case"moov":this.moovStartFound=!0,0===this.mdats.length&&(this.isProgressive=!0);default:void 0!==this[i]&&a.warn("ISOFile","Duplicate Box of type: "+i+", overriding previous occurrence"),this[i]=t}this.updateUsedBytes&&this.updateUsedBytes(t,e)}},m.prototype.checkBuffer=function(e){if(null==e)throw"Buffer must be defined and non empty";if(void 0===e.fileStart)throw"Buffer must have a fileStart property";return 0===e.byteLength?(a.warn("ISOFile","Ignoring empty buffer (fileStart: "+e.fileStart+")"),this.stream.logBufferLevel(),!1):(a.info("ISOFile","Processing buffer (fileStart: "+e.fileStart+")"),e.usedBytes=0,this.stream.insertBuffer(e),this.stream.logBufferLevel(),!!this.stream.initialized()||(a.warn("ISOFile","Not ready to start parsing"),!1))},m.prototype.appendBuffer=function(e,t){var i;if(this.checkBuffer(e))return this.parse(),this.moovStartFound&&!this.moovStartSent&&(this.moovStartSent=!0,this.onMoovStart&&this.onMoovStart()),this.moov?(this.sampleListBuilt||(this.buildSampleLists(),this.sampleListBuilt=!0),this.updateSampleLists(),this.onReady&&!this.readySent&&(this.readySent=!0,this.onReady(this.getInfo())),this.processSamples(t),this.nextSeekPosition?(i=this.nextSeekPosition,this.nextSeekPosition=void 0):i=this.nextParsePosition,this.stream.getEndFilePositionAfter&&(i=this.stream.getEndFilePositionAfter(i))):i=this.nextParsePosition?this.nextParsePosition:0,this.sidx&&this.onSidx&&!this.sidxSent&&(this.onSidx(this.sidx),this.sidxSent=!0),this.meta&&(this.flattenItemInfo&&!this.itemListBuilt&&(this.flattenItemInfo(),this.itemListBuilt=!0),this.processItems&&this.processItems(this.onItem)),this.stream.cleanBuffers&&(a.info("ISOFile","Done processing buffer (fileStart: "+e.fileStart+") - next buffer to fetch should have a fileStart position of "+i),this.stream.logBufferLevel(),this.stream.cleanBuffers(),this.stream.logBufferLevel(!0),a.info("ISOFile","Sample data size in memory: "+this.getAllocatedSampleDataSize())),i},m.prototype.getInfo=function(){var e,t,i,n,r,a={},s=new Date("1904-01-01T00:00:00Z").getTime();if(this.moov)for(a.hasMoov=!0,a.duration=this.moov.mvhd.duration,a.timescale=this.moov.mvhd.timescale,a.isFragmented=null!=this.moov.mvex,a.isFragmented&&this.moov.mvex.mehd&&(a.fragment_duration=this.moov.mvex.mehd.fragment_duration),a.isProgressive=this.isProgressive,a.hasIOD=null!=this.moov.iods,a.brands=[],a.brands.push(this.ftyp.major_brand),a.brands=a.brands.concat(this.ftyp.compatible_brands),a.created=new Date(s+1e3*this.moov.mvhd.creation_time),a.modified=new Date(s+1e3*this.moov.mvhd.modification_time),a.tracks=[],a.audioTracks=[],a.videoTracks=[],a.subtitleTracks=[],a.metadataTracks=[],a.hintTracks=[],a.otherTracks=[],e=0;e0?a.mime+='video/mp4; codecs="':a.audioTracks&&a.audioTracks.length>0?a.mime+='audio/mp4; codecs="':a.mime+='application/mp4; codecs="',e=0;e=i.samples.length)&&(a.info("ISOFile","Sending fragmented data on track #"+n.id+" for samples ["+Math.max(0,i.nextSample-n.nb_samples)+","+(i.nextSample-1)+"]"),a.info("ISOFile","Sample data size in memory: "+this.getAllocatedSampleDataSize()),this.onSegment&&this.onSegment(n.id,n.user,n.segmentStream.buffer,i.nextSample,e||i.nextSample>=i.samples.length),n.segmentStream=null,n!==this.fragmentedTracks[t]))break}}if(null!==this.onSamples)for(t=0;t=i.samples.length)&&(a.debug("ISOFile","Sending samples on track #"+s.id+" for sample "+i.nextSample),this.onSamples&&this.onSamples(s.id,s.user,s.samples),s.samples=[],s!==this.extractedTracks[t]))break}}}},m.prototype.getBox=function(e){var t=this.getBoxes(e,!0);return t.length?t[0]:null},m.prototype.getBoxes=function(e,t){var i=[];return m._sweep.call(this,e,i,t),i},m._sweep=function(e,t,i){for(var n in this.type&&this.type==e&&t.push(this),this.boxes){if(t.length&&i)return;m._sweep.call(this.boxes[n],e,t,i)}},m.prototype.getTrackSamplesInfo=function(e){var t=this.getTrackById(e);return t?t.samples:void 0},m.prototype.getTrackSample=function(e,t){var i=this.getTrackById(e);return this.getSample(i,t)},m.prototype.releaseUsedSamples=function(e,t){var i=0,n=this.getTrackById(e);n.lastValidSample||(n.lastValidSample=0);for(var r=n.lastValidSample;re*r.timescale){l=n-1;break}t&&r.is_sync&&(u=n)}for(t&&(l=u),e=i.samples[l].cts,i.nextSample=l;i.samples[l].alreadyRead===i.samples[l].size&&i.samples[l+1];)l++;return s=i.samples[l].offset+i.samples[l].alreadyRead,a.info("ISOFile","Seeking to "+(t?"RAP":"")+" sample #"+i.nextSample+" on track "+i.tkhd.track_id+", time "+a.getDurationString(e,o)+" and offset: "+s),{offset:s,time:e/o}},m.prototype.seek=function(e,t){var i,n,r,s=this.moov,o={offset:1/0,time:1/0};if(this.moov){for(r=0;r-1){s=o;break}switch(s){case"Visual":r.add("vmhd").set("graphicsmode",0).set("opcolor",[0,0,0]),a.set("width",t.width).set("height",t.height).set("horizresolution",72<<16).set("vertresolution",72<<16).set("frame_count",1).set("compressorname",t.type+" Compressor").set("depth",24);break;case"Audio":r.add("smhd").set("balance",t.balance||0),a.set("channel_count",t.channel_count||2).set("samplesize",t.samplesize||16).set("samplerate",t.samplerate||65536);break;case"Hint":r.add("hmhd");break;case"Subtitle":switch(r.add("sthd"),t.type){case"stpp":a.set("namespace",t.namespace||"nonamespace").set("schema_location",t.schema_location||"").set("auxiliary_mime_types",t.auxiliary_mime_types||"")}break;case"Metadata":case"System":default:r.add("nmhd")}t.description&&a.addBox(t.description),t.description_boxes&&t.description_boxes.forEach((function(e){a.addBox(e)})),r.add("dinf").add("dref").addEntry((new d["url Box"]).set("flags",1));var h=r.add("stbl");return h.add("stsd").addEntry(a),h.add("stts").set("sample_counts",[]).set("sample_deltas",[]),h.add("stsc").set("first_chunk",[]).set("samples_per_chunk",[]).set("sample_description_index",[]),h.add("stco").set("chunk_offsets",[]),h.add("stsz").set("sample_sizes",[]),this.moov.mvex.add("trex").set("track_id",t.id).set("default_sample_description_index",t.default_sample_description_index||1).set("default_sample_duration",t.default_sample_duration||0).set("default_sample_size",t.default_sample_size||0).set("default_sample_flags",t.default_sample_flags||0),this.buildTrakSampleLists(i),t.id}},d.Box.prototype.computeSize=function(e){var t=e||new o;t.endianness=o.BIG_ENDIAN,this.write(t)},m.prototype.addSample=function(e,t,i){var n=i||{},r={},a=this.getTrackById(e);if(null!==a){r.number=a.samples.length,r.track_id=a.tkhd.track_id,r.timescale=a.mdia.mdhd.timescale,r.description_index=n.sample_description_index?n.sample_description_index-1:0,r.description=a.mdia.minf.stbl.stsd.entries[r.description_index],r.data=t,r.size=t.length,r.alreadyRead=r.size,r.duration=n.duration||1,r.cts=n.cts||0,r.dts=n.dts||0,r.is_sync=n.is_sync||!1,r.is_leading=n.is_leading||0,r.depends_on=n.depends_on||0,r.is_depended_on=n.is_depended_on||0,r.has_redundancy=n.has_redundancy||0,r.degradation_priority=n.degradation_priority||0,r.offset=0,r.subsamples=n.subsamples,a.samples.push(r),a.samples_size+=r.size,a.samples_duration+=r.duration,this.processSamples();var s=m.createSingleSampleMoof(r);return this.addBox(s),s.computeSize(),s.trafs[0].truns[0].data_offset=s.size+8,this.add("mdat").data=t,r}},m.createSingleSampleMoof=function(e){var t=new d.moofBox;t.add("mfhd").set("sequence_number",this.nextMoofNumber),this.nextMoofNumber++;var i=t.add("traf");return i.add("tfhd").set("track_id",e.track_id).set("flags",d.TFHD_FLAG_DEFAULT_BASE_IS_MOOF),i.add("tfdt").set("baseMediaDecodeTime",e.dts),i.add("trun").set("flags",d.TRUN_FLAGS_DATA_OFFSET|d.TRUN_FLAGS_DURATION|d.TRUN_FLAGS_SIZE|d.TRUN_FLAGS_FLAGS|d.TRUN_FLAGS_CTS_OFFSET).set("data_offset",0).set("first_sample_flags",0).set("sample_count",1).set("sample_duration",[e.duration]).set("sample_size",[e.size]).set("sample_flags",[0]).set("sample_composition_time_offset",[e.cts-e.dts]),t},m.prototype.lastMoofIndex=0,m.prototype.samplesDataSize=0,m.prototype.resetTables=function(){var e,t,i,n,r,a;for(this.initial_duration=this.moov.mvhd.duration,this.moov.mvhd.duration=0,e=0;e=2&&(u=r[s].grouping_type+"/0",(o=new l(r[s].grouping_type,0)).is_fragment=!0,t.sample_groups_info[u]||(t.sample_groups_info[u]=o))}else for(s=0;s=2&&(u=n[s].grouping_type+"/0",o=new l(n[s].grouping_type,0),e.sample_groups_info[u]||(e.sample_groups_info[u]=o))},m.setSampleGroupProperties=function(e,t,i,n){var r,a;for(r in t.sample_groups=[],n){var s;if(t.sample_groups[r]={},t.sample_groups[r].grouping_type=n[r].grouping_type,t.sample_groups[r].grouping_type_parameter=n[r].grouping_type_parameter,i>=n[r].last_sample_in_run&&(n[r].last_sample_in_run<0&&(n[r].last_sample_in_run=0),n[r].entry_index++,n[r].entry_index<=n[r].sbgp.entries.length-1&&(n[r].last_sample_in_run+=n[r].sbgp.entries[n[r].entry_index].sample_count)),n[r].entry_index<=n[r].sbgp.entries.length-1?t.sample_groups[r].group_description_index=n[r].sbgp.entries[n[r].entry_index].group_description_index:t.sample_groups[r].group_description_index=-1,0!==t.sample_groups[r].group_description_index)s=n[r].fragment_description?n[r].fragment_description:n[r].description,t.sample_groups[r].group_description_index>0?(a=t.sample_groups[r].group_description_index>65535?(t.sample_groups[r].group_description_index>>16)-1:t.sample_groups[r].group_description_index-1,s&&a>=0&&(t.sample_groups[r].description=s.entries[a])):s&&s.version>=2&&s.default_group_description_index>0&&(t.sample_groups[r].description=s.entries[s.default_group_description_index-1])}},m.process_sdtp=function(e,t,i){t&&(e?(t.is_leading=e.is_leading[i],t.depends_on=e.sample_depends_on[i],t.is_depended_on=e.sample_is_depended_on[i],t.has_redundancy=e.sample_has_redundancy[i]):(t.is_leading=0,t.depends_on=0,t.is_depended_on=0,t.has_redundancy=0))},m.prototype.buildSampleLists=function(){var e,t;for(e=0;ey&&(b++,y<0&&(y=0),y+=a.sample_counts[b]),t>0?(e.samples[t-1].duration=a.sample_deltas[b],e.samples_duration+=e.samples[t-1].duration,C.dts=e.samples[t-1].dts+e.samples[t-1].duration):C.dts=0,s?(t>=S&&(T++,S<0&&(S=0),S+=s.sample_counts[T]),C.cts=e.samples[t].dts+s.sample_offsets[T]):C.cts=C.dts,o?(t==o.sample_numbers[E]-1?(C.is_sync=!0,E++):(C.is_sync=!1,C.degradation_priority=0),l&&l.entries[w].sample_delta+A==t+1&&(C.subsamples=l.entries[w].subsamples,A+=l.entries[w].sample_delta,w++)):C.is_sync=!0,m.process_sdtp(e.mdia.minf.stbl.sdtp,C,C.number),C.degradation_priority=c?c.priority[t]:0,l&&l.entries[w].sample_delta+A==t&&(C.subsamples=l.entries[w].subsamples,A+=l.entries[w].sample_delta),(h.length>0||d.length>0)&&m.setSampleGroupProperties(e,C,t,e.sample_groups_info)}t>0&&(e.samples[t-1].duration=Math.max(e.mdia.mdhd.duration-e.samples[t-1].dts,0),e.samples_duration+=e.samples[t-1].duration)}},m.prototype.updateSampleLists=function(){var e,t,i,n,r,a,s,o,u,l,h,c,f,p,_;if(void 0!==this.moov)for(;this.lastMoofIndex0&&m.initSampleGroups(c,h,h.sbgps,c.mdia.minf.stbl.sgpds,h.sgpds),t=0;t0?p.dts=c.samples[c.samples.length-2].dts+c.samples[c.samples.length-2].duration:(h.tfdt?p.dts=h.tfdt.baseMediaDecodeTime:p.dts=0,c.first_traf_merged=!0),p.cts=p.dts,g.flags&d.TRUN_FLAGS_CTS_OFFSET&&(p.cts=p.dts+g.sample_composition_time_offset[i]),_=s,g.flags&d.TRUN_FLAGS_FLAGS?_=g.sample_flags[i]:0===i&&g.flags&d.TRUN_FLAGS_FIRST_FLAG&&(_=g.first_sample_flags),p.is_sync=!(_>>16&1),p.is_leading=_>>26&3,p.depends_on=_>>24&3,p.is_depended_on=_>>22&3,p.has_redundancy=_>>20&3,p.degradation_priority=65535&_;var v=!!(h.tfhd.flags&d.TFHD_FLAG_BASE_DATA_OFFSET),y=!!(h.tfhd.flags&d.TFHD_FLAG_DEFAULT_BASE_IS_MOOF),b=!!(g.flags&d.TRUN_FLAGS_DATA_OFFSET),S=0;S=v?h.tfhd.base_data_offset:y||0===t?l.start:o,p.offset=0===t&&0===i?b?S+g.data_offset:S:o,o=p.offset+p.size,(h.sbgps.length>0||h.sgpds.length>0||c.mdia.minf.stbl.sbgps.length>0||c.mdia.minf.stbl.sgpds.length>0)&&m.setSampleGroupProperties(c,p,p.number_in_traf,h.sample_groups_info)}}if(h.subs){c.has_fragment_subsamples=!0;var T=h.first_sample_index;for(t=0;t-1))return null;var s=(i=this.stream.buffers[r]).byteLength-(n.offset+n.alreadyRead-i.fileStart);if(n.size-n.alreadyRead<=s)return a.debug("ISOFile","Getting sample #"+t+" data (alreadyRead: "+n.alreadyRead+" offset: "+(n.offset+n.alreadyRead-i.fileStart)+" read size: "+(n.size-n.alreadyRead)+" full size: "+n.size+")"),o.memcpy(n.data.buffer,n.alreadyRead,i,n.offset+n.alreadyRead-i.fileStart,n.size-n.alreadyRead),i.usedBytes+=n.size-n.alreadyRead,this.stream.logBufferLevel(),n.alreadyRead=n.size,n;if(0===s)return null;a.debug("ISOFile","Getting sample #"+t+" partial data (alreadyRead: "+n.alreadyRead+" offset: "+(n.offset+n.alreadyRead-i.fileStart)+" read size: "+s+" full size: "+n.size+")"),o.memcpy(n.data.buffer,n.alreadyRead,i,n.offset+n.alreadyRead-i.fileStart,s),n.alreadyRead+=s,i.usedBytes+=s,this.stream.logBufferLevel()}},m.prototype.releaseSample=function(e,t){var i=e.samples[t];return i.data?(this.samplesDataSize-=i.size,i.data=null,i.alreadyRead=0,i.size):0},m.prototype.getAllocatedSampleDataSize=function(){return this.samplesDataSize},m.prototype.getCodecs=function(){var e,t="";for(e=0;e0&&(t+=","),t+=this.moov.traks[e].mdia.minf.stbl.stsd.entries[0].getCodec()}return t},m.prototype.getTrexById=function(e){var t;if(!this.moov||!this.moov.mvex)return null;for(t=0;t0&&(i.protection=r.ipro.protections[r.iinf.item_infos[e].protection_index-1]),r.iinf.item_infos[e].item_type?i.type=r.iinf.item_infos[e].item_type:i.type="mime",i.content_type=r.iinf.item_infos[e].content_type,i.content_encoding=r.iinf.item_infos[e].content_encoding;if(r.iloc)for(e=0;e0){var c=r.iprp.ipco.boxes[d.property_index-1];i.properties[c.type]=c,i.properties.boxes.push(c)}}}}}},m.prototype.getItem=function(e){var t,i;if(!this.meta)return null;if(!(i=this.items[e]).data&&i.size)i.data=new Uint8Array(i.size),i.alreadyRead=0,this.itemsDataSize+=i.size,a.debug("ISOFile","Allocating item #"+e+" of size "+i.size+" (total: "+this.itemsDataSize+")");else if(i.alreadyRead===i.size)return i;for(var n=0;n-1))return null;var u=(t=this.stream.buffers[s]).byteLength-(r.offset+r.alreadyRead-t.fileStart);if(!(r.length-r.alreadyRead<=u))return a.debug("ISOFile","Getting item #"+e+" extent #"+n+" partial data (alreadyRead: "+r.alreadyRead+" offset: "+(r.offset+r.alreadyRead-t.fileStart)+" read size: "+u+" full extent size: "+r.length+" full item size: "+i.size+")"),o.memcpy(i.data.buffer,i.alreadyRead,t,r.offset+r.alreadyRead-t.fileStart,u),r.alreadyRead+=u,i.alreadyRead+=u,t.usedBytes+=u,this.stream.logBufferLevel(),null;a.debug("ISOFile","Getting item #"+e+" extent #"+n+" data (alreadyRead: "+r.alreadyRead+" offset: "+(r.offset+r.alreadyRead-t.fileStart)+" read size: "+(r.length-r.alreadyRead)+" full extent size: "+r.length+" full item size: "+i.size+")"),o.memcpy(i.data.buffer,i.alreadyRead,t,r.offset+r.alreadyRead-t.fileStart,r.length-r.alreadyRead),t.usedBytes+=r.length-r.alreadyRead,this.stream.logBufferLevel(),i.alreadyRead+=r.length-r.alreadyRead,r.alreadyRead=r.length}}return i.alreadyRead===i.size?i:null},m.prototype.releaseItem=function(e){var t=this.items[e];if(t.data){this.itemsDataSize-=t.size,t.data=null,t.alreadyRead=0;for(var i=0;i0?this.moov.traks[e].samples[0].duration:0),t.push(n)}return t},d.Box.prototype.printHeader=function(e){this.size+=8,this.size>u&&(this.size+=8),"uuid"===this.type&&(this.size+=16),e.log(e.indent+"size:"+this.size),e.log(e.indent+"type:"+this.type)},d.FullBox.prototype.printHeader=function(e){this.size+=4,d.Box.prototype.printHeader.call(this,e),e.log(e.indent+"version:"+this.version),e.log(e.indent+"flags:"+this.flags)},d.Box.prototype.print=function(e){this.printHeader(e)},d.ContainerBox.prototype.print=function(e){this.printHeader(e);for(var t=0;t>8)),e.log(e.indent+"matrix: "+this.matrix.join(", ")),e.log(e.indent+"next_track_id: "+this.next_track_id)},d.tkhdBox.prototype.print=function(e){d.FullBox.prototype.printHeader.call(this,e),e.log(e.indent+"creation_time: "+this.creation_time),e.log(e.indent+"modification_time: "+this.modification_time),e.log(e.indent+"track_id: "+this.track_id),e.log(e.indent+"duration: "+this.duration),e.log(e.indent+"volume: "+(this.volume>>8)),e.log(e.indent+"matrix: "+this.matrix.join(", ")),e.log(e.indent+"layer: "+this.layer),e.log(e.indent+"alternate_group: "+this.alternate_group),e.log(e.indent+"width: "+this.width),e.log(e.indent+"height: "+this.height)};var _={createFile:function(e,t){var i=void 0===e||e,n=new m(t);return n.discardMdatData=!i,n}};void 0!==i&&(i.createFile=_.createFile)},{}],40:[function(e,t,i){ +/*! @name mpd-parser @version 0.19.0 @license Apache-2.0 */ +"use strict";Object.defineProperty(i,"__esModule",{value:!0});var n=e("@videojs/vhs-utils/cjs/resolve-url"),r=e("global/window"),a=e("@videojs/vhs-utils/cjs/decode-b64-to-uint8-array"),s=e("@xmldom/xmldom");function o(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var u=o(n),l=o(r),h=o(a),d=function(e){return!!e&&"object"==typeof e},c=function e(){for(var t=arguments.length,i=new Array(t),n=0;n=0&&(f.minimumUpdatePeriod=1e3*u),t&&(f.locations=t),"dynamic"===s&&(f.suggestedPresentationDelay=o);var p=0===f.playlists.length;return h.length&&(f.mediaGroups.AUDIO.audio=function(e,t,i){var n;void 0===t&&(t={}),void 0===i&&(i=!1);var r=e.reduce((function(e,r){var a=r.attributes.role&&r.attributes.role.value||"",s=r.attributes.lang||"",o=r.attributes.label||"main";if(s&&!r.attributes.label){var u=a?" ("+a+")":"";o=""+r.attributes.lang+u}e[o]||(e[o]={language:s,autoselect:!0,default:"main"===a,playlists:[],uri:""});var l=I(function(e,t){var i,n=e.attributes,r=e.segments,a=e.sidx,s={attributes:(i={NAME:n.id,BANDWIDTH:n.bandwidth,CODECS:n.codecs},i["PROGRAM-ID"]=1,i),uri:"",endList:"static"===n.type,timeline:n.periodIndex,resolvedUri:"",targetDuration:n.duration,segments:r,mediaSequence:r.length?r[0].number:1};return n.contentProtection&&(s.contentProtection=n.contentProtection),a&&(s.sidx=a),t&&(s.attributes.AUDIO="audio",s.attributes.SUBTITLES="subs"),s}(r,i),t);return e[o].playlists.push(l),void 0===n&&"main"===a&&((n=r).default=!0),e}),{});n||(r[Object.keys(r)[0]].default=!0);return r}(h,i,p)),d.length&&(f.mediaGroups.SUBTITLES.subs=function(e,t){return void 0===t&&(t={}),e.reduce((function(e,i){var n=i.attributes.lang||"text";return e[n]||(e[n]={language:n,default:!1,autoselect:!1,playlists:[],uri:""}),e[n].playlists.push(I(function(e){var t,i=e.attributes,n=e.segments;void 0===n&&(n=[{uri:i.baseUrl,timeline:i.periodIndex,resolvedUri:i.baseUrl||"",duration:i.sourceDuration,number:0}],i.duration=i.sourceDuration);var r=((t={NAME:i.id,BANDWIDTH:i.bandwidth})["PROGRAM-ID"]=1,t);return i.codecs&&(r.CODECS=i.codecs),{attributes:r,uri:"",endList:"static"===i.type,timeline:i.periodIndex,resolvedUri:i.baseUrl||"",targetDuration:i.duration,segments:n,mediaSequence:n.length?n[0].number:1}}(i),t)),e}),{})}(d,i)),c.length&&(f.mediaGroups["CLOSED-CAPTIONS"].cc=c.reduce((function(e,t){return t?(t.forEach((function(t){var i=t.channel,n=t.language;e[n]={autoselect:!1,default:!1,instreamId:i,language:n},t.hasOwnProperty("aspectRatio")&&(e[n].aspectRatio=t.aspectRatio),t.hasOwnProperty("easyReader")&&(e[n].easyReader=t.easyReader),t.hasOwnProperty("3D")&&(e[n]["3D"]=t["3D"])})),e):e}),{})),f},M=function(e,t,i){var n=e.NOW,r=e.clientOffset,a=e.availabilityStartTime,s=e.timescale,o=void 0===s?1:s,u=e.start,l=void 0===u?0:u,h=e.minimumUpdatePeriod,d=(n+r)/1e3+(void 0===h?0:h)-(a+l);return Math.ceil((d*o-t)/i)},F=function(e,t){for(var i=e.type,n=e.minimumUpdatePeriod,r=void 0===n?0:n,a=e.media,s=void 0===a?"":a,o=e.sourceDuration,u=e.timescale,l=void 0===u?1:u,h=e.startNumber,d=void 0===h?1:h,c=e.periodIndex,f=[],p=-1,m=0;mp&&(p=y);var b=void 0;if(v<0){var S=m+1;b=S===t.length?"dynamic"===i&&r>0&&s.indexOf("$Number$")>0?M(e,p,g):(o*l-p)/g:(t[S].t-p)/g}else b=v+1;for(var T=d+f.length+b,E=d+f.length;E=r?a:""+new Array(r-a.length+1).join("0")+a)}}(t))},j=function(e,t){var i={RepresentationID:e.id,Bandwidth:e.bandwidth||0},n=e.initialization,r=void 0===n?{sourceURL:"",range:""}:n,a=S({baseUrl:e.baseUrl,source:N(r.sourceURL,i),range:r.range});return function(e,t){return e.duration||t?e.duration?w(e):F(e,t):[{number:e.startNumber||1,duration:e.sourceDuration,time:0,timeline:e.periodIndex}]}(e,t).map((function(t){i.Number=t.number,i.Time=t.time;var n=N(e.media||"",i),r=e.timescale||1,s=e.presentationTimeOffset||0,o=e.periodStart+(t.time-s)/r;return{uri:n,timeline:t.timeline,duration:t.duration,resolvedUri:u.default(e.baseUrl||"",n),map:a,number:t.number,presentationTime:o}}))},V=function(e,t){var i=e.duration,n=e.segmentUrls,r=void 0===n?[]:n,a=e.periodStart;if(!i&&!t||i&&t)throw new Error(y);var s,o=r.map((function(t){return function(e,t){var i=e.baseUrl,n=e.initialization,r=void 0===n?{}:n,a=S({baseUrl:i,source:r.sourceURL,range:r.range}),s=S({baseUrl:i,source:t.media,range:t.mediaRange});return s.map=a,s}(e,t)}));return i&&(s=w(e)),t&&(s=F(e,t)),s.map((function(t,i){if(o[i]){var n=o[i],r=e.timescale||1,s=e.presentationTimeOffset||0;return n.timeline=t.timeline,n.duration=t.duration,n.number=t.number,n.presentationTime=a+(t.time-s)/r,n}})).filter((function(e){return e}))},H=function(e){var t,i,n=e.attributes,r=e.segmentInfo;r.template?(i=j,t=c(n,r.template)):r.base?(i=A,t=c(n,r.base)):r.list&&(i=V,t=c(n,r.list));var a={attributes:n};if(!i)return a;var s=i(t,r.segmentTimeline);if(t.duration){var o=t,u=o.duration,l=o.timescale,h=void 0===l?1:l;t.duration=u/h}else s.length?t.duration=s.reduce((function(e,t){return Math.max(e,Math.ceil(t.duration))}),0):t.duration=0;return a.attributes=t,a.segments=s,r.base&&t.indexRange&&(a.sidx=s[0],a.segments=[]),a},z=function(e){return e.map(H)},G=function(e,t){return p(e.childNodes).filter((function(e){return e.tagName===t}))},W=function(e){return e.textContent.trim()},Y=function(e){var t=/P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/.exec(e);if(!t)return 0;var i=t.slice(1),n=i[0],r=i[1],a=i[2],s=i[3],o=i[4],u=i[5];return 31536e3*parseFloat(n||0)+2592e3*parseFloat(r||0)+86400*parseFloat(a||0)+3600*parseFloat(s||0)+60*parseFloat(o||0)+parseFloat(u||0)},q={mediaPresentationDuration:function(e){return Y(e)},availabilityStartTime:function(e){return/^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/.test(t=e)&&(t+="Z"),Date.parse(t)/1e3;var t},minimumUpdatePeriod:function(e){return Y(e)},suggestedPresentationDelay:function(e){return Y(e)},type:function(e){return e},timeShiftBufferDepth:function(e){return Y(e)},start:function(e){return Y(e)},width:function(e){return parseInt(e,10)},height:function(e){return parseInt(e,10)},bandwidth:function(e){return parseInt(e,10)},startNumber:function(e){return parseInt(e,10)},timescale:function(e){return parseInt(e,10)},presentationTimeOffset:function(e){return parseInt(e,10)},duration:function(e){var t=parseInt(e,10);return isNaN(t)?Y(e):t},d:function(e){return parseInt(e,10)},t:function(e){return parseInt(e,10)},r:function(e){return parseInt(e,10)},DEFAULT:function(e){return e}},K=function(e){return e&&e.attributes?p(e.attributes).reduce((function(e,t){var i=q[t.name]||q.DEFAULT;return e[t.name]=i(t.value),e}),{}):{}},X={"urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b":"org.w3.clearkey","urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed":"com.widevine.alpha","urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95":"com.microsoft.playready","urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb":"com.adobe.primetime"},Q=function(e,t){return t.length?f(e.map((function(e){return t.map((function(t){return u.default(e,W(t))}))}))):e},$=function(e){var t=G(e,"SegmentTemplate")[0],i=G(e,"SegmentList")[0],n=i&&G(i,"SegmentURL").map((function(e){return c({tag:"SegmentURL"},K(e))})),r=G(e,"SegmentBase")[0],a=i||t,s=a&&G(a,"SegmentTimeline")[0],o=i||r||t,u=o&&G(o,"Initialization")[0],l=t&&K(t);l&&u?l.initialization=u&&K(u):l&&l.initialization&&(l.initialization={sourceURL:l.initialization});var h={template:l,segmentTimeline:s&&G(s,"S").map((function(e){return K(e)})),list:i&&c(K(i),{segmentUrls:n,initialization:K(u)}),base:r&&c(K(r),{initialization:K(u)})};return Object.keys(h).forEach((function(e){h[e]||delete h[e]})),h},J=function(e,t,i){return function(n){var r,a=K(n),s=Q(t,G(n,"BaseURL")),o=G(n,"Role")[0],u={role:K(o)},l=c(e,a,u),d=G(n,"Accessibility")[0],p="urn:scte:dash:cc:cea-608:2015"===(r=K(d)).schemeIdUri?r.value.split(";").map((function(e){var t,i;if(i=e,/^CC\d=/.test(e)){var n=e.split("=");t=n[0],i=n[1]}else/^CC\d$/.test(e)&&(t=e);return{channel:t,language:i}})):"urn:scte:dash:cc:cea-708:2015"===r.schemeIdUri?r.value.split(";").map((function(e){var t={channel:void 0,language:void 0,aspectRatio:1,easyReader:0,"3D":0};if(/=/.test(e)){var i=e.split("="),n=i[0],r=i[1],a=void 0===r?"":r;t.channel=n,t.language=e,a.split(",").forEach((function(e){var i=e.split(":"),n=i[0],r=i[1];"lang"===n?t.language=r:"er"===n?t.easyReader=Number(r):"war"===n?t.aspectRatio=Number(r):"3D"===n&&(t["3D"]=Number(r))}))}else t.language=e;return t.channel&&(t.channel="SERVICE"+t.channel),t})):void 0;p&&(l=c(l,{captionServices:p}));var m=G(n,"Label")[0];if(m&&m.childNodes.length){var _=m.childNodes[0].nodeValue.trim();l=c(l,{label:_})}var g=G(n,"ContentProtection").reduce((function(e,t){var i=K(t),n=X[i.schemeIdUri];if(n){e[n]={attributes:i};var r=G(t,"cenc:pssh")[0];if(r){var a=W(r),s=a&&h.default(a);e[n].pssh=s}}return e}),{});Object.keys(g).length&&(l=c(l,{contentProtection:g}));var v=$(n),y=G(n,"Representation"),b=c(i,v);return f(y.map(function(e,t,i){return function(n){var r=G(n,"BaseURL"),a=Q(t,r),s=c(e,K(n)),o=$(n);return a.map((function(e){return{segmentInfo:c(i,o),attributes:c(s,{baseUrl:e})}}))}}(l,s,b)))}},Z=function(e,t){return function(i,n){var r=Q(t,G(i.node,"BaseURL")),a=parseInt(i.attributes.id,10),s=l.default.isNaN(a)?n:a,o=c(e,{periodIndex:s,periodStart:i.attributes.start});"number"==typeof i.attributes.duration&&(o.periodDuration=i.attributes.duration);var u=G(i.node,"AdaptationSet"),h=$(i.node);return f(u.map(J(o,r,h)))}},ee=function(e,t){void 0===t&&(t={});var i=t,n=i.manifestUri,r=void 0===n?"":n,a=i.NOW,s=void 0===a?Date.now():a,o=i.clientOffset,u=void 0===o?0:o,l=G(e,"Period");if(!l.length)throw new Error(m);var h=G(e,"Location"),d=K(e),c=Q([r],G(e,"BaseURL"));d.type=d.type||"static",d.sourceDuration=d.mediaPresentationDuration||0,d.NOW=s,d.clientOffset=u,h.length&&(d.locations=h.map(W));var p=[];return l.forEach((function(e,t){var i=K(e),n=p[t-1];i.start=function(e){var t=e.attributes,i=e.priorPeriodAttributes,n=e.mpdType;return"number"==typeof t.start?t.start:i&&"number"==typeof i.start&&"number"==typeof i.duration?i.start+i.duration:i||"static"!==n?null:0}({attributes:i,priorPeriodAttributes:n?n.attributes:null,mpdType:d.type}),p.push({node:e,attributes:i})})),{locations:d.locations,representationInfo:f(p.map(Z(d,c)))}},te=function(e){if(""===e)throw new Error(_);var t,i,n=new s.DOMParser;try{i=(t=n.parseFromString(e,"application/xml"))&&"MPD"===t.documentElement.tagName?t.documentElement:null}catch(e){}if(!i||i&&i.getElementsByTagName("parsererror").length>0)throw new Error(g);return i};i.VERSION="0.19.0",i.addSidxSegmentsToPlaylist=C,i.generateSidxKey=k,i.inheritAttributes=ee,i.parse=function(e,t){void 0===t&&(t={});var i=ee(te(e),t),n=z(i.representationInfo);return U(n,i.locations,t.sidxMapping)},i.parseUTCTiming=function(e){return function(e){var t=G(e,"UTCTiming")[0];if(!t)return null;var i=K(t);switch(i.schemeIdUri){case"urn:mpeg:dash:utc:http-head:2014":case"urn:mpeg:dash:utc:http-head:2012":i.method="HEAD";break;case"urn:mpeg:dash:utc:http-xsdate:2014":case"urn:mpeg:dash:utc:http-iso:2014":case"urn:mpeg:dash:utc:http-xsdate:2012":case"urn:mpeg:dash:utc:http-iso:2012":i.method="GET";break;case"urn:mpeg:dash:utc:direct:2014":case"urn:mpeg:dash:utc:direct:2012":i.method="DIRECT",i.value=Date.parse(i.value);break;case"urn:mpeg:dash:utc:http-ntp:2014":case"urn:mpeg:dash:utc:ntp:2014":case"urn:mpeg:dash:utc:sntp:2014":default:throw new Error(b)}return i}(te(e))},i.stringToMpdXml=te,i.toM3u8=U,i.toPlaylists=z},{"@videojs/vhs-utils/cjs/decode-b64-to-uint8-array":13,"@videojs/vhs-utils/cjs/resolve-url":20,"@xmldom/xmldom":28,"global/window":34}],41:[function(e,t,i){var n,r;n=window,r=function(){return function(e){var t={};function i(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,i),r.l=!0,r.exports}return i.m=e,i.c=t,i.d=function(e,t,n){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)i.d(n,r,function(t){return e[t]}.bind(null,r));return n},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=14)}([function(e,t,i){"use strict";var n=i(6),r=i.n(n),a=function(){function e(){}return e.e=function(t,i){t&&!e.FORCE_GLOBAL_TAG||(t=e.GLOBAL_TAG);var n="["+t+"] > "+i;e.ENABLE_CALLBACK&&e.emitter.emit("log","error",n),e.ENABLE_ERROR&&(console.error?console.error(n):console.warn)},e.i=function(t,i){t&&!e.FORCE_GLOBAL_TAG||(t=e.GLOBAL_TAG);var n="["+t+"] > "+i;e.ENABLE_CALLBACK&&e.emitter.emit("log","info",n),e.ENABLE_INFO&&console.info&&console.info(n)},e.w=function(t,i){t&&!e.FORCE_GLOBAL_TAG||(t=e.GLOBAL_TAG);var n="["+t+"] > "+i;e.ENABLE_CALLBACK&&e.emitter.emit("log","warn",n),e.ENABLE_WARN&&console.warn},e.d=function(t,i){t&&!e.FORCE_GLOBAL_TAG||(t=e.GLOBAL_TAG);var n="["+t+"] > "+i;e.ENABLE_CALLBACK&&e.emitter.emit("log","debug",n),e.ENABLE_DEBUG&&console.debug&&console.debug(n)},e.v=function(t,i){t&&!e.FORCE_GLOBAL_TAG||(t=e.GLOBAL_TAG);var n="["+t+"] > "+i;e.ENABLE_CALLBACK&&e.emitter.emit("log","verbose",n),e.ENABLE_VERBOSE},e}();a.GLOBAL_TAG="mpegts.js",a.FORCE_GLOBAL_TAG=!1,a.ENABLE_ERROR=!0,a.ENABLE_INFO=!0,a.ENABLE_WARN=!0,a.ENABLE_DEBUG=!0,a.ENABLE_VERBOSE=!0,a.ENABLE_CALLBACK=!1,a.emitter=new r.a,t.a=a},function(e,t,i){"use strict";t.a={IO_ERROR:"io_error",DEMUX_ERROR:"demux_error",INIT_SEGMENT:"init_segment",MEDIA_SEGMENT:"media_segment",LOADING_COMPLETE:"loading_complete",RECOVERED_EARLY_EOF:"recovered_early_eof",MEDIA_INFO:"media_info",METADATA_ARRIVED:"metadata_arrived",SCRIPTDATA_ARRIVED:"scriptdata_arrived",TIMED_ID3_METADATA_ARRIVED:"timed_id3_metadata_arrived",PES_PRIVATE_DATA_DESCRIPTOR:"pes_private_data_descriptor",PES_PRIVATE_DATA_ARRIVED:"pes_private_data_arrived",STATISTICS_INFO:"statistics_info",RECOMMEND_SEEKPOINT:"recommend_seekpoint"}},function(e,t,i){"use strict";i.d(t,"c",(function(){return r})),i.d(t,"b",(function(){return a})),i.d(t,"a",(function(){return s}));var n=i(3),r={kIdle:0,kConnecting:1,kBuffering:2,kError:3,kComplete:4},a={OK:"OK",EXCEPTION:"Exception",HTTP_STATUS_CODE_INVALID:"HttpStatusCodeInvalid",CONNECTING_TIMEOUT:"ConnectingTimeout",EARLY_EOF:"EarlyEof",UNRECOVERABLE_EARLY_EOF:"UnrecoverableEarlyEof"},s=function(){function e(e){this._type=e||"undefined",this._status=r.kIdle,this._needStash=!1,this._onContentLengthKnown=null,this._onURLRedirect=null,this._onDataArrival=null,this._onError=null,this._onComplete=null}return e.prototype.destroy=function(){this._status=r.kIdle,this._onContentLengthKnown=null,this._onURLRedirect=null,this._onDataArrival=null,this._onError=null,this._onComplete=null},e.prototype.isWorking=function(){return this._status===r.kConnecting||this._status===r.kBuffering},Object.defineProperty(e.prototype,"type",{get:function(){return this._type},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"status",{get:function(){return this._status},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"needStashBuffer",{get:function(){return this._needStash},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onContentLengthKnown",{get:function(){return this._onContentLengthKnown},set:function(e){this._onContentLengthKnown=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onURLRedirect",{get:function(){return this._onURLRedirect},set:function(e){this._onURLRedirect=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onDataArrival",{get:function(){return this._onDataArrival},set:function(e){this._onDataArrival=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onError",{get:function(){return this._onError},set:function(e){this._onError=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onComplete",{get:function(){return this._onComplete},set:function(e){this._onComplete=e},enumerable:!1,configurable:!0}),e.prototype.open=function(e,t){throw new n.c("Unimplemented abstract function!")},e.prototype.abort=function(){throw new n.c("Unimplemented abstract function!")},e}()},function(e,t,i){"use strict";i.d(t,"d",(function(){return a})),i.d(t,"a",(function(){return s})),i.d(t,"b",(function(){return o})),i.d(t,"c",(function(){return u}));var n,r=(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)t.hasOwnProperty(i)&&(e[i]=t[i])})(e,t)},function(e,t){function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)}),a=function(){function e(e){this._message=e}return Object.defineProperty(e.prototype,"name",{get:function(){return"RuntimeException"},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"message",{get:function(){return this._message},enumerable:!1,configurable:!0}),e.prototype.toString=function(){return this.name+": "+this.message},e}(),s=function(e){function t(t){return e.call(this,t)||this}return r(t,e),Object.defineProperty(t.prototype,"name",{get:function(){return"IllegalStateException"},enumerable:!1,configurable:!0}),t}(a),o=function(e){function t(t){return e.call(this,t)||this}return r(t,e),Object.defineProperty(t.prototype,"name",{get:function(){return"InvalidArgumentException"},enumerable:!1,configurable:!0}),t}(a),u=function(e){function t(t){return e.call(this,t)||this}return r(t,e),Object.defineProperty(t.prototype,"name",{get:function(){return"NotImplementedException"},enumerable:!1,configurable:!0}),t}(a)},function(e,t,i){"use strict";var n={};!function(){var e=self.navigator.userAgent.toLowerCase(),t=/(edge)\/([\w.]+)/.exec(e)||/(opr)[\/]([\w.]+)/.exec(e)||/(chrome)[ \/]([\w.]+)/.exec(e)||/(iemobile)[\/]([\w.]+)/.exec(e)||/(version)(applewebkit)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(e)||/(webkit)[ \/]([\w.]+).*(version)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(e)||/(webkit)[ \/]([\w.]+)/.exec(e)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(e)||/(msie) ([\w.]+)/.exec(e)||e.indexOf("trident")>=0&&/(rv)(?::| )([\w.]+)/.exec(e)||e.indexOf("compatible")<0&&/(firefox)[ \/]([\w.]+)/.exec(e)||[],i=/(ipad)/.exec(e)||/(ipod)/.exec(e)||/(windows phone)/.exec(e)||/(iphone)/.exec(e)||/(kindle)/.exec(e)||/(android)/.exec(e)||/(windows)/.exec(e)||/(mac)/.exec(e)||/(linux)/.exec(e)||/(cros)/.exec(e)||[],r={browser:t[5]||t[3]||t[1]||"",version:t[2]||t[4]||"0",majorVersion:t[4]||t[2]||"0",platform:i[0]||""},a={};if(r.browser){a[r.browser]=!0;var s=r.majorVersion.split(".");a.version={major:parseInt(r.majorVersion,10),string:r.version},s.length>1&&(a.version.minor=parseInt(s[1],10)),s.length>2&&(a.version.build=parseInt(s[2],10))}for(var o in r.platform&&(a[r.platform]=!0),(a.chrome||a.opr||a.safari)&&(a.webkit=!0),(a.rv||a.iemobile)&&(a.rv&&delete a.rv,r.browser="msie",a.msie=!0),a.edge&&(delete a.edge,r.browser="msedge",a.msedge=!0),a.opr&&(r.browser="opera",a.opera=!0),a.safari&&a.android&&(r.browser="android",a.android=!0),a.name=r.browser,a.platform=r.platform,n)n.hasOwnProperty(o)&&delete n[o];Object.assign(n,a)}(),t.a=n},function(e,t,i){"use strict";t.a={OK:"OK",FORMAT_ERROR:"FormatError",FORMAT_UNSUPPORTED:"FormatUnsupported",CODEC_UNSUPPORTED:"CodecUnsupported"}},function(e,t,i){"use strict";var n,r="object"==typeof Reflect?Reflect:null,a=r&&"function"==typeof r.apply?r.apply:function(e,t,i){return Function.prototype.apply.call(e,t,i)};n=r&&"function"==typeof r.ownKeys?r.ownKeys:Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:function(e){return Object.getOwnPropertyNames(e)};var s=Number.isNaN||function(e){return e!=e};function o(){o.init.call(this)}e.exports=o,e.exports.once=function(e,t){return new Promise((function(i,n){function r(i){e.removeListener(t,a),n(i)}function a(){"function"==typeof e.removeListener&&e.removeListener("error",r),i([].slice.call(arguments))}g(e,t,a,{once:!0}),"error"!==t&&function(e,t,i){"function"==typeof e.on&&g(e,"error",t,{once:!0})}(e,r)}))},o.EventEmitter=o,o.prototype._events=void 0,o.prototype._eventsCount=0,o.prototype._maxListeners=void 0;var u=10;function l(e){if("function"!=typeof e)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof e)}function h(e){return void 0===e._maxListeners?o.defaultMaxListeners:e._maxListeners}function d(e,t,i,n){var r,a,s;if(l(i),void 0===(a=e._events)?(a=e._events=Object.create(null),e._eventsCount=0):(void 0!==a.newListener&&(e.emit("newListener",t,i.listener?i.listener:i),a=e._events),s=a[t]),void 0===s)s=a[t]=i,++e._eventsCount;else if("function"==typeof s?s=a[t]=n?[i,s]:[s,i]:n?s.unshift(i):s.push(i),(r=h(e))>0&&s.length>r&&!s.warned){s.warned=!0;var o=new Error("Possible EventEmitter memory leak detected. "+s.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");o.name="MaxListenersExceededWarning",o.emitter=e,o.type=t,o.count=s.length,console&&console.warn}return e}function c(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function f(e,t,i){var n={fired:!1,wrapFn:void 0,target:e,type:t,listener:i},r=c.bind(n);return r.listener=i,n.wrapFn=r,r}function p(e,t,i){var n=e._events;if(void 0===n)return[];var r=n[t];return void 0===r?[]:"function"==typeof r?i?[r.listener||r]:[r]:i?function(e){for(var t=new Array(e.length),i=0;i0&&(s=t[0]),s instanceof Error)throw s;var o=new Error("Unhandled error."+(s?" ("+s.message+")":""));throw o.context=s,o}var u=r[e];if(void 0===u)return!1;if("function"==typeof u)a(u,this,t);else{var l=u.length,h=_(u,l);for(i=0;i=0;a--)if(i[a]===t||i[a].listener===t){s=i[a].listener,r=a;break}if(r<0)return this;0===r?i.shift():function(e,t){for(;t+1=0;n--)this.removeListener(e,t[n]);return this},o.prototype.listeners=function(e){return p(this,e,!0)},o.prototype.rawListeners=function(e){return p(this,e,!1)},o.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):m.call(e,t)},o.prototype.listenerCount=m,o.prototype.eventNames=function(){return this._eventsCount>0?n(this._events):[]}},function(e,t,i){"use strict";i.d(t,"d",(function(){return n})),i.d(t,"b",(function(){return r})),i.d(t,"a",(function(){return a})),i.d(t,"c",(function(){return s}));var n=function(e,t,i,n,r){this.dts=e,this.pts=t,this.duration=i,this.originalDts=n,this.isSyncPoint=r,this.fileposition=null},r=function(){function e(){this.beginDts=0,this.endDts=0,this.beginPts=0,this.endPts=0,this.originalBeginDts=0,this.originalEndDts=0,this.syncPoints=[],this.firstSample=null,this.lastSample=null}return e.prototype.appendSyncPoint=function(e){e.isSyncPoint=!0,this.syncPoints.push(e)},e}(),a=function(){function e(){this._list=[]}return e.prototype.clear=function(){this._list=[]},e.prototype.appendArray=function(e){var t=this._list;0!==e.length&&(t.length>0&&e[0].originalDts=t[r].dts&&et[n].lastSample.originalDts&&e=t[n].lastSample.originalDts&&(n===t.length-1||n0&&(r=this._searchNearestSegmentBefore(i.originalBeginDts)+1),this._lastAppendLocation=r,this._list.splice(r,0,i)},e.prototype.getLastSegmentBefore=function(e){var t=this._searchNearestSegmentBefore(e);return t>=0?this._list[t]:null},e.prototype.getLastSampleBefore=function(e){var t=this.getLastSegmentBefore(e);return null!=t?t.lastSample:null},e.prototype.getLastSyncPointBefore=function(e){for(var t=this._searchNearestSegmentBefore(e),i=this._list[t].syncPoints;0===i.length&&t>0;)t--,i=this._list[t].syncPoints;return i.length>0?i[i.length-1]:null},e}()},function(e,t,i){"use strict";var n=function(){function e(){this.mimeType=null,this.duration=null,this.hasAudio=null,this.hasVideo=null,this.audioCodec=null,this.videoCodec=null,this.audioDataRate=null,this.videoDataRate=null,this.audioSampleRate=null,this.audioChannelCount=null,this.width=null,this.height=null,this.fps=null,this.profile=null,this.level=null,this.refFrames=null,this.chromaFormat=null,this.sarNum=null,this.sarDen=null,this.metadata=null,this.segments=null,this.segmentCount=null,this.hasKeyframesIndex=null,this.keyframesIndex=null}return e.prototype.isComplete=function(){var e=!1===this.hasAudio||!0===this.hasAudio&&null!=this.audioCodec&&null!=this.audioSampleRate&&null!=this.audioChannelCount,t=!1===this.hasVideo||!0===this.hasVideo&&null!=this.videoCodec&&null!=this.width&&null!=this.height&&null!=this.fps&&null!=this.profile&&null!=this.level&&null!=this.refFrames&&null!=this.chromaFormat&&null!=this.sarNum&&null!=this.sarDen;return null!=this.mimeType&&e&&t},e.prototype.isSeekable=function(){return!0===this.hasKeyframesIndex},e.prototype.getNearestKeyframe=function(e){if(null==this.keyframesIndex)return null;var t=this.keyframesIndex,i=this._search(t.times,e);return{index:i,milliseconds:t.times[i],fileposition:t.filepositions[i]}},e.prototype._search=function(e,t){var i=0,n=e.length-1,r=0,a=0,s=n;for(t=e[r]&&t0){var i=e.getConfig();t.emit("change",i)}},e.registerListener=function(t){e.emitter.addListener("change",t)},e.removeListener=function(t){e.emitter.removeListener("change",t)},e.addLogListener=function(t){a.a.emitter.addListener("log",t),a.a.emitter.listenerCount("log")>0&&(a.a.ENABLE_CALLBACK=!0,e._notifyChange())},e.removeLogListener=function(t){a.a.emitter.removeListener("log",t),0===a.a.emitter.listenerCount("log")&&(a.a.ENABLE_CALLBACK=!1,e._notifyChange())},e}();s.emitter=new r.a,t.a=s},function(e,t,i){"use strict";var n=i(6),r=i.n(n),a=i(0),s=i(4),o=i(8);function u(e,t,i){var n=e;if(t+i=128){t.push(String.fromCharCode(65535&a)),n+=2;continue}}else if(i[n]<240){if(u(i,n,2)&&(a=(15&i[n])<<12|(63&i[n+1])<<6|63&i[n+2])>=2048&&55296!=(63488&a)){t.push(String.fromCharCode(65535&a)),n+=3;continue}}else if(i[n]<248){var a;if(u(i,n,3)&&(a=(7&i[n])<<18|(63&i[n+1])<<12|(63&i[n+2])<<6|63&i[n+3])>65536&&a<1114112){a-=65536,t.push(String.fromCharCode(a>>>10|55296)),t.push(String.fromCharCode(1023&a|56320)),n+=4;continue}}t.push(String.fromCharCode(65533)),++n}return t.join("")},c=i(3),f=(l=new ArrayBuffer(2),new DataView(l).setInt16(0,256,!0),256===new Int16Array(l)[0]),p=function(){function e(){}return e.parseScriptData=function(t,i,n){var r={};try{var s=e.parseValue(t,i,n),o=e.parseValue(t,i+s.size,n-s.size);r[s.data]=o.data}catch(e){a.a.e("AMF",e.toString())}return r},e.parseObject=function(t,i,n){if(n<3)throw new c.a("Data not enough when parse ScriptDataObject");var r=e.parseString(t,i,n),a=e.parseValue(t,i+r.size,n-r.size),s=a.objectEnd;return{data:{name:r.data,value:a.data},size:r.size+a.size,objectEnd:s}},e.parseVariable=function(t,i,n){return e.parseObject(t,i,n)},e.parseString=function(e,t,i){if(i<2)throw new c.a("Data not enough when parse String");var n=new DataView(e,t,i).getUint16(0,!f);return{data:n>0?d(new Uint8Array(e,t+2,n)):"",size:2+n}},e.parseLongString=function(e,t,i){if(i<4)throw new c.a("Data not enough when parse LongString");var n=new DataView(e,t,i).getUint32(0,!f);return{data:n>0?d(new Uint8Array(e,t+4,n)):"",size:4+n}},e.parseDate=function(e,t,i){if(i<10)throw new c.a("Data size invalid when parse Date");var n=new DataView(e,t,i),r=n.getFloat64(0,!f),a=n.getInt16(8,!f);return{data:new Date(r+=60*a*1e3),size:10}},e.parseValue=function(t,i,n){if(n<1)throw new c.a("Data not enough when parse Value");var r,s=new DataView(t,i,n),o=1,u=s.getUint8(0),l=!1;try{switch(u){case 0:r=s.getFloat64(1,!f),o+=8;break;case 1:r=!!s.getUint8(1),o+=1;break;case 2:var h=e.parseString(t,i+1,n-1);r=h.data,o+=h.size;break;case 3:r={};var d=0;for(9==(16777215&s.getUint32(n-4,!f))&&(d=3);o32)throw new c.b("ExpGolomb: readBits() bits exceeded max 32bits!");if(e<=this._current_word_bits_left){var t=this._current_word>>>32-e;return this._current_word<<=e,this._current_word_bits_left-=e,t}var i=this._current_word_bits_left?this._current_word:0;i>>>=32-this._current_word_bits_left;var n=e-this._current_word_bits_left;this._fillCurrentWord();var r=Math.min(n,this._current_word_bits_left),a=this._current_word>>>32-r;return this._current_word<<=r,this._current_word_bits_left-=r,i<>>e))return this._current_word<<=e,this._current_word_bits_left-=e,e;return this._fillCurrentWord(),e+this._skipLeadingZero()},e.prototype.readUEG=function(){var e=this._skipLeadingZero();return this.readBits(e+1)-1},e.prototype.readSEG=function(){var e=this.readUEG();return 1&e?e+1>>>1:-1*(e>>>1)},e}(),_=function(){function e(){}return e._ebsp2rbsp=function(e){for(var t=e,i=t.byteLength,n=new Uint8Array(i),r=0,a=0;a=2&&3===t[a]&&0===t[a-1]&&0===t[a-2]||(n[r]=t[a],r++);return new Uint8Array(n.buffer,0,r)},e.parseSPS=function(t){for(var i=t.subarray(1,4),n="avc1.",r=0;r<3;r++){var a=i[r].toString(16);a.length<2&&(a="0"+a),n+=a}var s=e._ebsp2rbsp(t),o=new m(s);o.readByte();var u=o.readByte();o.readByte();var l=o.readByte();o.readUEG();var h=e.getProfileString(u),d=e.getLevelString(l),c=1,f=420,p=8,_=8;if((100===u||110===u||122===u||244===u||44===u||83===u||86===u||118===u||128===u||138===u||144===u)&&(3===(c=o.readUEG())&&o.readBits(1),c<=3&&(f=[0,420,422,444][c]),p=o.readUEG()+8,_=o.readUEG()+8,o.readBits(1),o.readBool()))for(var g=3!==c?8:12,v=0;v0&&U<16?(I=[1,12,10,16,40,24,20,32,80,18,15,64,160,4,3,2][U-1],L=[1,11,11,11,33,11,11,11,33,11,11,33,99,3,2,1][U-1]):255===U&&(I=o.readByte()<<8|o.readByte(),L=o.readByte()<<8|o.readByte())}if(o.readBool()&&o.readBool(),o.readBool()&&(o.readBits(4),o.readBool()&&o.readBits(24)),o.readBool()&&(o.readUEG(),o.readUEG()),o.readBool()){var M=o.readBits(32),F=o.readBits(32);R=o.readBool(),x=(D=F)/(O=2*M)}}var B=1;1===I&&1===L||(B=I/L);var N=0,j=0;0===c?(N=1,j=2-w):(N=3===c?1:2,j=(1===c?2:1)*(2-w));var V=16*(T+1),H=16*(E+1)*(2-w);V-=(A+C)*N,H-=(k+P)*j;var z=Math.ceil(V*B);return o.destroy(),o=null,{codec_mimetype:n,profile_idc:u,level_idc:l,profile_string:h,level_string:d,chroma_format_idc:c,bit_depth:p,bit_depth_luma:p,bit_depth_chroma:_,ref_frames:S,chroma_format:f,chroma_format_string:e.getChromaFormatString(f),frame_rate:{fixed:R,fps:x,fps_den:O,fps_num:D},sar_ratio:{width:I,height:L},codec_size:{width:V,height:H},present_size:{width:z,height:H}}},e._skipScalingList=function(e,t){for(var i=8,n=8,r=0;r>>2!=0,a=0!=(1&t[4]),s=(n=t)[5]<<24|n[6]<<16|n[7]<<8|n[8];return s<9?i:{match:!0,consumed:s,dataOffset:s,hasAudioTrack:r,hasVideoTrack:a}},e.prototype.bindDataSource=function(e){return e.onDataArrival=this.parseChunks.bind(this),this},Object.defineProperty(e.prototype,"onTrackMetadata",{get:function(){return this._onTrackMetadata},set:function(e){this._onTrackMetadata=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onMediaInfo",{get:function(){return this._onMediaInfo},set:function(e){this._onMediaInfo=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onMetaDataArrived",{get:function(){return this._onMetaDataArrived},set:function(e){this._onMetaDataArrived=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onScriptDataArrived",{get:function(){return this._onScriptDataArrived},set:function(e){this._onScriptDataArrived=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onError",{get:function(){return this._onError},set:function(e){this._onError=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onDataAvailable",{get:function(){return this._onDataAvailable},set:function(e){this._onDataAvailable=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"timestampBase",{get:function(){return this._timestampBase},set:function(e){this._timestampBase=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"overridedDuration",{get:function(){return this._duration},set:function(e){this._durationOverrided=!0,this._duration=e,this._mediaInfo.duration=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"overridedHasAudio",{set:function(e){this._hasAudioFlagOverrided=!0,this._hasAudio=e,this._mediaInfo.hasAudio=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"overridedHasVideo",{set:function(e){this._hasVideoFlagOverrided=!0,this._hasVideo=e,this._mediaInfo.hasVideo=e},enumerable:!1,configurable:!0}),e.prototype.resetMediaInfo=function(){this._mediaInfo=new o.a},e.prototype._isInitialMetadataDispatched=function(){return this._hasAudio&&this._hasVideo?this._audioInitialMetadataDispatched&&this._videoInitialMetadataDispatched:this._hasAudio&&!this._hasVideo?this._audioInitialMetadataDispatched:!(this._hasAudio||!this._hasVideo)&&this._videoInitialMetadataDispatched},e.prototype.parseChunks=function(t,i){if(!(this._onError&&this._onMediaInfo&&this._onTrackMetadata&&this._onDataAvailable))throw new c.a("Flv: onError & onMediaInfo & onTrackMetadata & onDataAvailable callback must be specified");var n=0,r=this._littleEndian;if(0===i){if(!(t.byteLength>13))return 0;n=e.probe(t).dataOffset}for(this._firstParse&&(this._firstParse=!1,i+n!==this._dataOffset&&a.a.w(this.TAG,"First time parsing but chunk byteStart invalid!"),0!==(s=new DataView(t,n)).getUint32(0,!r)&&a.a.w(this.TAG,"PrevTagSize0 !== 0 !!!"),n+=4);nt.byteLength)break;var o=s.getUint8(0),u=16777215&s.getUint32(0,!r);if(n+11+u+4>t.byteLength)break;if(8===o||9===o||18===o){var l=s.getUint8(4),h=s.getUint8(5),d=s.getUint8(6)|h<<8|l<<16|s.getUint8(7)<<24;0!=(16777215&s.getUint32(7,!r))&&a.a.w(this.TAG,"Meet tag which has StreamID != 0!");var f=n+11;switch(o){case 8:this._parseAudioData(t,f,u,d);break;case 9:this._parseVideoData(t,f,u,d,i+n);break;case 18:this._parseScriptData(t,f,u)}var p=s.getUint32(11+u,!r);p!==11+u&&a.a.w(this.TAG,"Invalid PrevTagSize "+p),n+=11+u+4}else a.a.w(this.TAG,"Unsupported tag type "+o+", skipped"),n+=11+u+4}return this._isInitialMetadataDispatched()&&this._dispatch&&(this._audioTrack.length||this._videoTrack.length)&&this._onDataAvailable(this._audioTrack,this._videoTrack),n},e.prototype._parseScriptData=function(e,t,i){var n=p.parseScriptData(e,t,i);if(n.hasOwnProperty("onMetaData")){if(null==n.onMetaData||"object"!=typeof n.onMetaData)return void a.a.w(this.TAG,"Invalid onMetaData structure!");this._metadata&&a.a.w(this.TAG,"Found another onMetaData tag!"),this._metadata=n;var r=this._metadata.onMetaData;if(this._onMetaDataArrived&&this._onMetaDataArrived(Object.assign({},r)),"boolean"==typeof r.hasAudio&&!1===this._hasAudioFlagOverrided&&(this._hasAudio=r.hasAudio,this._mediaInfo.hasAudio=this._hasAudio),"boolean"==typeof r.hasVideo&&!1===this._hasVideoFlagOverrided&&(this._hasVideo=r.hasVideo,this._mediaInfo.hasVideo=this._hasVideo),"number"==typeof r.audiodatarate&&(this._mediaInfo.audioDataRate=r.audiodatarate),"number"==typeof r.videodatarate&&(this._mediaInfo.videoDataRate=r.videodatarate),"number"==typeof r.width&&(this._mediaInfo.width=r.width),"number"==typeof r.height&&(this._mediaInfo.height=r.height),"number"==typeof r.duration){if(!this._durationOverrided){var s=Math.floor(r.duration*this._timescale);this._duration=s,this._mediaInfo.duration=s}}else this._mediaInfo.duration=0;if("number"==typeof r.framerate){var o=Math.floor(1e3*r.framerate);if(o>0){var u=o/1e3;this._referenceFrameRate.fixed=!0,this._referenceFrameRate.fps=u,this._referenceFrameRate.fps_num=o,this._referenceFrameRate.fps_den=1e3,this._mediaInfo.fps=u}}if("object"==typeof r.keyframes){this._mediaInfo.hasKeyframesIndex=!0;var l=r.keyframes;this._mediaInfo.keyframesIndex=this._parseKeyframesIndex(l),r.keyframes=null}else this._mediaInfo.hasKeyframesIndex=!1;this._dispatch=!1,this._mediaInfo.metadata=r,a.a.v(this.TAG,"Parsed onMetaData"),this._mediaInfo.isComplete()&&this._onMediaInfo(this._mediaInfo)}Object.keys(n).length>0&&this._onScriptDataArrived&&this._onScriptDataArrived(Object.assign({},n))},e.prototype._parseKeyframesIndex=function(e){for(var t=[],i=[],n=1;n>>4;if(2===s||10===s){var o=0,u=(12&r)>>>2;if(u>=0&&u<=4){o=this._flvSoundRateTable[u];var l=1&r,h=this._audioMetadata,d=this._audioTrack;if(h||(!1===this._hasAudio&&!1===this._hasAudioFlagOverrided&&(this._hasAudio=!0,this._mediaInfo.hasAudio=!0),(h=this._audioMetadata={}).type="audio",h.id=d.id,h.timescale=this._timescale,h.duration=this._duration,h.audioSampleRate=o,h.channelCount=0===l?1:2),10===s){var c=this._parseAACAudioData(e,t+1,i-1);if(null==c)return;if(0===c.packetType){h.config&&a.a.w(this.TAG,"Found another AudioSpecificConfig!");var f=c.data;h.audioSampleRate=f.samplingRate,h.channelCount=f.channelCount,h.codec=f.codec,h.originalCodec=f.originalCodec,h.config=f.config,h.refSampleDuration=1024/h.audioSampleRate*h.timescale,a.a.v(this.TAG,"Parsed AudioSpecificConfig"),this._isInitialMetadataDispatched()?this._dispatch&&(this._audioTrack.length||this._videoTrack.length)&&this._onDataAvailable(this._audioTrack,this._videoTrack):this._audioInitialMetadataDispatched=!0,this._dispatch=!1,this._onTrackMetadata("audio",h),(_=this._mediaInfo).audioCodec=h.originalCodec,_.audioSampleRate=h.audioSampleRate,_.audioChannelCount=h.channelCount,_.hasVideo?null!=_.videoCodec&&(_.mimeType='video/x-flv; codecs="'+_.videoCodec+","+_.audioCodec+'"'):_.mimeType='video/x-flv; codecs="'+_.audioCodec+'"',_.isComplete()&&this._onMediaInfo(_)}else if(1===c.packetType){var p=this._timestampBase+n,m={unit:c.data,length:c.data.byteLength,dts:p,pts:p};d.samples.push(m),d.length+=c.data.length}else a.a.e(this.TAG,"Flv: Unsupported AAC data type "+c.packetType)}else if(2===s){if(!h.codec){var _;if(null==(f=this._parseMP3AudioData(e,t+1,i-1,!0)))return;h.audioSampleRate=f.samplingRate,h.channelCount=f.channelCount,h.codec=f.codec,h.originalCodec=f.originalCodec,h.refSampleDuration=1152/h.audioSampleRate*h.timescale,a.a.v(this.TAG,"Parsed MPEG Audio Frame Header"),this._audioInitialMetadataDispatched=!0,this._onTrackMetadata("audio",h),(_=this._mediaInfo).audioCodec=h.codec,_.audioSampleRate=h.audioSampleRate,_.audioChannelCount=h.channelCount,_.audioDataRate=f.bitRate,_.hasVideo?null!=_.videoCodec&&(_.mimeType='video/x-flv; codecs="'+_.videoCodec+","+_.audioCodec+'"'):_.mimeType='video/x-flv; codecs="'+_.audioCodec+'"',_.isComplete()&&this._onMediaInfo(_)}var v=this._parseMP3AudioData(e,t+1,i-1,!1);if(null==v)return;p=this._timestampBase+n;var y={unit:v,length:v.byteLength,dts:p,pts:p};d.samples.push(y),d.length+=v.length}}else this._onError(g.a.FORMAT_ERROR,"Flv: Invalid audio sample rate idx: "+u)}else this._onError(g.a.CODEC_UNSUPPORTED,"Flv: Unsupported audio codec idx: "+s)}},e.prototype._parseAACAudioData=function(e,t,i){if(!(i<=1)){var n={},r=new Uint8Array(e,t,i);return n.packetType=r[0],0===r[0]?n.data=this._parseAACAudioSpecificConfig(e,t+1,i-1):n.data=r.subarray(1),n}a.a.w(this.TAG,"Flv: Invalid AAC packet, missing AACPacketType or/and Data!")},e.prototype._parseAACAudioSpecificConfig=function(e,t,i){var n,r,a=new Uint8Array(e,t,i),s=null,o=0,u=null;if(o=n=a[0]>>>3,(r=(7&a[0])<<1|a[1]>>>7)<0||r>=this._mpegSamplingRates.length)this._onError(g.a.FORMAT_ERROR,"Flv: AAC invalid sampling frequency index!");else{var l=this._mpegSamplingRates[r],h=(120&a[1])>>>3;if(!(h<0||h>=8)){5===o&&(u=(7&a[1])<<1|a[2]>>>7,a[2]);var d=self.navigator.userAgent.toLowerCase();return-1!==d.indexOf("firefox")?r>=6?(o=5,s=new Array(4),u=r-3):(o=2,s=new Array(2),u=r):-1!==d.indexOf("android")?(o=2,s=new Array(2),u=r):(o=5,u=r,s=new Array(4),r>=6?u=r-3:1===h&&(o=2,s=new Array(2),u=r)),s[0]=o<<3,s[0]|=(15&r)>>>1,s[1]=(15&r)<<7,s[1]|=(15&h)<<3,5===o&&(s[1]|=(15&u)>>>1,s[2]=(1&u)<<7,s[2]|=8,s[3]=0),{config:s,samplingRate:l,channelCount:h,codec:"mp4a.40."+o,originalCodec:"mp4a.40."+n}}this._onError(g.a.FORMAT_ERROR,"Flv: AAC invalid channel configuration")}},e.prototype._parseMP3AudioData=function(e,t,i,n){if(!(i<4)){this._littleEndian;var r=new Uint8Array(e,t,i),s=null;if(n){if(255!==r[0])return;var o=r[1]>>>3&3,u=(6&r[1])>>1,l=(240&r[2])>>>4,h=(12&r[2])>>>2,d=3!=(r[3]>>>6&3)?2:1,c=0,f=0;switch(o){case 0:c=this._mpegAudioV25SampleRateTable[h];break;case 2:c=this._mpegAudioV20SampleRateTable[h];break;case 3:c=this._mpegAudioV10SampleRateTable[h]}switch(u){case 1:l>>4,u=15&s;7===u?this._parseAVCVideoPacket(e,t+1,i-1,n,r,o):this._onError(g.a.CODEC_UNSUPPORTED,"Flv: Unsupported codec in video frame: "+u)}},e.prototype._parseAVCVideoPacket=function(e,t,i,n,r,s){if(i<4)a.a.w(this.TAG,"Flv: Invalid AVC packet, missing AVCPacketType or/and CompositionTime");else{var o=this._littleEndian,u=new DataView(e,t,i),l=u.getUint8(0),h=(16777215&u.getUint32(0,!o))<<8>>8;if(0===l)this._parseAVCDecoderConfigurationRecord(e,t+4,i-4);else if(1===l)this._parseAVCVideoData(e,t+4,i-4,n,r,s,h);else if(2!==l)return void this._onError(g.a.FORMAT_ERROR,"Flv: Invalid video packet type "+l)}},e.prototype._parseAVCDecoderConfigurationRecord=function(e,t,i){if(i<7)a.a.w(this.TAG,"Flv: Invalid AVCDecoderConfigurationRecord, lack of data!");else{var n=this._videoMetadata,r=this._videoTrack,s=this._littleEndian,o=new DataView(e,t,i);n?void 0!==n.avcc&&a.a.w(this.TAG,"Found another AVCDecoderConfigurationRecord!"):(!1===this._hasVideo&&!1===this._hasVideoFlagOverrided&&(this._hasVideo=!0,this._mediaInfo.hasVideo=!0),(n=this._videoMetadata={}).type="video",n.id=r.id,n.timescale=this._timescale,n.duration=this._duration);var u=o.getUint8(0),l=o.getUint8(1);if(o.getUint8(2),o.getUint8(3),1===u&&0!==l)if(this._naluLengthSize=1+(3&o.getUint8(4)),3===this._naluLengthSize||4===this._naluLengthSize){var h=31&o.getUint8(5);if(0!==h){h>1&&a.a.w(this.TAG,"Flv: Strange AVCDecoderConfigurationRecord: SPS Count = "+h);for(var d=6,c=0;c1&&a.a.w(this.TAG,"Flv: Strange AVCDecoderConfigurationRecord: PPS Count = "+A),d++,c=0;c=i){a.a.w(this.TAG,"Malformed Nalu near timestamp "+p+", offset = "+c+", dataSize = "+i);break}var _=l.getUint32(c,!u);if(3===f&&(_>>>=8),_>i-f)return void a.a.w(this.TAG,"Malformed Nalus near timestamp "+p+", NaluSize > DataSize!");var g=31&l.getUint8(c+f);5===g&&(m=!0);var v=new Uint8Array(e,t+c,f+_),y={type:g,data:v};h.push(y),d+=v.byteLength,c+=f+_}if(h.length){var b=this._videoTrack,S={units:h,length:d,isKeyframe:m,dts:p,cts:o,pts:p+o};m&&(S.fileposition=r),b.samples.push(S),b.length+=d}},e}(),y=function(){function e(){}return e.prototype.destroy=function(){this.onError=null,this.onMediaInfo=null,this.onMetaDataArrived=null,this.onTrackMetadata=null,this.onDataAvailable=null,this.onTimedID3Metadata=null,this.onPESPrivateData=null,this.onPESPrivateDataDescriptor=null},e}(),b=function(){this.program_pmt_pid={}};!function(e){e[e.kMPEG1Audio=3]="kMPEG1Audio",e[e.kMPEG2Audio=4]="kMPEG2Audio",e[e.kPESPrivateData=6]="kPESPrivateData",e[e.kADTSAAC=15]="kADTSAAC",e[e.kID3=21]="kID3",e[e.kH264=27]="kH264",e[e.kH265=36]="kH265"}(h||(h={}));var S,T=function(){this.pid_stream_type={},this.common_pids={h264:void 0,adts_aac:void 0},this.pes_private_data_pids={},this.timed_id3_pids={}},E=function(){},w=function(){this.slices=[],this.total_length=0,this.expected_length=0,this.file_position=0};!function(e){e[e.kUnspecified=0]="kUnspecified",e[e.kSliceNonIDR=1]="kSliceNonIDR",e[e.kSliceDPA=2]="kSliceDPA",e[e.kSliceDPB=3]="kSliceDPB",e[e.kSliceDPC=4]="kSliceDPC",e[e.kSliceIDR=5]="kSliceIDR",e[e.kSliceSEI=6]="kSliceSEI",e[e.kSliceSPS=7]="kSliceSPS",e[e.kSlicePPS=8]="kSlicePPS",e[e.kSliceAUD=9]="kSliceAUD",e[e.kEndOfSequence=10]="kEndOfSequence",e[e.kEndOfStream=11]="kEndOfStream",e[e.kFiller=12]="kFiller",e[e.kSPSExt=13]="kSPSExt",e[e.kReserved0=14]="kReserved0"}(S||(S={}));var A,C,k=function(){},P=function(e){var t=e.data.byteLength;this.type=e.type,this.data=new Uint8Array(4+t),new DataView(this.data.buffer).setUint32(0,t),this.data.set(e.data,4)},I=function(){function e(e){this.TAG="H264AnnexBParser",this.current_startcode_offset_=0,this.eof_flag_=!1,this.data_=e,this.current_startcode_offset_=this.findNextStartCodeOffset(0),this.eof_flag_&&a.a.e(this.TAG,"Could not found H264 startcode until payload end!")}return e.prototype.findNextStartCodeOffset=function(e){for(var t=e,i=this.data_;;){if(t+3>=i.byteLength)return this.eof_flag_=!0,i.byteLength;var n=i[t+0]<<24|i[t+1]<<16|i[t+2]<<8|i[t+3],r=i[t+0]<<16|i[t+1]<<8|i[t+2];if(1===n||1===r)return t;t++}},e.prototype.readNextNaluPayload=function(){for(var e=this.data_,t=null;null==t&&!this.eof_flag_;){var i=this.current_startcode_offset_,n=31&e[i+=1==(e[i]<<24|e[i+1]<<16|e[i+2]<<8|e[i+3])?4:3],r=(128&e[i])>>>7,a=this.findNextStartCodeOffset(i);if(this.current_startcode_offset_=a,!(n>=S.kReserved0)&&0===r){var s=e.subarray(i,a);(t=new k).type=n,t.data=s}}return t},e}(),L=function(){function e(e,t,i){var n=8+e.byteLength+1+2+t.byteLength,r=!1;66!==e[3]&&77!==e[3]&&88!==e[3]&&(r=!0,n+=4);var a=this.data=new Uint8Array(n);a[0]=1,a[1]=e[1],a[2]=e[2],a[3]=e[3],a[4]=255,a[5]=225;var s=e.byteLength;a[6]=s>>>8,a[7]=255&s;var o=8;a.set(e,8),a[o+=s]=1;var u=t.byteLength;a[o+1]=u>>>8,a[o+2]=255&u,a.set(t,o+3),o+=3+u,r&&(a[o]=252|i.chroma_format_idc,a[o+1]=248|i.bit_depth_luma-8,a[o+2]=248|i.bit_depth_chroma-8,a[o+3]=0,o+=4)}return e.prototype.getData=function(){return this.data},e}();!function(e){e[e.kNull=0]="kNull",e[e.kAACMain=1]="kAACMain",e[e.kAAC_LC=2]="kAAC_LC",e[e.kAAC_SSR=3]="kAAC_SSR",e[e.kAAC_LTP=4]="kAAC_LTP",e[e.kAAC_SBR=5]="kAAC_SBR",e[e.kAAC_Scalable=6]="kAAC_Scalable",e[e.kLayer1=32]="kLayer1",e[e.kLayer2=33]="kLayer2",e[e.kLayer3=34]="kLayer3"}(A||(A={})),function(e){e[e.k96000Hz=0]="k96000Hz",e[e.k88200Hz=1]="k88200Hz",e[e.k64000Hz=2]="k64000Hz",e[e.k48000Hz=3]="k48000Hz",e[e.k44100Hz=4]="k44100Hz",e[e.k32000Hz=5]="k32000Hz",e[e.k24000Hz=6]="k24000Hz",e[e.k22050Hz=7]="k22050Hz",e[e.k16000Hz=8]="k16000Hz",e[e.k12000Hz=9]="k12000Hz",e[e.k11025Hz=10]="k11025Hz",e[e.k8000Hz=11]="k8000Hz",e[e.k7350Hz=12]="k7350Hz"}(C||(C={}));var x,R=[96e3,88200,64e3,48e3,44100,32e3,24e3,22050,16e3,12e3,11025,8e3,7350],D=function(){},O=function(){function e(e){this.TAG="AACADTSParser",this.data_=e,this.current_syncword_offset_=this.findNextSyncwordOffset(0),this.eof_flag_&&a.a.e(this.TAG,"Could not found ADTS syncword until payload end")}return e.prototype.findNextSyncwordOffset=function(e){for(var t=e,i=this.data_;;){if(t+7>=i.byteLength)return this.eof_flag_=!0,i.byteLength;if(4095==(i[t+0]<<8|i[t+1])>>>4)return t;t++}},e.prototype.readNextAACFrame=function(){for(var e=this.data_,t=null;null==t&&!this.eof_flag_;){var i=this.current_syncword_offset_,n=(8&e[i+1])>>>3,r=(6&e[i+1])>>>1,a=1&e[i+1],s=(192&e[i+2])>>>6,o=(60&e[i+2])>>>2,u=(1&e[i+2])<<2|(192&e[i+3])>>>6,l=(3&e[i+3])<<11|e[i+4]<<3|(224&e[i+5])>>>5;if(e[i+6],i+l>this.data_.byteLength){this.eof_flag_=!0,this.has_last_incomplete_data=!0;break}var h=1===a?7:9,d=l-h;i+=h;var c=this.findNextSyncwordOffset(i+d);if(this.current_syncword_offset_=c,(0===n||1===n)&&0===r){var f=e.subarray(i,i+d);(t=new D).audio_object_type=s+1,t.sampling_freq_index=o,t.sampling_frequency=R[o],t.channel_config=u,t.data=f}}return t},e.prototype.hasIncompleteData=function(){return this.has_last_incomplete_data},e.prototype.getIncompleteData=function(){return this.has_last_incomplete_data?this.data_.subarray(this.current_syncword_offset_):null},e}(),U=function(e){var t=null,i=e.audio_object_type,n=e.audio_object_type,r=e.sampling_freq_index,a=e.channel_config,s=0,o=navigator.userAgent.toLowerCase();-1!==o.indexOf("firefox")?r>=6?(n=5,t=new Array(4),s=r-3):(n=2,t=new Array(2),s=r):-1!==o.indexOf("android")?(n=2,t=new Array(2),s=r):(n=5,s=r,t=new Array(4),r>=6?s=r-3:1===a&&(n=2,t=new Array(2),s=r)),t[0]=n<<3,t[0]|=(15&r)>>>1,t[1]=(15&r)<<7,t[1]|=(15&a)<<3,5===n&&(t[1]|=(15&s)>>>1,t[2]=(1&s)<<7,t[2]|=8,t[3]=0),this.config=t,this.sampling_rate=R[r],this.channel_count=a,this.codec_mimetype="mp4a.40."+n,this.original_codec_mimetype="mp4a.40."+i},M=function(){},F=function(){},B=(x=function(e,t){return(x=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)t.hasOwnProperty(i)&&(e[i]=t[i])})(e,t)},function(e,t){function i(){this.constructor=e}x(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)}),N=function(e){function t(t,i){var n=e.call(this)||this;return n.TAG="TSDemuxer",n.first_parse_=!0,n.media_info_=new o.a,n.timescale_=90,n.duration_=0,n.current_pmt_pid_=-1,n.program_pmt_map_={},n.pes_slice_queues_={},n.video_metadata_={sps:void 0,pps:void 0,sps_details:void 0},n.audio_metadata_={audio_object_type:void 0,sampling_freq_index:void 0,sampling_frequency:void 0,channel_config:void 0},n.aac_last_sample_pts_=void 0,n.aac_last_incomplete_data_=null,n.has_video_=!1,n.has_audio_=!1,n.video_init_segment_dispatched_=!1,n.audio_init_segment_dispatched_=!1,n.video_metadata_changed_=!1,n.audio_metadata_changed_=!1,n.video_track_={type:"video",id:1,sequenceNumber:0,samples:[],length:0},n.audio_track_={type:"audio",id:2,sequenceNumber:0,samples:[],length:0},n.ts_packet_size_=t.ts_packet_size,n.sync_offset_=t.sync_offset,n.config_=i,n}return B(t,e),t.prototype.destroy=function(){this.media_info_=null,this.pes_slice_queues_=null,this.video_metadata_=null,this.audio_metadata_=null,this.aac_last_incomplete_data_=null,this.video_track_=null,this.audio_track_=null,e.prototype.destroy.call(this)},t.probe=function(e){var t=new Uint8Array(e),i=-1,n=188;if(t.byteLength<=3*n)return a.a.e("TSDemuxer","Probe data "+t.byteLength+" bytes is too few for judging MPEG-TS stream format!"),{match:!1};for(;-1===i;){for(var r=Math.min(1e3,t.byteLength-3*n),s=0;s=4?(a.a.v("TSDemuxer","ts_packet_size = 192, m2ts mode"),i-=4):204===n&&a.a.v("TSDemuxer","ts_packet_size = 204, RS encoded MPEG2-TS stream"),{match:!0,consumed:0,ts_packet_size:n,sync_offset:i})},t.prototype.bindDataSource=function(e){return e.onDataArrival=this.parseChunks.bind(this),this},t.prototype.resetMediaInfo=function(){this.media_info_=new o.a},t.prototype.parseChunks=function(e,t){if(!(this.onError&&this.onMediaInfo&&this.onTrackMetadata&&this.onDataAvailable))throw new c.a("onError & onMediaInfo & onTrackMetadata & onDataAvailable callback must be specified");var i=0;for(this.first_parse_&&(this.first_parse_=!1,i=this.sync_offset_);i+this.ts_packet_size_<=e.byteLength;){var n=t+i;192===this.ts_packet_size_&&(i+=4);var r=new Uint8Array(e,i,188),s=r[0];if(71!==s){a.a.e(this.TAG,"sync_byte = "+s+", not 0x47");break}var o=(64&r[1])>>>6,u=(r[1],(31&r[1])<<8|r[2]),l=(48&r[3])>>>4,h=15&r[3],d={},f=4;if(2==l||3==l){var p=r[4];if(5+p===188){i+=188,204===this.ts_packet_size_&&(i+=16);continue}p>0&&(d=this.parseAdaptationField(e,i+4,1+p)),f=5+p}if(1==l||3==l)if(0===u||u===this.current_pmt_pid_){o&&(f+=1+r[f]);var m=188-f;0===u?this.parsePAT(e,i+f,m,{payload_unit_start_indicator:o,continuity_conunter:h}):this.parsePMT(e,i+f,m,{payload_unit_start_indicator:o,continuity_conunter:h})}else if(null!=this.pmt_&&null!=this.pmt_.pid_stream_type[u]){m=188-f;var _=this.pmt_.pid_stream_type[u];u!==this.pmt_.common_pids.h264&&u!==this.pmt_.common_pids.adts_aac&&!0!==this.pmt_.pes_private_data_pids[u]&&!0!==this.pmt_.timed_id3_pids[u]||this.handlePESSlice(e,i+f,m,{pid:u,stream_type:_,file_position:n,payload_unit_start_indicator:o,continuity_conunter:h,random_access_indicator:d.random_access_indicator})}i+=188,204===this.ts_packet_size_&&(i+=16)}return this.dispatchAudioVideoMediaSegment(),i},t.prototype.parseAdaptationField=function(e,t,i){var n=new Uint8Array(e,t,i),r=n[0];return r>0?r>183?(a.a.w(this.TAG,"Illegal adaptation_field_length: "+r),{}):{discontinuity_indicator:(128&n[1])>>>7,random_access_indicator:(64&n[1])>>>6,elementary_stream_priority_indicator:(32&n[1])>>>5}:{}},t.prototype.parsePAT=function(e,t,i,n){var r=new Uint8Array(e,t,i),s=r[0];if(0===s){var o=(15&r[1])<<8|r[2],u=(r[3],r[4],(62&r[5])>>>1),l=1&r[5],h=r[6],d=(r[7],null);if(1===l&&0===h)(d=new b).version_number=u;else if(null==(d=this.pat_))return;for(var c=o-5-4,f=-1,p=-1,m=8;m<8+c;m+=4){var _=r[m]<<8|r[m+1],g=(31&r[m+2])<<8|r[m+3];0===_?d.network_pid=g:(d.program_pmt_pid[_]=g,-1===f&&(f=_),-1===p&&(p=g))}1===l&&0===h&&(null==this.pat_&&a.a.v(this.TAG,"Parsed first PAT: "+JSON.stringify(d)),this.pat_=d,this.current_program_=f,this.current_pmt_pid_=p)}else a.a.e(this.TAG,"parsePAT: table_id "+s+" is not corresponded to PAT!")},t.prototype.parsePMT=function(e,t,i,n){var r=new Uint8Array(e,t,i),s=r[0];if(2===s){var o=(15&r[1])<<8|r[2],u=r[3]<<8|r[4],l=(62&r[5])>>>1,d=1&r[5],c=r[6],f=(r[7],null);if(1===d&&0===c)(f=new T).program_number=u,f.version_number=l,this.program_pmt_map_[u]=f;else if(null==(f=this.program_pmt_map_[u]))return;r[8],r[9];for(var p=(15&r[10])<<8|r[11],m=12+p,_=o-9-p-4,g=m;g0){var S=r.subarray(g+5,g+5+b);this.dispatchPESPrivateDataDescriptor(y,v,S)}}else v===h.kID3&&(f.timed_id3_pids[y]=!0);else f.common_pids.adts_aac=y;else f.common_pids.h264=y;g+=5+b}u===this.current_program_&&(null==this.pmt_&&a.a.v(this.TAG,"Parsed first PMT: "+JSON.stringify(f)),this.pmt_=f,f.common_pids.h264&&(this.has_video_=!0),f.common_pids.adts_aac&&(this.has_audio_=!0))}else a.a.e(this.TAG,"parsePMT: table_id "+s+" is not corresponded to PMT!")},t.prototype.handlePESSlice=function(e,t,i,n){var r=new Uint8Array(e,t,i),s=r[0]<<16|r[1]<<8|r[2],o=(r[3],r[4]<<8|r[5]);if(n.payload_unit_start_indicator){if(1!==s)return void a.a.e(this.TAG,"handlePESSlice: packet_start_code_prefix should be 1 but with value "+s);var u=this.pes_slice_queues_[n.pid];u&&(0===u.expected_length||u.expected_length===u.total_length?this.emitPESSlices(u,n):this.cleanPESSlices(u,n)),this.pes_slice_queues_[n.pid]=new w,this.pes_slice_queues_[n.pid].file_position=n.file_position,this.pes_slice_queues_[n.pid].random_access_indicator=n.random_access_indicator}if(null!=this.pes_slice_queues_[n.pid]){var l=this.pes_slice_queues_[n.pid];l.slices.push(r),n.payload_unit_start_indicator&&(l.expected_length=0===o?0:o+6),l.total_length+=r.byteLength,l.expected_length>0&&l.expected_length===l.total_length?this.emitPESSlices(l,n):l.expected_length>0&&l.expected_length>>6,o=t[8],u=void 0,l=void 0;2!==s&&3!==s||(u=536870912*(14&t[9])+4194304*(255&t[10])+16384*(254&t[11])+128*(255&t[12])+(254&t[13])/2,l=3===s?536870912*(14&t[14])+4194304*(255&t[15])+16384*(254&t[16])+128*(255&t[17])+(254&t[18])/2:u);var d=9+o,c=void 0;if(0!==r){if(r<3+o)return void a.a.v(this.TAG,"Malformed PES: PES_packet_length < 3 + PES_header_data_length");c=r-3-o}else c=t.byteLength-d;var f=t.subarray(d,d+c);switch(e.stream_type){case h.kMPEG1Audio:case h.kMPEG2Audio:break;case h.kPESPrivateData:this.parsePESPrivateDataPayload(f,u,l,e.pid,n);break;case h.kADTSAAC:this.parseAACPayload(f,u);break;case h.kID3:this.parseTimedID3MetadataPayload(f,u,l,e.pid,n);break;case h.kH264:this.parseH264Payload(f,u,l,e.file_position,e.random_access_indicator);break;case h.kH265:}}else 188!==n&&191!==n&&240!==n&&241!==n&&255!==n&&242!==n&&248!==n||e.stream_type!==h.kPESPrivateData||(d=6,c=void 0,c=0!==r?r:t.byteLength-d,f=t.subarray(d,d+c),this.parsePESPrivateDataPayload(f,void 0,void 0,e.pid,n));else a.a.e(this.TAG,"parsePES: packet_start_code_prefix should be 1 but with value "+i)},t.prototype.parseH264Payload=function(e,t,i,n,r){for(var s=new I(e),o=null,u=[],l=0,h=!1;null!=(o=s.readNextNaluPayload());){var d=new P(o);if(d.type===S.kSliceSPS){var c=_.parseSPS(o.data);this.video_init_segment_dispatched_?!0===this.detectVideoMetadataChange(d,c)&&(a.a.v(this.TAG,"H264: Critical h264 metadata has been changed, attempt to re-generate InitSegment"),this.video_metadata_changed_=!0,this.video_metadata_={sps:d,pps:void 0,sps_details:c}):(this.video_metadata_.sps=d,this.video_metadata_.sps_details=c)}else d.type===S.kSlicePPS?this.video_init_segment_dispatched_&&!this.video_metadata_changed_||(this.video_metadata_.pps=d,this.video_metadata_.sps&&this.video_metadata_.pps&&(this.video_metadata_changed_&&this.dispatchVideoMediaSegment(),this.dispatchVideoInitSegment())):(d.type===S.kSliceIDR||d.type===S.kSliceNonIDR&&1===r)&&(h=!0);this.video_init_segment_dispatched_&&(u.push(d),l+=d.data.byteLength)}var f=Math.floor(t/this.timescale_),p=Math.floor(i/this.timescale_);if(u.length){var m=this.video_track_,g={units:u,length:l,isKeyframe:h,dts:p,pts:f,cts:f-p,file_position:n};m.samples.push(g),m.length+=l}},t.prototype.detectVideoMetadataChange=function(e,t){if(t.codec_mimetype!==this.video_metadata_.sps_details.codec_mimetype)return a.a.v(this.TAG,"H264: Codec mimeType changed from "+this.video_metadata_.sps_details.codec_mimetype+" to "+t.codec_mimetype),!0;if(t.codec_size.width!==this.video_metadata_.sps_details.codec_size.width||t.codec_size.height!==this.video_metadata_.sps_details.codec_size.height){var i=this.video_metadata_.sps_details.codec_size,n=t.codec_size;return a.a.v(this.TAG,"H264: Coded Resolution changed from "+i.width+"x"+i.height+" to "+n.width+"x"+n.height),!0}return t.present_size.width!==this.video_metadata_.sps_details.present_size.width&&(a.a.v(this.TAG,"H264: Present resolution width changed from "+this.video_metadata_.sps_details.present_size.width+" to "+t.present_size.width),!0)},t.prototype.isInitSegmentDispatched=function(){return this.has_video_&&this.has_audio_?this.video_init_segment_dispatched_&&this.audio_init_segment_dispatched_:this.has_video_&&!this.has_audio_?this.video_init_segment_dispatched_:!(this.has_video_||!this.has_audio_)&&this.audio_init_segment_dispatched_},t.prototype.dispatchVideoInitSegment=function(){var e=this.video_metadata_.sps_details,t={type:"video"};t.id=this.video_track_.id,t.timescale=1e3,t.duration=this.duration_,t.codecWidth=e.codec_size.width,t.codecHeight=e.codec_size.height,t.presentWidth=e.present_size.width,t.presentHeight=e.present_size.height,t.profile=e.profile_string,t.level=e.level_string,t.bitDepth=e.bit_depth,t.chromaFormat=e.chroma_format,t.sarRatio=e.sar_ratio,t.frameRate=e.frame_rate;var i=t.frameRate.fps_den,n=t.frameRate.fps_num;t.refSampleDuration=i/n*1e3,t.codec=e.codec_mimetype;var r=this.video_metadata_.sps.data.subarray(4),s=this.video_metadata_.pps.data.subarray(4),o=new L(r,s,e);t.avcc=o.getData(),0==this.video_init_segment_dispatched_&&a.a.v(this.TAG,"Generated first AVCDecoderConfigurationRecord for mimeType: "+t.codec),this.onTrackMetadata("video",t),this.video_init_segment_dispatched_=!0,this.video_metadata_changed_=!1;var u=this.media_info_;u.hasVideo=!0,u.width=t.codecWidth,u.height=t.codecHeight,u.fps=t.frameRate.fps,u.profile=t.profile,u.level=t.level,u.refFrames=e.ref_frames,u.chromaFormat=e.chroma_format_string,u.sarNum=t.sarRatio.width,u.sarDen=t.sarRatio.height,u.videoCodec=t.codec,u.hasAudio&&u.audioCodec?u.mimeType='video/mp2t; codecs="'+u.videoCodec+","+u.audioCodec+'"':u.mimeType='video/mp2t; codecs="'+u.videoCodec+'"',u.isComplete()&&this.onMediaInfo(u)},t.prototype.dispatchVideoMediaSegment=function(){this.isInitSegmentDispatched()&&this.video_track_.length&&this.onDataAvailable(null,this.video_track_)},t.prototype.dispatchAudioMediaSegment=function(){this.isInitSegmentDispatched()&&this.audio_track_.length&&this.onDataAvailable(this.audio_track_,null)},t.prototype.dispatchAudioVideoMediaSegment=function(){this.isInitSegmentDispatched()&&(this.audio_track_.length||this.video_track_.length)&&this.onDataAvailable(this.audio_track_,this.video_track_)},t.prototype.parseAACPayload=function(e,t){if(!this.has_video_||this.video_init_segment_dispatched_){if(this.aac_last_incomplete_data_){var i=new Uint8Array(e.byteLength+this.aac_last_incomplete_data_.byteLength);i.set(this.aac_last_incomplete_data_,0),i.set(e,this.aac_last_incomplete_data_.byteLength),e=i}var n,r;if(null!=t)r=t/this.timescale_;else{if(null==this.aac_last_sample_pts_)return void a.a.w(this.TAG,"AAC: Unknown pts");n=1024/this.audio_metadata_.sampling_frequency*1e3,r=this.aac_last_sample_pts_+n}if(this.aac_last_incomplete_data_&&this.aac_last_sample_pts_){n=1024/this.audio_metadata_.sampling_frequency*1e3;var s=this.aac_last_sample_pts_+n;Math.abs(s-r)>1&&(a.a.w(this.TAG,"AAC: Detected pts overlapped, expected: "+s+"ms, PES pts: "+r+"ms"),r=s)}for(var o,u=new O(e),l=null,h=r;null!=(l=u.readNextAACFrame());){n=1024/l.sampling_frequency*1e3,0==this.audio_init_segment_dispatched_?(this.audio_metadata_.audio_object_type=l.audio_object_type,this.audio_metadata_.sampling_freq_index=l.sampling_freq_index,this.audio_metadata_.sampling_frequency=l.sampling_frequency,this.audio_metadata_.channel_config=l.channel_config,this.dispatchAudioInitSegment(l)):this.detectAudioMetadataChange(l)&&(this.dispatchAudioMediaSegment(),this.dispatchAudioInitSegment(l)),o=h;var d=Math.floor(h),c={unit:l.data,length:l.data.byteLength,pts:d,dts:d};this.audio_track_.samples.push(c),this.audio_track_.length+=l.data.byteLength,h+=n}u.hasIncompleteData()&&(this.aac_last_incomplete_data_=u.getIncompleteData()),o&&(this.aac_last_sample_pts_=o)}},t.prototype.detectAudioMetadataChange=function(e){return e.audio_object_type!==this.audio_metadata_.audio_object_type?(a.a.v(this.TAG,"AAC: AudioObjectType changed from "+this.audio_metadata_.audio_object_type+" to "+e.audio_object_type),!0):e.sampling_freq_index!==this.audio_metadata_.sampling_freq_index?(a.a.v(this.TAG,"AAC: SamplingFrequencyIndex changed from "+this.audio_metadata_.sampling_freq_index+" to "+e.sampling_freq_index),!0):e.channel_config!==this.audio_metadata_.channel_config&&(a.a.v(this.TAG,"AAC: Channel configuration changed from "+this.audio_metadata_.channel_config+" to "+e.channel_config),!0)},t.prototype.dispatchAudioInitSegment=function(e){var t=new U(e),i={type:"audio"};i.id=this.audio_track_.id,i.timescale=1e3,i.duration=this.duration_,i.audioSampleRate=t.sampling_rate,i.channelCount=t.channel_count,i.codec=t.codec_mimetype,i.originalCodec=t.original_codec_mimetype,i.config=t.config,i.refSampleDuration=1024/i.audioSampleRate*i.timescale,0==this.audio_init_segment_dispatched_&&a.a.v(this.TAG,"Generated first AudioSpecificConfig for mimeType: "+i.codec),this.onTrackMetadata("audio",i),this.audio_init_segment_dispatched_=!0,this.video_metadata_changed_=!1;var n=this.media_info_;n.hasAudio=!0,n.audioCodec=i.originalCodec,n.audioSampleRate=i.audioSampleRate,n.audioChannelCount=i.channelCount,n.hasVideo&&n.videoCodec?n.mimeType='video/mp2t; codecs="'+n.videoCodec+","+n.audioCodec+'"':n.mimeType='video/mp2t; codecs="'+n.audioCodec+'"',n.isComplete()&&this.onMediaInfo(n)},t.prototype.dispatchPESPrivateDataDescriptor=function(e,t,i){var n=new F;n.pid=e,n.stream_type=t,n.descriptor=i,this.onPESPrivateDataDescriptor&&this.onPESPrivateDataDescriptor(n)},t.prototype.parsePESPrivateDataPayload=function(e,t,i,n,r){var a=new M;if(a.pid=n,a.stream_id=r,a.len=e.byteLength,a.data=e,null!=t){var s=Math.floor(t/this.timescale_);a.pts=s}else a.nearest_pts=this.aac_last_sample_pts_;if(null!=i){var o=Math.floor(i/this.timescale_);a.dts=o}this.onPESPrivateData&&this.onPESPrivateData(a)},t.prototype.parseTimedID3MetadataPayload=function(e,t,i,n,r){var a=new M;if(a.pid=n,a.stream_id=r,a.len=e.byteLength,a.data=e,null!=t){var s=Math.floor(t/this.timescale_);a.pts=s}if(null!=i){var o=Math.floor(i/this.timescale_);a.dts=o}this.onTimedID3Metadata&&this.onTimedID3Metadata(a)},t}(y),j=function(){function e(){}return e.init=function(){for(var t in e.types={avc1:[],avcC:[],btrt:[],dinf:[],dref:[],esds:[],ftyp:[],hdlr:[],mdat:[],mdhd:[],mdia:[],mfhd:[],minf:[],moof:[],moov:[],mp4a:[],mvex:[],mvhd:[],sdtp:[],stbl:[],stco:[],stsc:[],stsd:[],stsz:[],stts:[],tfdt:[],tfhd:[],traf:[],trak:[],trun:[],trex:[],tkhd:[],vmhd:[],smhd:[],".mp3":[]},e.types)e.types.hasOwnProperty(t)&&(e.types[t]=[t.charCodeAt(0),t.charCodeAt(1),t.charCodeAt(2),t.charCodeAt(3)]);var i=e.constants={};i.FTYP=new Uint8Array([105,115,111,109,0,0,0,1,105,115,111,109,97,118,99,49]),i.STSD_PREFIX=new Uint8Array([0,0,0,0,0,0,0,1]),i.STTS=new Uint8Array([0,0,0,0,0,0,0,0]),i.STSC=i.STCO=i.STTS,i.STSZ=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0]),i.HDLR_VIDEO=new Uint8Array([0,0,0,0,0,0,0,0,118,105,100,101,0,0,0,0,0,0,0,0,0,0,0,0,86,105,100,101,111,72,97,110,100,108,101,114,0]),i.HDLR_AUDIO=new Uint8Array([0,0,0,0,0,0,0,0,115,111,117,110,0,0,0,0,0,0,0,0,0,0,0,0,83,111,117,110,100,72,97,110,100,108,101,114,0]),i.DREF=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,12,117,114,108,32,0,0,0,1]),i.SMHD=new Uint8Array([0,0,0,0,0,0,0,0]),i.VMHD=new Uint8Array([0,0,0,1,0,0,0,0,0,0,0,0])},e.box=function(e){for(var t=8,i=null,n=Array.prototype.slice.call(arguments,1),r=n.length,a=0;a>>24&255,i[1]=t>>>16&255,i[2]=t>>>8&255,i[3]=255&t,i.set(e,4);var s=8;for(a=0;a>>24&255,t>>>16&255,t>>>8&255,255&t,i>>>24&255,i>>>16&255,i>>>8&255,255&i,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255]))},e.trak=function(t){return e.box(e.types.trak,e.tkhd(t),e.mdia(t))},e.tkhd=function(t){var i=t.id,n=t.duration,r=t.presentWidth,a=t.presentHeight;return e.box(e.types.tkhd,new Uint8Array([0,0,0,7,0,0,0,0,0,0,0,0,i>>>24&255,i>>>16&255,i>>>8&255,255&i,0,0,0,0,n>>>24&255,n>>>16&255,n>>>8&255,255&n,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,r>>>8&255,255&r,0,0,a>>>8&255,255&a,0,0]))},e.mdia=function(t){return e.box(e.types.mdia,e.mdhd(t),e.hdlr(t),e.minf(t))},e.mdhd=function(t){var i=t.timescale,n=t.duration;return e.box(e.types.mdhd,new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,i>>>24&255,i>>>16&255,i>>>8&255,255&i,n>>>24&255,n>>>16&255,n>>>8&255,255&n,85,196,0,0]))},e.hdlr=function(t){var i;return i="audio"===t.type?e.constants.HDLR_AUDIO:e.constants.HDLR_VIDEO,e.box(e.types.hdlr,i)},e.minf=function(t){var i;return i="audio"===t.type?e.box(e.types.smhd,e.constants.SMHD):e.box(e.types.vmhd,e.constants.VMHD),e.box(e.types.minf,i,e.dinf(),e.stbl(t))},e.dinf=function(){return e.box(e.types.dinf,e.box(e.types.dref,e.constants.DREF))},e.stbl=function(t){return e.box(e.types.stbl,e.stsd(t),e.box(e.types.stts,e.constants.STTS),e.box(e.types.stsc,e.constants.STSC),e.box(e.types.stsz,e.constants.STSZ),e.box(e.types.stco,e.constants.STCO))},e.stsd=function(t){return"audio"===t.type?"mp3"===t.codec?e.box(e.types.stsd,e.constants.STSD_PREFIX,e.mp3(t)):e.box(e.types.stsd,e.constants.STSD_PREFIX,e.mp4a(t)):e.box(e.types.stsd,e.constants.STSD_PREFIX,e.avc1(t))},e.mp3=function(t){var i=t.channelCount,n=t.audioSampleRate,r=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,i,0,16,0,0,0,0,n>>>8&255,255&n,0,0]);return e.box(e.types[".mp3"],r)},e.mp4a=function(t){var i=t.channelCount,n=t.audioSampleRate,r=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,i,0,16,0,0,0,0,n>>>8&255,255&n,0,0]);return e.box(e.types.mp4a,r,e.esds(t))},e.esds=function(t){var i=t.config||[],n=i.length,r=new Uint8Array([0,0,0,0,3,23+n,0,1,0,4,15+n,64,21,0,0,0,0,0,0,0,0,0,0,0,5].concat([n]).concat(i).concat([6,1,2]));return e.box(e.types.esds,r)},e.avc1=function(t){var i=t.avcc,n=t.codecWidth,r=t.codecHeight,a=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,n>>>8&255,255&n,r>>>8&255,255&r,0,72,0,0,0,72,0,0,0,0,0,0,0,1,10,120,113,113,47,102,108,118,46,106,115,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,255,255]);return e.box(e.types.avc1,a,e.box(e.types.avcC,i))},e.mvex=function(t){return e.box(e.types.mvex,e.trex(t))},e.trex=function(t){var i=t.id,n=new Uint8Array([0,0,0,0,i>>>24&255,i>>>16&255,i>>>8&255,255&i,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1]);return e.box(e.types.trex,n)},e.moof=function(t,i){return e.box(e.types.moof,e.mfhd(t.sequenceNumber),e.traf(t,i))},e.mfhd=function(t){var i=new Uint8Array([0,0,0,0,t>>>24&255,t>>>16&255,t>>>8&255,255&t]);return e.box(e.types.mfhd,i)},e.traf=function(t,i){var n=t.id,r=e.box(e.types.tfhd,new Uint8Array([0,0,0,0,n>>>24&255,n>>>16&255,n>>>8&255,255&n])),a=e.box(e.types.tfdt,new Uint8Array([0,0,0,0,i>>>24&255,i>>>16&255,i>>>8&255,255&i])),s=e.sdtp(t),o=e.trun(t,s.byteLength+16+16+8+16+8+8);return e.box(e.types.traf,r,a,o,s)},e.sdtp=function(t){for(var i=t.samples||[],n=i.length,r=new Uint8Array(4+n),a=0;a>>24&255,r>>>16&255,r>>>8&255,255&r,i>>>24&255,i>>>16&255,i>>>8&255,255&i],0);for(var o=0;o>>24&255,u>>>16&255,u>>>8&255,255&u,l>>>24&255,l>>>16&255,l>>>8&255,255&l,h.isLeading<<2|h.dependsOn,h.isDependedOn<<6|h.hasRedundancy<<4|h.isNonSync,0,0,d>>>24&255,d>>>16&255,d>>>8&255,255&d],12+16*o)}return e.box(e.types.trun,s)},e.mdat=function(t){return e.box(e.types.mdat,t)},e}();j.init();var V=j,H=function(){function e(){}return e.getSilentFrame=function(e,t){if("mp4a.40.2"===e){if(1===t)return new Uint8Array([0,200,0,128,35,128]);if(2===t)return new Uint8Array([33,0,73,144,2,25,0,35,128]);if(3===t)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,142]);if(4===t)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,128,44,128,8,2,56]);if(5===t)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,130,48,4,153,0,33,144,2,56]);if(6===t)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,130,48,4,153,0,33,144,2,0,178,0,32,8,224])}else{if(1===t)return new Uint8Array([1,64,34,128,163,78,230,128,186,8,0,0,0,28,6,241,193,10,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,94]);if(2===t)return new Uint8Array([1,64,34,128,163,94,230,128,186,8,0,0,0,0,149,0,6,241,161,10,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,94]);if(3===t)return new Uint8Array([1,64,34,128,163,94,230,128,186,8,0,0,0,0,149,0,6,241,161,10,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,94])}return null},e}(),z=i(7),G=function(){function e(e){this.TAG="MP4Remuxer",this._config=e,this._isLive=!0===e.isLive,this._dtsBase=-1,this._dtsBaseInited=!1,this._audioDtsBase=1/0,this._videoDtsBase=1/0,this._audioNextDts=void 0,this._videoNextDts=void 0,this._audioStashedLastSample=null,this._videoStashedLastSample=null,this._audioMeta=null,this._videoMeta=null,this._audioSegmentInfoList=new z.c("audio"),this._videoSegmentInfoList=new z.c("video"),this._onInitSegment=null,this._onMediaSegment=null,this._forceFirstIDR=!(!s.a.chrome||!(s.a.version.major<50||50===s.a.version.major&&s.a.version.build<2661)),this._fillSilentAfterSeek=s.a.msedge||s.a.msie,this._mp3UseMpegAudio=!s.a.firefox,this._fillAudioTimestampGap=this._config.fixAudioTimestampGap}return e.prototype.destroy=function(){this._dtsBase=-1,this._dtsBaseInited=!1,this._audioMeta=null,this._videoMeta=null,this._audioSegmentInfoList.clear(),this._audioSegmentInfoList=null,this._videoSegmentInfoList.clear(),this._videoSegmentInfoList=null,this._onInitSegment=null,this._onMediaSegment=null},e.prototype.bindDataSource=function(e){return e.onDataAvailable=this.remux.bind(this),e.onTrackMetadata=this._onTrackMetadataReceived.bind(this),this},Object.defineProperty(e.prototype,"onInitSegment",{get:function(){return this._onInitSegment},set:function(e){this._onInitSegment=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onMediaSegment",{get:function(){return this._onMediaSegment},set:function(e){this._onMediaSegment=e},enumerable:!1,configurable:!0}),e.prototype.insertDiscontinuity=function(){this._audioNextDts=this._videoNextDts=void 0},e.prototype.seek=function(e){this._audioStashedLastSample=null,this._videoStashedLastSample=null,this._videoSegmentInfoList.clear(),this._audioSegmentInfoList.clear()},e.prototype.remux=function(e,t){if(!this._onMediaSegment)throw new c.a("MP4Remuxer: onMediaSegment callback must be specificed!");this._dtsBaseInited||this._calculateDtsBase(e,t),t&&this._remuxVideo(t),e&&this._remuxAudio(e)},e.prototype._onTrackMetadataReceived=function(e,t){var i=null,n="mp4",r=t.codec;if("audio"===e)this._audioMeta=t,"mp3"===t.codec&&this._mp3UseMpegAudio?(n="mpeg",r="",i=new Uint8Array):i=V.generateInitSegment(t);else{if("video"!==e)return;this._videoMeta=t,i=V.generateInitSegment(t)}if(!this._onInitSegment)throw new c.a("MP4Remuxer: onInitSegment callback must be specified!");this._onInitSegment(e,{type:e,data:i.buffer,codec:r,container:e+"/"+n,mediaDuration:t.duration})},e.prototype._calculateDtsBase=function(e,t){this._dtsBaseInited||(e&&e.samples&&e.samples.length&&(this._audioDtsBase=e.samples[0].dts),t&&t.samples&&t.samples.length&&(this._videoDtsBase=t.samples[0].dts),this._dtsBase=Math.min(this._audioDtsBase,this._videoDtsBase),this._dtsBaseInited=!0)},e.prototype.getTimestampBase=function(){if(this._dtsBaseInited)return this._dtsBase},e.prototype.flushStashedSamples=function(){var e=this._videoStashedLastSample,t=this._audioStashedLastSample,i={type:"video",id:1,sequenceNumber:0,samples:[],length:0};null!=e&&(i.samples.push(e),i.length=e.length);var n={type:"audio",id:2,sequenceNumber:0,samples:[],length:0};null!=t&&(n.samples.push(t),n.length=t.length),this._videoStashedLastSample=null,this._audioStashedLastSample=null,this._remuxVideo(i,!0),this._remuxAudio(n,!0)},e.prototype._remuxAudio=function(e,t){if(null!=this._audioMeta){var i,n=e,r=n.samples,o=void 0,u=-1,l=this._audioMeta.refSampleDuration,h="mp3"===this._audioMeta.codec&&this._mp3UseMpegAudio,d=this._dtsBaseInited&&void 0===this._audioNextDts,c=!1;if(r&&0!==r.length&&(1!==r.length||t)){var f=0,p=null,m=0;h?(f=0,m=n.length):(f=8,m=8+n.length);var _=null;if(r.length>1&&(m-=(_=r.pop()).length),null!=this._audioStashedLastSample){var g=this._audioStashedLastSample;this._audioStashedLastSample=null,r.unshift(g),m+=g.length}null!=_&&(this._audioStashedLastSample=_);var v=r[0].dts-this._dtsBase;if(this._audioNextDts)o=v-this._audioNextDts;else if(this._audioSegmentInfoList.isEmpty())o=0,this._fillSilentAfterSeek&&!this._videoSegmentInfoList.isEmpty()&&"mp3"!==this._audioMeta.originalCodec&&(c=!0);else{var y=this._audioSegmentInfoList.getLastSampleBefore(v);if(null!=y){var b=v-(y.originalDts+y.duration);b<=3&&(b=0),o=v-(y.dts+y.duration+b)}else o=0}if(c){var S=v-o,T=this._videoSegmentInfoList.getLastSegmentBefore(v);if(null!=T&&T.beginDts=3*l&&this._fillAudioTimestampGap&&!s.a.safari){I=!0;var D,O=Math.floor(o/l);a.a.w(this.TAG,"Large audio timestamp gap detected, may cause AV sync to drift. Silent frames will be generated to avoid unsync.\noriginalDts: "+P+" ms, curRefDts: "+R+" ms, dtsCorrection: "+Math.round(o)+" ms, generate: "+O+" frames"),E=Math.floor(R),x=Math.floor(R+l)-E,null==(D=H.getSilentFrame(this._audioMeta.originalCodec,this._audioMeta.channelCount))&&(a.a.w(this.TAG,"Unable to generate silent frame for "+this._audioMeta.originalCodec+" with "+this._audioMeta.channelCount+" channels, repeat last frame"),D=k),L=[];for(var U=0;U=1?A[A.length-1].duration:Math.floor(l),this._audioNextDts=E+x;-1===u&&(u=E),A.push({dts:E,pts:E,cts:0,unit:g.unit,size:g.unit.byteLength,duration:x,originalDts:P,flags:{isLeading:0,dependsOn:1,isDependedOn:0,hasRedundancy:0}}),I&&A.push.apply(A,L)}}if(0===A.length)return n.samples=[],void(n.length=0);for(h?p=new Uint8Array(m):((p=new Uint8Array(m))[0]=m>>>24&255,p[1]=m>>>16&255,p[2]=m>>>8&255,p[3]=255&m,p.set(V.types.mdat,4)),C=0;C1&&(d-=(c=a.pop()).length),null!=this._videoStashedLastSample){var f=this._videoStashedLastSample;this._videoStashedLastSample=null,a.unshift(f),d+=f.length}null!=c&&(this._videoStashedLastSample=c);var p=a[0].dts-this._dtsBase;if(this._videoNextDts)s=p-this._videoNextDts;else if(this._videoSegmentInfoList.isEmpty())s=0;else{var m=this._videoSegmentInfoList.getLastSampleBefore(p);if(null!=m){var _=p-(m.originalDts+m.duration);_<=3&&(_=0),s=p-(m.dts+m.duration+_)}else s=0}for(var g=new z.b,v=[],y=0;y=1?v[v.length-1].duration:Math.floor(this._videoMeta.refSampleDuration),S){var C=new z.d(T,w,A,f.dts,!0);C.fileposition=f.fileposition,g.appendSyncPoint(C)}v.push({dts:T,pts:w,cts:E,units:f.units,size:f.length,isKeyframe:S,duration:A,originalDts:b,flags:{isLeading:0,dependsOn:S?2:1,isDependedOn:S?1:0,hasRedundancy:0,isNonSync:S?0:1}})}for((h=new Uint8Array(d))[0]=d>>>24&255,h[1]=d>>>16&255,h[2]=d>>>8&255,h[3]=255&d,h.set(V.types.mdat,4),y=0;y0)this._demuxer.bindDataSource(this._ioctl),this._demuxer.timestampBase=this._mediaDataSource.segments[this._currentSegmentIndex].timestampBase,r=this._demuxer.parseChunks(e,t);else if((n=N.probe(e)).match){var s=this._demuxer=new N(n,this._config);this._remuxer||(this._remuxer=new G(this._config)),s.onError=this._onDemuxException.bind(this),s.onMediaInfo=this._onMediaInfo.bind(this),s.onMetaDataArrived=this._onMetaDataArrived.bind(this),s.onTimedID3Metadata=this._onTimedID3Metadata.bind(this),s.onPESPrivateDataDescriptor=this._onPESPrivateDataDescriptor.bind(this),s.onPESPrivateData=this._onPESPrivateData.bind(this),this._remuxer.bindDataSource(this._demuxer),this._demuxer.bindDataSource(this._ioctl),this._remuxer.onInitSegment=this._onRemuxerInitSegmentArrival.bind(this),this._remuxer.onMediaSegment=this._onRemuxerMediaSegmentArrival.bind(this),r=this._demuxer.parseChunks(e,t)}else if((n=v.probe(e)).match){this._demuxer=new v(n,this._config),this._remuxer||(this._remuxer=new G(this._config));var o=this._mediaDataSource;null==o.duration||isNaN(o.duration)||(this._demuxer.overridedDuration=o.duration),"boolean"==typeof o.hasAudio&&(this._demuxer.overridedHasAudio=o.hasAudio),"boolean"==typeof o.hasVideo&&(this._demuxer.overridedHasVideo=o.hasVideo),this._demuxer.timestampBase=o.segments[this._currentSegmentIndex].timestampBase,this._demuxer.onError=this._onDemuxException.bind(this),this._demuxer.onMediaInfo=this._onMediaInfo.bind(this),this._demuxer.onMetaDataArrived=this._onMetaDataArrived.bind(this),this._demuxer.onScriptDataArrived=this._onScriptDataArrived.bind(this),this._remuxer.bindDataSource(this._demuxer.bindDataSource(this._ioctl)),this._remuxer.onInitSegment=this._onRemuxerInitSegmentArrival.bind(this),this._remuxer.onMediaSegment=this._onRemuxerMediaSegmentArrival.bind(this),r=this._demuxer.parseChunks(e,t)}else n=null,a.a.e(this.TAG,"Non MPEG-TS/FLV, Unsupported media type!"),Promise.resolve().then((function(){i._internalAbort()})),this._emitter.emit(Y.a.DEMUX_ERROR,g.a.FORMAT_UNSUPPORTED,"Non MPEG-TS/FLV, Unsupported media type!"),r=0;return r},e.prototype._onMediaInfo=function(e){var t=this;null==this._mediaInfo&&(this._mediaInfo=Object.assign({},e),this._mediaInfo.keyframesIndex=null,this._mediaInfo.segments=[],this._mediaInfo.segmentCount=this._mediaDataSource.segments.length,Object.setPrototypeOf(this._mediaInfo,o.a.prototype));var i=Object.assign({},e);Object.setPrototypeOf(i,o.a.prototype),this._mediaInfo.segments[this._currentSegmentIndex]=i,this._reportSegmentMediaInfo(this._currentSegmentIndex),null!=this._pendingSeekTime&&Promise.resolve().then((function(){var e=t._pendingSeekTime;t._pendingSeekTime=null,t.seek(e)}))},e.prototype._onMetaDataArrived=function(e){this._emitter.emit(Y.a.METADATA_ARRIVED,e)},e.prototype._onScriptDataArrived=function(e){this._emitter.emit(Y.a.SCRIPTDATA_ARRIVED,e)},e.prototype._onTimedID3Metadata=function(e){var t=this._remuxer.getTimestampBase();null!=t&&(null!=e.pts&&(e.pts-=t),null!=e.dts&&(e.dts-=t),this._emitter.emit(Y.a.TIMED_ID3_METADATA_ARRIVED,e))},e.prototype._onPESPrivateDataDescriptor=function(e){this._emitter.emit(Y.a.PES_PRIVATE_DATA_DESCRIPTOR,e)},e.prototype._onPESPrivateData=function(e){var t=this._remuxer.getTimestampBase();null!=t&&(null!=e.pts&&(e.pts-=t),null!=e.nearest_pts&&(e.nearest_pts-=t),null!=e.dts&&(e.dts-=t),this._emitter.emit(Y.a.PES_PRIVATE_DATA_ARRIVED,e))},e.prototype._onIOSeeked=function(){this._remuxer.insertDiscontinuity()},e.prototype._onIOComplete=function(e){var t=e+1;t0&&i[0].originalDts===n&&(n=i[0].pts),this._emitter.emit(Y.a.RECOMMEND_SEEKPOINT,n)}},e.prototype._enableStatisticsReporter=function(){null==this._statisticsReporter&&(this._statisticsReporter=self.setInterval(this._reportStatisticsInfo.bind(this),this._config.statisticsInfoReportInterval))},e.prototype._disableStatisticsReporter=function(){this._statisticsReporter&&(self.clearInterval(this._statisticsReporter),this._statisticsReporter=null)},e.prototype._reportSegmentMediaInfo=function(e){var t=this._mediaInfo.segments[e],i=Object.assign({},t);i.duration=this._mediaInfo.duration,i.segmentCount=this._mediaInfo.segmentCount,delete i.segments,delete i.keyframesIndex,this._emitter.emit(Y.a.MEDIA_INFO,i)},e.prototype._reportStatisticsInfo=function(){var e={};e.url=this._ioctl.currentURL,e.hasRedirect=this._ioctl.hasRedirect,e.hasRedirect&&(e.redirectedURL=this._ioctl.currentRedirectedURL),e.speed=this._ioctl.currentSpeed,e.loaderType=this._ioctl.loaderType,e.currentSegmentIndex=this._currentSegmentIndex,e.totalSegmentCount=this._mediaDataSource.segments.length,this._emitter.emit(Y.a.STATISTICS_INFO,e)},e}();t.a=q},function(e,t,i){"use strict";var n,r=i(0),a=function(){function e(){this._firstCheckpoint=0,this._lastCheckpoint=0,this._intervalBytes=0,this._totalBytes=0,this._lastSecondBytes=0,self.performance&&self.performance.now?this._now=self.performance.now.bind(self.performance):this._now=Date.now}return e.prototype.reset=function(){this._firstCheckpoint=this._lastCheckpoint=0,this._totalBytes=this._intervalBytes=0,this._lastSecondBytes=0},e.prototype.addBytes=function(e){0===this._firstCheckpoint?(this._firstCheckpoint=this._now(),this._lastCheckpoint=this._firstCheckpoint,this._intervalBytes+=e,this._totalBytes+=e):this._now()-this._lastCheckpoint<1e3?(this._intervalBytes+=e,this._totalBytes+=e):(this._lastSecondBytes=this._intervalBytes,this._intervalBytes=e,this._totalBytes+=e,this._lastCheckpoint=this._now())},Object.defineProperty(e.prototype,"currentKBps",{get:function(){this.addBytes(0);var e=(this._now()-this._lastCheckpoint)/1e3;return 0==e&&(e=1),this._intervalBytes/e/1024},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"lastSecondKBps",{get:function(){return this.addBytes(0),0!==this._lastSecondBytes?this._lastSecondBytes/1024:this._now()-this._lastCheckpoint>=500?this.currentKBps:0},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"averageKBps",{get:function(){var e=(this._now()-this._firstCheckpoint)/1e3;return this._totalBytes/e/1024},enumerable:!1,configurable:!0}),e}(),s=i(2),o=i(4),u=i(3),l=(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)t.hasOwnProperty(i)&&(e[i]=t[i])})(e,t)},function(e,t){function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)}),h=function(e){function t(t,i){var n=e.call(this,"fetch-stream-loader")||this;return n.TAG="FetchStreamLoader",n._seekHandler=t,n._config=i,n._needStash=!0,n._requestAbort=!1,n._abortController=null,n._contentLength=null,n._receivedLength=0,n}return l(t,e),t.isSupported=function(){try{var e=o.a.msedge&&o.a.version.minor>=15048,t=!o.a.msedge||e;return self.fetch&&self.ReadableStream&&t}catch(e){return!1}},t.prototype.destroy=function(){this.isWorking()&&this.abort(),e.prototype.destroy.call(this)},t.prototype.open=function(e,t){var i=this;this._dataSource=e,this._range=t;var n=e.url;this._config.reuseRedirectedURL&&null!=e.redirectedURL&&(n=e.redirectedURL);var r=this._seekHandler.getConfig(n,t),a=new self.Headers;if("object"==typeof r.headers){var o=r.headers;for(var l in o)o.hasOwnProperty(l)&&a.append(l,o[l])}var h={method:"GET",headers:a,mode:"cors",cache:"default",referrerPolicy:"no-referrer-when-downgrade"};if("object"==typeof this._config.headers)for(var l in this._config.headers)a.append(l,this._config.headers[l]);!1===e.cors&&(h.mode="same-origin"),e.withCredentials&&(h.credentials="include"),e.referrerPolicy&&(h.referrerPolicy=e.referrerPolicy),self.AbortController&&(this._abortController=new self.AbortController,h.signal=this._abortController.signal),this._status=s.c.kConnecting,self.fetch(r.url,h).then((function(e){if(i._requestAbort)return i._status=s.c.kIdle,void e.body.cancel();if(e.ok&&e.status>=200&&e.status<=299){if(e.url!==r.url&&i._onURLRedirect){var t=i._seekHandler.removeURLParameters(e.url);i._onURLRedirect(t)}var n=e.headers.get("Content-Length");return null!=n&&(i._contentLength=parseInt(n),0!==i._contentLength&&i._onContentLengthKnown&&i._onContentLengthKnown(i._contentLength)),i._pump.call(i,e.body.getReader())}if(i._status=s.c.kError,!i._onError)throw new u.d("FetchStreamLoader: Http code invalid, "+e.status+" "+e.statusText);i._onError(s.b.HTTP_STATUS_CODE_INVALID,{code:e.status,msg:e.statusText})})).catch((function(e){if(!i._abortController||!i._abortController.signal.aborted){if(i._status=s.c.kError,!i._onError)throw e;i._onError(s.b.EXCEPTION,{code:-1,msg:e.message})}}))},t.prototype.abort=function(){if(this._requestAbort=!0,(this._status!==s.c.kBuffering||!o.a.chrome)&&this._abortController)try{this._abortController.abort()}catch(e){}},t.prototype._pump=function(e){var t=this;return e.read().then((function(i){if(i.done)if(null!==t._contentLength&&t._receivedLength299)){if(this._status=s.c.kError,!this._onError)throw new u.d("MozChunkedLoader: Http code invalid, "+t.status+" "+t.statusText);this._onError(s.b.HTTP_STATUS_CODE_INVALID,{code:t.status,msg:t.statusText})}else this._status=s.c.kBuffering}},t.prototype._onProgress=function(e){if(this._status!==s.c.kError){null===this._contentLength&&null!==e.total&&0!==e.total&&(this._contentLength=e.total,this._onContentLengthKnown&&this._onContentLengthKnown(this._contentLength));var t=e.target.response,i=this._range.from+this._receivedLength;this._receivedLength+=t.byteLength,this._onDataArrival&&this._onDataArrival(t,i,this._receivedLength)}},t.prototype._onLoadEnd=function(e){!0!==this._requestAbort?this._status!==s.c.kError&&(this._status=s.c.kComplete,this._onComplete&&this._onComplete(this._range.from,this._range.from+this._receivedLength-1)):this._requestAbort=!1},t.prototype._onXhrError=function(e){this._status=s.c.kError;var t=0,i=null;if(this._contentLength&&e.loaded=this._contentLength&&(i=this._range.from+this._contentLength-1),this._currentRequestRange={from:t,to:i},this._internalOpen(this._dataSource,this._currentRequestRange)},t.prototype._internalOpen=function(e,t){this._lastTimeLoaded=0;var i=e.url;this._config.reuseRedirectedURL&&(null!=this._currentRedirectedURL?i=this._currentRedirectedURL:null!=e.redirectedURL&&(i=e.redirectedURL));var n=this._seekHandler.getConfig(i,t);this._currentRequestURL=n.url;var r=this._xhr=new XMLHttpRequest;if(r.open("GET",n.url,!0),r.responseType="arraybuffer",r.onreadystatechange=this._onReadyStateChange.bind(this),r.onprogress=this._onProgress.bind(this),r.onload=this._onLoad.bind(this),r.onerror=this._onXhrError.bind(this),e.withCredentials&&(r.withCredentials=!0),"object"==typeof n.headers){var a=n.headers;for(var s in a)a.hasOwnProperty(s)&&r.setRequestHeader(s,a[s])}if("object"==typeof this._config.headers)for(var s in a=this._config.headers)a.hasOwnProperty(s)&&r.setRequestHeader(s,a[s]);r.send()},t.prototype.abort=function(){this._requestAbort=!0,this._internalAbort(),this._status=s.c.kComplete},t.prototype._internalAbort=function(){this._xhr&&(this._xhr.onreadystatechange=null,this._xhr.onprogress=null,this._xhr.onload=null,this._xhr.onerror=null,this._xhr.abort(),this._xhr=null)},t.prototype._onReadyStateChange=function(e){var t=e.target;if(2===t.readyState){if(null!=t.responseURL){var i=this._seekHandler.removeURLParameters(t.responseURL);t.responseURL!==this._currentRequestURL&&i!==this._currentRedirectedURL&&(this._currentRedirectedURL=i,this._onURLRedirect&&this._onURLRedirect(i))}if(t.status>=200&&t.status<=299){if(this._waitForTotalLength)return;this._status=s.c.kBuffering}else{if(this._status=s.c.kError,!this._onError)throw new u.d("RangeLoader: Http code invalid, "+t.status+" "+t.statusText);this._onError(s.b.HTTP_STATUS_CODE_INVALID,{code:t.status,msg:t.statusText})}}},t.prototype._onProgress=function(e){if(this._status!==s.c.kError){if(null===this._contentLength){var t=!1;if(this._waitForTotalLength){this._waitForTotalLength=!1,this._totalLengthReceived=!0,t=!0;var i=e.total;this._internalAbort(),null!=i&0!==i&&(this._totalLength=i)}if(-1===this._range.to?this._contentLength=this._totalLength-this._range.from:this._contentLength=this._range.to-this._range.from+1,t)return void this._openSubRange();this._onContentLengthKnown&&this._onContentLengthKnown(this._contentLength)}var n=e.loaded-this._lastTimeLoaded;this._lastTimeLoaded=e.loaded,this._speedSampler.addBytes(n)}},t.prototype._normalizeSpeed=function(e){var t=this._chunkSizeKBList,i=t.length-1,n=0,r=0,a=i;if(e=t[n]&&e=3&&(t=this._speedSampler.currentKBps)),0!==t){var i=this._normalizeSpeed(t);this._currentSpeedNormalized!==i&&(this._currentSpeedNormalized=i,this._currentChunkSizeKB=i)}var n=e.target.response,r=this._range.from+this._receivedLength;this._receivedLength+=n.byteLength;var a=!1;null!=this._contentLength&&this._receivedLength0&&this._receivedLength0)for(var a=i.split("&"),s=0;s0;o[0]!==this._startName&&o[0]!==this._endName&&(u&&(r+="&"),r+=a[s])}return 0===r.length?t:t+"?"+r},e}(),y=function(){function e(e,t,i){this.TAG="IOController",this._config=t,this._extraData=i,this._stashInitialSize=65536,null!=t.stashInitialSize&&t.stashInitialSize>0&&(this._stashInitialSize=t.stashInitialSize),this._stashUsed=0,this._stashSize=this._stashInitialSize,this._bufferSize=3145728,this._stashBuffer=new ArrayBuffer(this._bufferSize),this._stashByteStart=0,this._enableStash=!0,!1===t.enableStashBuffer&&(this._enableStash=!1),this._loader=null,this._loaderClass=null,this._seekHandler=null,this._dataSource=e,this._isWebSocketURL=/wss?:\/\/(.+?)/.test(e.url),this._refTotalLength=e.filesize?e.filesize:null,this._totalLength=this._refTotalLength,this._fullRequestFlag=!1,this._currentRange=null,this._redirectedURL=null,this._speedNormalized=0,this._speedSampler=new a,this._speedNormalizeList=[32,64,96,128,192,256,384,512,768,1024,1536,2048,3072,4096],this._isEarlyEofReconnecting=!1,this._paused=!1,this._resumeFrom=0,this._onDataArrival=null,this._onSeeked=null,this._onError=null,this._onComplete=null,this._onRedirect=null,this._onRecoveredEarlyEof=null,this._selectSeekHandler(),this._selectLoader(),this._createLoader()}return e.prototype.destroy=function(){this._loader.isWorking()&&this._loader.abort(),this._loader.destroy(),this._loader=null,this._loaderClass=null,this._dataSource=null,this._stashBuffer=null,this._stashUsed=this._stashSize=this._bufferSize=this._stashByteStart=0,this._currentRange=null,this._speedSampler=null,this._isEarlyEofReconnecting=!1,this._onDataArrival=null,this._onSeeked=null,this._onError=null,this._onComplete=null,this._onRedirect=null,this._onRecoveredEarlyEof=null,this._extraData=null},e.prototype.isWorking=function(){return this._loader&&this._loader.isWorking()&&!this._paused},e.prototype.isPaused=function(){return this._paused},Object.defineProperty(e.prototype,"status",{get:function(){return this._loader.status},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"extraData",{get:function(){return this._extraData},set:function(e){this._extraData=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onDataArrival",{get:function(){return this._onDataArrival},set:function(e){this._onDataArrival=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onSeeked",{get:function(){return this._onSeeked},set:function(e){this._onSeeked=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onError",{get:function(){return this._onError},set:function(e){this._onError=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onComplete",{get:function(){return this._onComplete},set:function(e){this._onComplete=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onRedirect",{get:function(){return this._onRedirect},set:function(e){this._onRedirect=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onRecoveredEarlyEof",{get:function(){return this._onRecoveredEarlyEof},set:function(e){this._onRecoveredEarlyEof=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"currentURL",{get:function(){return this._dataSource.url},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"hasRedirect",{get:function(){return null!=this._redirectedURL||null!=this._dataSource.redirectedURL},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"currentRedirectedURL",{get:function(){return this._redirectedURL||this._dataSource.redirectedURL},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"currentSpeed",{get:function(){return this._loaderClass===p?this._loader.currentSpeed:this._speedSampler.lastSecondKBps},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"loaderType",{get:function(){return this._loader.type},enumerable:!1,configurable:!0}),e.prototype._selectSeekHandler=function(){var e=this._config;if("range"===e.seekType)this._seekHandler=new g(this._config.rangeLoadZeroStart);else if("param"===e.seekType){var t=e.seekParamStart||"bstart",i=e.seekParamEnd||"bend";this._seekHandler=new v(t,i)}else{if("custom"!==e.seekType)throw new u.b("Invalid seekType in config: "+e.seekType);if("function"!=typeof e.customSeekHandler)throw new u.b("Custom seekType specified in config but invalid customSeekHandler!");this._seekHandler=new e.customSeekHandler}},e.prototype._selectLoader=function(){if(null!=this._config.customLoader)this._loaderClass=this._config.customLoader;else if(this._isWebSocketURL)this._loaderClass=_;else if(h.isSupported())this._loaderClass=h;else if(c.isSupported())this._loaderClass=c;else{if(!p.isSupported())throw new u.d("Your browser doesn't support xhr with arraybuffer responseType!");this._loaderClass=p}},e.prototype._createLoader=function(){this._loader=new this._loaderClass(this._seekHandler,this._config),!1===this._loader.needStashBuffer&&(this._enableStash=!1),this._loader.onContentLengthKnown=this._onContentLengthKnown.bind(this),this._loader.onURLRedirect=this._onURLRedirect.bind(this),this._loader.onDataArrival=this._onLoaderChunkArrival.bind(this),this._loader.onComplete=this._onLoaderComplete.bind(this),this._loader.onError=this._onLoaderError.bind(this)},e.prototype.open=function(e){this._currentRange={from:0,to:-1},e&&(this._currentRange.from=e),this._speedSampler.reset(),e||(this._fullRequestFlag=!0),this._loader.open(this._dataSource,Object.assign({},this._currentRange))},e.prototype.abort=function(){this._loader.abort(),this._paused&&(this._paused=!1,this._resumeFrom=0)},e.prototype.pause=function(){this.isWorking()&&(this._loader.abort(),0!==this._stashUsed?(this._resumeFrom=this._stashByteStart,this._currentRange.to=this._stashByteStart-1):this._resumeFrom=this._currentRange.to+1,this._stashUsed=0,this._stashByteStart=0,this._paused=!0)},e.prototype.resume=function(){if(this._paused){this._paused=!1;var e=this._resumeFrom;this._resumeFrom=0,this._internalSeek(e,!0)}},e.prototype.seek=function(e){this._paused=!1,this._stashUsed=0,this._stashByteStart=0,this._internalSeek(e,!0)},e.prototype._internalSeek=function(e,t){this._loader.isWorking()&&this._loader.abort(),this._flushStashBuffer(t),this._loader.destroy(),this._loader=null;var i={from:e,to:-1};this._currentRange={from:i.from,to:-1},this._speedSampler.reset(),this._stashSize=this._stashInitialSize,this._createLoader(),this._loader.open(this._dataSource,i),this._onSeeked&&this._onSeeked()},e.prototype.updateUrl=function(e){if(!e||"string"!=typeof e||0===e.length)throw new u.b("Url must be a non-empty string!");this._dataSource.url=e},e.prototype._expandBuffer=function(e){for(var t=this._stashSize;t+10485760){var n=new Uint8Array(this._stashBuffer,0,this._stashUsed);new Uint8Array(i,0,t).set(n,0)}this._stashBuffer=i,this._bufferSize=t}},e.prototype._normalizeSpeed=function(e){var t=this._speedNormalizeList,i=t.length-1,n=0,r=0,a=i;if(e=t[n]&&e=512&&e<=1024?Math.floor(1.5*e):2*e)>8192&&(t=8192);var i=1024*t+1048576;this._bufferSize0){var a=this._stashBuffer.slice(0,this._stashUsed);(l=this._dispatchChunks(a,this._stashByteStart))0&&(h=new Uint8Array(a,l),o.set(h,0),this._stashUsed=h.byteLength,this._stashByteStart+=l):(this._stashUsed=0,this._stashByteStart+=l),this._stashUsed+e.byteLength>this._bufferSize&&(this._expandBuffer(this._stashUsed+e.byteLength),o=new Uint8Array(this._stashBuffer,0,this._bufferSize)),o.set(new Uint8Array(e),this._stashUsed),this._stashUsed+=e.byteLength}else(l=this._dispatchChunks(e,t))this._bufferSize&&(this._expandBuffer(s),o=new Uint8Array(this._stashBuffer,0,this._bufferSize)),o.set(new Uint8Array(e,l),0),this._stashUsed+=s,this._stashByteStart=t+l);else if(0===this._stashUsed){var s;(l=this._dispatchChunks(e,t))this._bufferSize&&this._expandBuffer(s),(o=new Uint8Array(this._stashBuffer,0,this._bufferSize)).set(new Uint8Array(e,l),0),this._stashUsed+=s,this._stashByteStart=t+l)}else{var o,l;if(this._stashUsed+e.byteLength>this._bufferSize&&this._expandBuffer(this._stashUsed+e.byteLength),(o=new Uint8Array(this._stashBuffer,0,this._bufferSize)).set(new Uint8Array(e),this._stashUsed),this._stashUsed+=e.byteLength,(l=this._dispatchChunks(this._stashBuffer.slice(0,this._stashUsed),this._stashByteStart))0){var h=new Uint8Array(this._stashBuffer,l);o.set(h,0)}this._stashUsed-=l,this._stashByteStart+=l}}},e.prototype._flushStashBuffer=function(e){if(this._stashUsed>0){var t=this._stashBuffer.slice(0,this._stashUsed),i=this._dispatchChunks(t,this._stashByteStart),n=t.byteLength-i;if(i0){var a=new Uint8Array(this._stashBuffer,0,this._bufferSize),s=new Uint8Array(t,i);a.set(s,0),this._stashUsed=s.byteLength,this._stashByteStart+=i}return 0}r.a.w(this.TAG,n+" bytes unconsumed data remain when flush buffer, dropped")}return this._stashUsed=0,this._stashByteStart=0,n}return 0},e.prototype._onLoaderComplete=function(e,t){this._flushStashBuffer(!0),this._onComplete&&this._onComplete(this._extraData)},e.prototype._onLoaderError=function(e,t){switch(r.a.e(this.TAG,"Loader error, code = "+t.code+", msg = "+t.msg),this._flushStashBuffer(!1),this._isEarlyEofReconnecting&&(this._isEarlyEofReconnecting=!1,e=s.b.UNRECOVERABLE_EARLY_EOF),e){case s.b.EARLY_EOF:if(!this._config.isLive&&this._totalLength){var i=this._currentRange.to+1;return void(i0}),!1)}e.exports=function(e,t){t=t||{};var r={main:i.m},o=t.all?{main:Object.keys(r.main)}:function(e,t){for(var i={main:[t]},n={main:[]},r={main:{}};s(i);)for(var o=Object.keys(i),u=0;u1)for(var i=1;i0&&(n+=";codecs="+i.codec);var r=!1;if(d.a.v(this.TAG,"Received Initialization Segment, mimeType: "+n),this._lastInitSegments[i.type]=i,n!==this._mimeTypes[i.type]){if(this._mimeTypes[i.type])d.a.v(this.TAG,"Notice: "+i.type+" mimeType changed, origin: "+this._mimeTypes[i.type]+", target: "+n);else{r=!0;try{var a=this._sourceBuffers[i.type]=this._mediaSource.addSourceBuffer(n);a.addEventListener("error",this.e.onSourceBufferError),a.addEventListener("updateend",this.e.onSourceBufferUpdateEnd)}catch(e){return d.a.e(this.TAG,e.message),void this._emitter.emit(S,{code:e.code,msg:e.message})}}this._mimeTypes[i.type]=n}t||this._pendingSegments[i.type].push(i),r||this._sourceBuffers[i.type]&&!this._sourceBuffers[i.type].updating&&this._doAppendSegments(),c.a.safari&&"audio/mpeg"===i.container&&i.mediaDuration>0&&(this._requireSetMediaDuration=!0,this._pendingMediaDuration=i.mediaDuration/1e3,this._updateMediaSourceDuration())},e.prototype.appendMediaSegment=function(e){var t=e;this._pendingSegments[t.type].push(t),this._config.autoCleanupSourceBuffer&&this._needCleanupSourceBuffer()&&this._doCleanupSourceBuffer();var i=this._sourceBuffers[t.type];!i||i.updating||this._hasPendingRemoveRanges()||this._doAppendSegments()},e.prototype.seek=function(e){for(var t in this._sourceBuffers)if(this._sourceBuffers[t]){var i=this._sourceBuffers[t];if("open"===this._mediaSource.readyState)try{i.abort()}catch(e){d.a.e(this.TAG,e.message)}this._idrList.clear();var n=this._pendingSegments[t];if(n.splice(0,n.length),"closed"!==this._mediaSource.readyState){for(var r=0;r=1&&e-n.start(0)>=this._config.autoCleanupMaxBackwardDuration)return!0}}return!1},e.prototype._doCleanupSourceBuffer=function(){var e=this._mediaElement.currentTime;for(var t in this._sourceBuffers){var i=this._sourceBuffers[t];if(i){for(var n=i.buffered,r=!1,a=0;a=this._config.autoCleanupMaxBackwardDuration){r=!0;var u=e-this._config.autoCleanupMinBackwardDuration;this._pendingRemoveRanges[t].push({start:s,end:u})}}else o0&&(isNaN(t)||i>t)&&(d.a.v(this.TAG,"Update MediaSource duration from "+t+" to "+i),this._mediaSource.duration=i),this._requireSetMediaDuration=!1,this._pendingMediaDuration=0}},e.prototype._doRemoveRanges=function(){for(var e in this._pendingRemoveRanges)if(this._sourceBuffers[e]&&!this._sourceBuffers[e].updating)for(var t=this._sourceBuffers[e],i=this._pendingRemoveRanges[e];i.length&&!t.updating;){var n=i.shift();t.remove(n.start,n.end)}},e.prototype._doAppendSegments=function(){var e=this._pendingSegments;for(var t in e)if(this._sourceBuffers[t]&&!this._sourceBuffers[t].updating&&e[t].length>0){var i=e[t].shift();if(i.timestampOffset){var n=this._sourceBuffers[t].timestampOffset,r=i.timestampOffset/1e3;Math.abs(n-r)>.1&&(d.a.v(this.TAG,"Update MPEG audio timestampOffset from "+n+" to "+r),this._sourceBuffers[t].timestampOffset=r),delete i.timestampOffset}if(!i.data||0===i.data.byteLength)continue;try{this._sourceBuffers[t].appendBuffer(i.data),this._isBufferFull=!1,"video"===t&&i.hasOwnProperty("info")&&this._idrList.appendArray(i.info.syncPoints)}catch(e){this._pendingSegments[t].unshift(i),22===e.code?(this._isBufferFull||this._emitter.emit(w),this._isBufferFull=!0):(d.a.e(this.TAG,e.message),this._emitter.emit(S,{code:e.code,msg:e.message}))}}},e.prototype._onSourceOpen=function(){if(d.a.v(this.TAG,"MediaSource onSourceOpen"),this._mediaSource.removeEventListener("sourceopen",this.e.onSourceOpen),this._pendingSourceBufferInit.length>0)for(var e=this._pendingSourceBufferInit;e.length;){var t=e.shift();this.appendInitSegment(t,!0)}this._hasPendingSegments()&&this._doAppendSegments(),this._emitter.emit(T)},e.prototype._onSourceEnded=function(){d.a.v(this.TAG,"MediaSource onSourceEnded")},e.prototype._onSourceClose=function(){d.a.v(this.TAG,"MediaSource onSourceClose"),this._mediaSource&&null!=this.e&&(this._mediaSource.removeEventListener("sourceopen",this.e.onSourceOpen),this._mediaSource.removeEventListener("sourceended",this.e.onSourceEnded),this._mediaSource.removeEventListener("sourceclose",this.e.onSourceClose))},e.prototype._hasPendingSegments=function(){var e=this._pendingSegments;return e.video.length>0||e.audio.length>0},e.prototype._hasPendingRemoveRanges=function(){var e=this._pendingRemoveRanges;return e.video.length>0||e.audio.length>0},e.prototype._onSourceBufferUpdateEnd=function(){this._requireSetMediaDuration?this._updateMediaSourceDuration():this._hasPendingRemoveRanges()?this._doRemoveRanges():this._hasPendingSegments()?this._doAppendSegments():this._hasPendingEos&&this.endOfStream(),this._emitter.emit(E)},e.prototype._onSourceBufferError=function(e){d.a.e(this.TAG,"SourceBuffer Error: "+e)},e}(),P=i(5),I={NETWORK_ERROR:"NetworkError",MEDIA_ERROR:"MediaError",OTHER_ERROR:"OtherError"},L={NETWORK_EXCEPTION:u.b.EXCEPTION,NETWORK_STATUS_CODE_INVALID:u.b.HTTP_STATUS_CODE_INVALID,NETWORK_TIMEOUT:u.b.CONNECTING_TIMEOUT,NETWORK_UNRECOVERABLE_EARLY_EOF:u.b.UNRECOVERABLE_EARLY_EOF,MEDIA_MSE_ERROR:"MediaMSEError",MEDIA_FORMAT_ERROR:P.a.FORMAT_ERROR,MEDIA_FORMAT_UNSUPPORTED:P.a.FORMAT_UNSUPPORTED,MEDIA_CODEC_UNSUPPORTED:P.a.CODEC_UNSUPPORTED},x=function(){function e(e,t){this.TAG="MSEPlayer",this._type="MSEPlayer",this._emitter=new h.a,this._config=s(),"object"==typeof t&&Object.assign(this._config,t);var i=e.type.toLowerCase();if("mse"!==i&&"mpegts"!==i&&"m2ts"!==i&&"flv"!==i)throw new C.b("MSEPlayer requires an mpegts/m2ts/flv MediaDataSource input!");!0===e.isLive&&(this._config.isLive=!0),this.e={onvLoadedMetadata:this._onvLoadedMetadata.bind(this),onvSeeking:this._onvSeeking.bind(this),onvCanPlay:this._onvCanPlay.bind(this),onvStalled:this._onvStalled.bind(this),onvProgress:this._onvProgress.bind(this)},self.performance&&self.performance.now?this._now=self.performance.now.bind(self.performance):this._now=Date.now,this._pendingSeekTime=null,this._requestSetTime=!1,this._seekpointRecord=null,this._progressChecker=null,this._mediaDataSource=e,this._mediaElement=null,this._msectl=null,this._transmuxer=null,this._mseSourceOpened=!1,this._hasPendingLoad=!1,this._receivedCanPlay=!1,this._mediaInfo=null,this._statisticsInfo=null;var n=c.a.chrome&&(c.a.version.major<50||50===c.a.version.major&&c.a.version.build<2661);this._alwaysSeekKeyframe=!!(n||c.a.msedge||c.a.msie),this._alwaysSeekKeyframe&&(this._config.accurateSeek=!1)}return e.prototype.destroy=function(){null!=this._progressChecker&&(window.clearInterval(this._progressChecker),this._progressChecker=null),this._transmuxer&&this.unload(),this._mediaElement&&this.detachMediaElement(),this.e=null,this._mediaDataSource=null,this._emitter.removeAllListeners(),this._emitter=null},e.prototype.on=function(e,t){var i=this;e===f.MEDIA_INFO?null!=this._mediaInfo&&Promise.resolve().then((function(){i._emitter.emit(f.MEDIA_INFO,i.mediaInfo)})):e===f.STATISTICS_INFO&&null!=this._statisticsInfo&&Promise.resolve().then((function(){i._emitter.emit(f.STATISTICS_INFO,i.statisticsInfo)})),this._emitter.addListener(e,t)},e.prototype.off=function(e,t){this._emitter.removeListener(e,t)},e.prototype.attachMediaElement=function(e){var t=this;if(this._mediaElement=e,e.addEventListener("loadedmetadata",this.e.onvLoadedMetadata),e.addEventListener("seeking",this.e.onvSeeking),e.addEventListener("canplay",this.e.onvCanPlay),e.addEventListener("stalled",this.e.onvStalled),e.addEventListener("progress",this.e.onvProgress),this._msectl=new k(this._config),this._msectl.on(E,this._onmseUpdateEnd.bind(this)),this._msectl.on(w,this._onmseBufferFull.bind(this)),this._msectl.on(T,(function(){t._mseSourceOpened=!0,t._hasPendingLoad&&(t._hasPendingLoad=!1,t.load())})),this._msectl.on(S,(function(e){t._emitter.emit(f.ERROR,I.MEDIA_ERROR,L.MEDIA_MSE_ERROR,e)})),this._msectl.attachMediaElement(e),null!=this._pendingSeekTime)try{e.currentTime=this._pendingSeekTime,this._pendingSeekTime=null}catch(e){}},e.prototype.detachMediaElement=function(){this._mediaElement&&(this._msectl.detachMediaElement(),this._mediaElement.removeEventListener("loadedmetadata",this.e.onvLoadedMetadata),this._mediaElement.removeEventListener("seeking",this.e.onvSeeking),this._mediaElement.removeEventListener("canplay",this.e.onvCanPlay),this._mediaElement.removeEventListener("stalled",this.e.onvStalled),this._mediaElement.removeEventListener("progress",this.e.onvProgress),this._mediaElement=null),this._msectl&&(this._msectl.destroy(),this._msectl=null)},e.prototype.load=function(){var e=this;if(!this._mediaElement)throw new C.a("HTMLMediaElement must be attached before load()!");if(this._transmuxer)throw new C.a("MSEPlayer.load() has been called, please call unload() first!");this._hasPendingLoad||(this._config.deferLoadAfterSourceOpen&&!1===this._mseSourceOpened?this._hasPendingLoad=!0:(this._mediaElement.readyState>0&&(this._requestSetTime=!0,this._mediaElement.currentTime=0),this._transmuxer=new b(this._mediaDataSource,this._config),this._transmuxer.on(v.a.INIT_SEGMENT,(function(t,i){e._msectl.appendInitSegment(i)})),this._transmuxer.on(v.a.MEDIA_SEGMENT,(function(t,i){if(e._msectl.appendMediaSegment(i),e._config.lazyLoad&&!e._config.isLive){var n=e._mediaElement.currentTime;i.info.endDts>=1e3*(n+e._config.lazyLoadMaxDuration)&&null==e._progressChecker&&(d.a.v(e.TAG,"Maximum buffering duration exceeded, suspend transmuxing task"),e._suspendTransmuxer())}})),this._transmuxer.on(v.a.LOADING_COMPLETE,(function(){e._msectl.endOfStream(),e._emitter.emit(f.LOADING_COMPLETE)})),this._transmuxer.on(v.a.RECOVERED_EARLY_EOF,(function(){e._emitter.emit(f.RECOVERED_EARLY_EOF)})),this._transmuxer.on(v.a.IO_ERROR,(function(t,i){e._emitter.emit(f.ERROR,I.NETWORK_ERROR,t,i)})),this._transmuxer.on(v.a.DEMUX_ERROR,(function(t,i){e._emitter.emit(f.ERROR,I.MEDIA_ERROR,t,{code:-1,msg:i})})),this._transmuxer.on(v.a.MEDIA_INFO,(function(t){e._mediaInfo=t,e._emitter.emit(f.MEDIA_INFO,Object.assign({},t))})),this._transmuxer.on(v.a.METADATA_ARRIVED,(function(t){e._emitter.emit(f.METADATA_ARRIVED,t)})),this._transmuxer.on(v.a.SCRIPTDATA_ARRIVED,(function(t){e._emitter.emit(f.SCRIPTDATA_ARRIVED,t)})),this._transmuxer.on(v.a.TIMED_ID3_METADATA_ARRIVED,(function(t){e._emitter.emit(f.TIMED_ID3_METADATA_ARRIVED,t)})),this._transmuxer.on(v.a.PES_PRIVATE_DATA_DESCRIPTOR,(function(t){e._emitter.emit(f.PES_PRIVATE_DATA_DESCRIPTOR,t)})),this._transmuxer.on(v.a.PES_PRIVATE_DATA_ARRIVED,(function(t){e._emitter.emit(f.PES_PRIVATE_DATA_ARRIVED,t)})),this._transmuxer.on(v.a.STATISTICS_INFO,(function(t){e._statisticsInfo=e._fillStatisticsInfo(t),e._emitter.emit(f.STATISTICS_INFO,Object.assign({},e._statisticsInfo))})),this._transmuxer.on(v.a.RECOMMEND_SEEKPOINT,(function(t){e._mediaElement&&!e._config.accurateSeek&&(e._requestSetTime=!0,e._mediaElement.currentTime=t/1e3)})),this._transmuxer.open()))},e.prototype.unload=function(){this._mediaElement&&this._mediaElement.pause(),this._msectl&&this._msectl.seek(0),this._transmuxer&&(this._transmuxer.close(),this._transmuxer.destroy(),this._transmuxer=null)},e.prototype.play=function(){return this._mediaElement.play()},e.prototype.pause=function(){this._mediaElement.pause()},Object.defineProperty(e.prototype,"type",{get:function(){return this._type},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"buffered",{get:function(){return this._mediaElement.buffered},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"duration",{get:function(){return this._mediaElement.duration},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"volume",{get:function(){return this._mediaElement.volume},set:function(e){this._mediaElement.volume=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"muted",{get:function(){return this._mediaElement.muted},set:function(e){this._mediaElement.muted=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"currentTime",{get:function(){return this._mediaElement?this._mediaElement.currentTime:0},set:function(e){this._mediaElement?this._internalSeek(e):this._pendingSeekTime=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"mediaInfo",{get:function(){return Object.assign({},this._mediaInfo)},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"statisticsInfo",{get:function(){return null==this._statisticsInfo&&(this._statisticsInfo={}),this._statisticsInfo=this._fillStatisticsInfo(this._statisticsInfo),Object.assign({},this._statisticsInfo)},enumerable:!1,configurable:!0}),e.prototype._fillStatisticsInfo=function(e){if(e.playerType=this._type,!(this._mediaElement instanceof HTMLVideoElement))return e;var t=!0,i=0,n=0;if(this._mediaElement.getVideoPlaybackQuality){var r=this._mediaElement.getVideoPlaybackQuality();i=r.totalVideoFrames,n=r.droppedVideoFrames}else null!=this._mediaElement.webkitDecodedFrameCount?(i=this._mediaElement.webkitDecodedFrameCount,n=this._mediaElement.webkitDroppedFrameCount):t=!1;return t&&(e.decodedFrames=i,e.droppedFrames=n),e},e.prototype._onmseUpdateEnd=function(){var e=this._mediaElement.buffered,t=this._mediaElement.currentTime;if(this._config.isLive&&this._config.liveBufferLatencyChasing&&e.length>0&&!this._mediaElement.paused){var i=e.end(e.length-1);if(i>this._config.liveBufferLatencyMaxLatency&&i-t>this._config.liveBufferLatencyMaxLatency){var n=i-this._config.liveBufferLatencyMinRemain;this.currentTime=n}}if(this._config.lazyLoad&&!this._config.isLive){for(var r=0,a=0;a=t+this._config.lazyLoadMaxDuration&&null==this._progressChecker&&(d.a.v(this.TAG,"Maximum buffering duration exceeded, suspend transmuxing task"),this._suspendTransmuxer())}},e.prototype._onmseBufferFull=function(){d.a.v(this.TAG,"MSE SourceBuffer is full, suspend transmuxing task"),null==this._progressChecker&&this._suspendTransmuxer()},e.prototype._suspendTransmuxer=function(){this._transmuxer&&(this._transmuxer.pause(),null==this._progressChecker&&(this._progressChecker=window.setInterval(this._checkProgressAndResume.bind(this),1e3)))},e.prototype._checkProgressAndResume=function(){for(var e=this._mediaElement.currentTime,t=this._mediaElement.buffered,i=!1,n=0;n=r&&e=a-this._config.lazyLoadRecoverDuration&&(i=!0);break}}i&&(window.clearInterval(this._progressChecker),this._progressChecker=null,i&&(d.a.v(this.TAG,"Continue loading from paused position"),this._transmuxer.resume()))},e.prototype._isTimepointBuffered=function(e){for(var t=this._mediaElement.buffered,i=0;i=n&&e0){var r=this._mediaElement.buffered.start(0);(r<1&&e0&&t.currentTime0){var n=i.start(0);if(n<1&&t0&&(this._mediaElement.currentTime=0),this._mediaElement.preload="auto",this._mediaElement.load(),this._statisticsReporter=window.setInterval(this._reportStatisticsInfo.bind(this),this._config.statisticsInfoReportInterval)},e.prototype.unload=function(){this._mediaElement&&(this._mediaElement.src="",this._mediaElement.removeAttribute("src")),null!=this._statisticsReporter&&(window.clearInterval(this._statisticsReporter),this._statisticsReporter=null)},e.prototype.play=function(){return this._mediaElement.play()},e.prototype.pause=function(){this._mediaElement.pause()},Object.defineProperty(e.prototype,"type",{get:function(){return this._type},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"buffered",{get:function(){return this._mediaElement.buffered},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"duration",{get:function(){return this._mediaElement.duration},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"volume",{get:function(){return this._mediaElement.volume},set:function(e){this._mediaElement.volume=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"muted",{get:function(){return this._mediaElement.muted},set:function(e){this._mediaElement.muted=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"currentTime",{get:function(){return this._mediaElement?this._mediaElement.currentTime:0},set:function(e){this._mediaElement?this._mediaElement.currentTime=e:this._pendingSeekTime=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"mediaInfo",{get:function(){var e={mimeType:(this._mediaElement instanceof HTMLAudioElement?"audio/":"video/")+this._mediaDataSource.type};return this._mediaElement&&(e.duration=Math.floor(1e3*this._mediaElement.duration),this._mediaElement instanceof HTMLVideoElement&&(e.width=this._mediaElement.videoWidth,e.height=this._mediaElement.videoHeight)),e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"statisticsInfo",{get:function(){var e={playerType:this._type,url:this._mediaDataSource.url};if(!(this._mediaElement instanceof HTMLVideoElement))return e;var t=!0,i=0,n=0;if(this._mediaElement.getVideoPlaybackQuality){var r=this._mediaElement.getVideoPlaybackQuality();i=r.totalVideoFrames,n=r.droppedVideoFrames}else null!=this._mediaElement.webkitDecodedFrameCount?(i=this._mediaElement.webkitDecodedFrameCount,n=this._mediaElement.webkitDroppedFrameCount):t=!1;return t&&(e.decodedFrames=i,e.droppedFrames=n),e},enumerable:!1,configurable:!0}),e.prototype._onvLoadedMetadata=function(e){null!=this._pendingSeekTime&&(this._mediaElement.currentTime=this._pendingSeekTime,this._pendingSeekTime=null),this._emitter.emit(f.MEDIA_INFO,this.mediaInfo)},e.prototype._reportStatisticsInfo=function(){this._emitter.emit(f.STATISTICS_INFO,this.statisticsInfo)},e}();n.a.install();var D={createPlayer:function(e,t){var i=e;if(null==i||"object"!=typeof i)throw new C.b("MediaDataSource must be an javascript object!");if(!i.hasOwnProperty("type"))throw new C.b("MediaDataSource must has type field to indicate video file type!");switch(i.type){case"mse":case"mpegts":case"m2ts":case"flv":return new x(i,t);default:return new R(i,t)}},isSupported:function(){return o.supportMSEH264Playback()},getFeatureList:function(){return o.getFeatureList()}};D.BaseLoader=u.a,D.LoaderStatus=u.c,D.LoaderErrors=u.b,D.Events=f,D.ErrorTypes=I,D.ErrorDetails=L,D.MSEPlayer=x,D.NativePlayer=R,D.LoggingControl=_.a,Object.defineProperty(D,"version",{enumerable:!0,get:function(){return"1.6.10"}}),t.default=D}])},"object"==typeof i&&"object"==typeof t?t.exports=r():"function"==typeof define&&define.amd?define([],r):"object"==typeof i?i.mpegts=r():n.mpegts=r()},{}],42:[function(e,t,i){var n=Math.pow(2,32);t.exports=function(e){var t=new DataView(e.buffer,e.byteOffset,e.byteLength),i={version:e[0],flags:new Uint8Array(e.subarray(1,4)),references:[],referenceId:t.getUint32(4),timescale:t.getUint32(8)},r=12;0===i.version?(i.earliestPresentationTime=t.getUint32(r),i.firstOffset=t.getUint32(r+4),r+=8):(i.earliestPresentationTime=t.getUint32(r)*n+t.getUint32(r+4),i.firstOffset=t.getUint32(r+8)*n+t.getUint32(r+12),r+=16),r+=2;var a=t.getUint16(r);for(r+=2;a>0;r+=12,a--)i.references.push({referenceType:(128&e[r])>>>7,referencedSize:2147483647&t.getUint32(r),subsegmentDuration:t.getUint32(r+4),startsWithSap:!!(128&e[r+8]),sapType:(112&e[r+8])>>>4,sapDeltaTime:268435455&t.getUint32(r+8)});return i}},{}],43:[function(e,t,i){var n,r,a,s,o,u,l;n=function(e){return 9e4*e},r=function(e,t){return e*t},a=function(e){return e/9e4},s=function(e,t){return e/t},o=function(e,t){return n(s(e,t))},u=function(e,t){return r(a(e),t)},l=function(e,t,i){return a(i?e:e-t)},t.exports={ONE_SECOND_IN_TS:9e4,secondsToVideoTs:n,secondsToAudioTs:r,videoTsToSeconds:a,audioTsToSeconds:s,audioTsToVideoTs:o,videoTsToAudioTs:u,metadataTsToSeconds:l}},{}],44:[function(e,t,i){var n,r,a=t.exports={};function s(){throw new Error("setTimeout has not been defined")}function o(){throw new Error("clearTimeout has not been defined")}function u(e){if(n===setTimeout)return setTimeout(e,0);if((n===s||!n)&&setTimeout)return n=setTimeout,setTimeout(e,0);try{return n(e,0)}catch(t){try{return n.call(null,e,0)}catch(t){return n.call(this,e,0)}}}!function(){try{n="function"==typeof setTimeout?setTimeout:s}catch(e){n=s}try{r="function"==typeof clearTimeout?clearTimeout:o}catch(e){r=o}}();var l,h=[],d=!1,c=-1;function f(){d&&l&&(d=!1,l.length?h=l.concat(h):c=-1,h.length&&p())}function p(){if(!d){var e=u(f);d=!0;for(var t=h.length;t;){for(l=h,h=[];++c1)for(var i=1;i + * Copyright Brightcove, Inc. + * Available under Apache License Version 2.0 + * + * + * Includes vtt.js + * Available under Apache License Version 2.0 + * + */ +"use strict";var n=e("global/window"),r=e("global/document"),a=e("@babel/runtime/helpers/extends"),s=e("@babel/runtime/helpers/assertThisInitialized"),o=e("@babel/runtime/helpers/inheritsLoose"),u=e("safe-json-parse/tuple"),l=e("keycode"),h=e("@videojs/xhr"),d=e("videojs-vtt.js"),c=e("@babel/runtime/helpers/construct"),f=e("@babel/runtime/helpers/inherits"),p=e("@videojs/vhs-utils/cjs/resolve-url.js"),m=e("m3u8-parser"),_=e("@videojs/vhs-utils/cjs/codecs.js"),g=e("@videojs/vhs-utils/cjs/media-types.js"),v=e("mpd-parser"),y=e("mux.js/lib/tools/parse-sidx"),b=e("@videojs/vhs-utils/cjs/id3-helpers"),S=e("@videojs/vhs-utils/cjs/containers"),T=e("@videojs/vhs-utils/cjs/byte-helpers"),E=e("mux.js/lib/utils/clock");function w(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}for(var A,C=w(n),k=w(r),P=w(a),I=w(s),L=w(o),x=w(u),R=w(l),D=w(h),O=w(d),U=w(c),M=w(f),F=w(p),B=w(y),N={},j=function(e,t){return N[e]=N[e]||[],t&&(N[e]=N[e].concat(t)),N[e]},V=function(e,t){var i=j(e).indexOf(t);return!(i<=-1)&&(N[e]=N[e].slice(),N[e].splice(i,1),!0)},H={prefixed:!0},z=[["requestFullscreen","exitFullscreen","fullscreenElement","fullscreenEnabled","fullscreenchange","fullscreenerror","fullscreen"],["webkitRequestFullscreen","webkitExitFullscreen","webkitFullscreenElement","webkitFullscreenEnabled","webkitfullscreenchange","webkitfullscreenerror","-webkit-full-screen"],["mozRequestFullScreen","mozCancelFullScreen","mozFullScreenElement","mozFullScreenEnabled","mozfullscreenchange","mozfullscreenerror","-moz-full-screen"],["msRequestFullscreen","msExitFullscreen","msFullscreenElement","msFullscreenEnabled","MSFullscreenChange","MSFullscreenError","-ms-fullscreen"]],G=z[0],W=0;W0?o:0)}if(C.default.console){var u=C.default.console[i];u||"debug"!==i||(u=C.default.console.info||C.default.console.log),u&&a&&s.test(i)&&u[Array.isArray(r)?"apply":"call"](C.default.console,r)}}}(t,r),r.createLogger=function(i){return e(t+": "+i)},r.levels={all:"debug|log|warn|error",off:"",debug:"debug|log|warn|error",info:"log|warn|error",warn:"warn|error",error:"error",DEFAULT:n},r.level=function(e){if("string"==typeof e){if(!r.levels.hasOwnProperty(e))throw new Error('"'+e+'" in not a valid log level');n=e}return n},(r.history=function(){return q?[].concat(q):[]}).filter=function(e){return(q||[]).filter((function(t){return new RegExp(".*"+e+".*").test(t[0])}))},r.history.clear=function(){q&&(q.length=0)},r.history.disable=function(){null!==q&&(q.length=0,q=null)},r.history.enable=function(){null===q&&(q=[])},r.error=function(){for(var e=arguments.length,t=new Array(e),r=0;r1?t-1:0),n=1;n=0)throw new Error("class has illegal whitespace characters")}function ke(){return k.default===C.default.document}function Pe(e){return ee(e)&&1===e.nodeType}function Ie(){try{return C.default.parent!==C.default.self}catch(e){return!0}}function Le(e){return function(t,i){if(!Ae(t))return k.default[e](null);Ae(i)&&(i=k.default.querySelector(i));var n=Pe(i)?i:k.default;return n[e]&&n[e](t)}}function xe(e,t,i,n){void 0===e&&(e="div"),void 0===t&&(t={}),void 0===i&&(i={});var r=k.default.createElement(e);return Object.getOwnPropertyNames(t).forEach((function(e){var i=t[e];-1!==e.indexOf("aria-")||"role"===e||"type"===e?(K.warn("Setting attributes in the second argument of createEl()\nhas been deprecated. Use the third argument instead.\ncreateEl(type, properties, attributes). Attempting to set "+e+" to "+i+"."),r.setAttribute(e,i)):"textContent"===e?Re(r,i):r[e]===i&&"tabIndex"!==e||(r[e]=i)})),Object.getOwnPropertyNames(i).forEach((function(e){r.setAttribute(e,i[e])})),n&&$e(r,n),r}function Re(e,t){return void 0===e.textContent?e.innerText=t:e.textContent=t,e}function De(e,t){t.firstChild?t.insertBefore(e,t.firstChild):t.appendChild(e)}function Oe(e,t){return Ce(t),e.classList?e.classList.contains(t):(i=t,new RegExp("(^|\\s)"+i+"($|\\s)")).test(e.className);var i}function Ue(e,t){return e.classList?e.classList.add(t):Oe(e,t)||(e.className=(e.className+" "+t).trim()),e}function Me(e,t){return e?(e.classList?e.classList.remove(t):(Ce(t),e.className=e.className.split(/\s+/).filter((function(e){return e!==t})).join(" ")),e):(K.warn("removeClass was called with an element that doesn't exist"),null)}function Fe(e,t,i){var n=Oe(e,t);if("function"==typeof i&&(i=i(e,t)),"boolean"!=typeof i&&(i=!n),i!==n)return i?Ue(e,t):Me(e,t),e}function Be(e,t){Object.getOwnPropertyNames(t).forEach((function(i){var n=t[i];null==n||!1===n?e.removeAttribute(i):e.setAttribute(i,!0===n?"":n)}))}function Ne(e){var t={},i=",autoplay,controls,playsinline,loop,muted,default,defaultMuted,";if(e&&e.attributes&&e.attributes.length>0)for(var n=e.attributes,r=n.length-1;r>=0;r--){var a=n[r].name,s=n[r].value;"boolean"!=typeof e[a]&&-1===i.indexOf(","+a+",")||(s=null!==s),t[a]=s}return t}function je(e,t){return e.getAttribute(t)}function Ve(e,t,i){e.setAttribute(t,i)}function He(e,t){e.removeAttribute(t)}function ze(){k.default.body.focus(),k.default.onselectstart=function(){return!1}}function Ge(){k.default.onselectstart=function(){return!0}}function We(e){if(e&&e.getBoundingClientRect&&e.parentNode){var t=e.getBoundingClientRect(),i={};return["bottom","height","left","right","top","width"].forEach((function(e){void 0!==t[e]&&(i[e]=t[e])})),i.height||(i.height=parseFloat(ie(e,"height"))),i.width||(i.width=parseFloat(ie(e,"width"))),i}}function Ye(e){if(!e||e&&!e.offsetParent)return{left:0,top:0,width:0,height:0};for(var t=e.offsetWidth,i=e.offsetHeight,n=0,r=0;e.offsetParent&&e!==k.default[H.fullscreenElement];)n+=e.offsetLeft,r+=e.offsetTop,e=e.offsetParent;return{left:n,top:r,width:t,height:i}}function qe(e,t){var i={x:0,y:0};if(Te)for(var n=e;n&&"html"!==n.nodeName.toLowerCase();){var r=ie(n,"transform");if(/^matrix/.test(r)){var a=r.slice(7,-1).split(/,\s/).map(Number);i.x+=a[4],i.y+=a[5]}else if(/^matrix3d/.test(r)){var s=r.slice(9,-1).split(/,\s/).map(Number);i.x+=s[12],i.y+=s[13]}n=n.parentNode}var o={},u=Ye(t.target),l=Ye(e),h=l.width,d=l.height,c=t.offsetY-(l.top-u.top),f=t.offsetX-(l.left-u.left);return t.changedTouches&&(f=t.changedTouches[0].pageX-l.left,c=t.changedTouches[0].pageY+l.top,Te&&(f-=i.x,c-=i.y)),o.y=1-Math.max(0,Math.min(1,c/d)),o.x=Math.max(0,Math.min(1,f/h)),o}function Ke(e){return ee(e)&&3===e.nodeType}function Xe(e){for(;e.firstChild;)e.removeChild(e.firstChild);return e}function Qe(e){return"function"==typeof e&&(e=e()),(Array.isArray(e)?e:[e]).map((function(e){return"function"==typeof e&&(e=e()),Pe(e)||Ke(e)?e:"string"==typeof e&&/\S/.test(e)?k.default.createTextNode(e):void 0})).filter((function(e){return e}))}function $e(e,t){return Qe(t).forEach((function(t){return e.appendChild(t)})),e}function Je(e,t){return $e(Xe(e),t)}function Ze(e){return void 0===e.button&&void 0===e.buttons||(0===e.button&&void 0===e.buttons||("mouseup"===e.type&&0===e.button&&0===e.buttons||0===e.button&&1===e.buttons))}var et,tt=Le("querySelector"),it=Le("querySelectorAll"),nt=Object.freeze({__proto__:null,isReal:ke,isEl:Pe,isInFrame:Ie,createEl:xe,textContent:Re,prependTo:De,hasClass:Oe,addClass:Ue,removeClass:Me,toggleClass:Fe,setAttributes:Be,getAttributes:Ne,getAttribute:je,setAttribute:Ve,removeAttribute:He,blockTextSelection:ze,unblockTextSelection:Ge,getBoundingClientRect:We,findPosition:Ye,getPointerPosition:qe,isTextNode:Ke,emptyEl:Xe,normalizeContent:Qe,appendContent:$e,insertContent:Je,isSingleLeftClick:Ze,$:tt,$$:it}),rt=!1,at=function(){if(!1!==et.options.autoSetup){var e=Array.prototype.slice.call(k.default.getElementsByTagName("video")),t=Array.prototype.slice.call(k.default.getElementsByTagName("audio")),i=Array.prototype.slice.call(k.default.getElementsByTagName("video-js")),n=e.concat(t,i);if(n&&n.length>0)for(var r=0,a=n.length;r-1&&(r={passive:!0}),e.addEventListener(t,n.dispatcher,r)}else e.attachEvent&&e.attachEvent("on"+t,n.dispatcher)}function bt(e,t,i){if(pt.has(e)){var n=pt.get(e);if(n.handlers){if(Array.isArray(t))return _t(bt,e,t,i);var r=function(e,t){n.handlers[t]=[],mt(e,t)};if(void 0!==t){var a=n.handlers[t];if(a)if(i){if(i.guid)for(var s=0;s=t&&(e.apply(void 0,arguments),i=n)}},Pt=function(){};Pt.prototype.allowedEvents_={},Pt.prototype.on=function(e,t){var i=this.addEventListener;this.addEventListener=function(){},yt(this,e,t),this.addEventListener=i},Pt.prototype.addEventListener=Pt.prototype.on,Pt.prototype.off=function(e,t){bt(this,e,t)},Pt.prototype.removeEventListener=Pt.prototype.off,Pt.prototype.one=function(e,t){var i=this.addEventListener;this.addEventListener=function(){},Tt(this,e,t),this.addEventListener=i},Pt.prototype.any=function(e,t){var i=this.addEventListener;this.addEventListener=function(){},Et(this,e,t),this.addEventListener=i},Pt.prototype.trigger=function(e){var t=e.type||e;"string"==typeof e&&(e={type:t}),e=gt(e),this.allowedEvents_[t]&&this["on"+t]&&this["on"+t](e),St(this,e)},Pt.prototype.dispatchEvent=Pt.prototype.trigger,Pt.prototype.queueTrigger=function(e){var t=this;wt||(wt=new Map);var i=e.type||e,n=wt.get(this);n||(n=new Map,wt.set(this,n));var r=n.get(i);n.delete(i),C.default.clearTimeout(r);var a=C.default.setTimeout((function(){0===n.size&&(n=null,wt.delete(t)),t.trigger(e)}),0);n.set(i,a)};var It=function(e){return"function"==typeof e.name?e.name():"string"==typeof e.name?e.name:e.name_?e.name_:e.constructor&&e.constructor.name?e.constructor.name:typeof e},Lt=function(e){return e instanceof Pt||!!e.eventBusEl_&&["on","one","off","trigger"].every((function(t){return"function"==typeof e[t]}))},xt=function(e){return"string"==typeof e&&/\S/.test(e)||Array.isArray(e)&&!!e.length},Rt=function(e,t,i){if(!e||!e.nodeName&&!Lt(e))throw new Error("Invalid target for "+It(t)+"#"+i+"; must be a DOM node or evented object.")},Dt=function(e,t,i){if(!xt(e))throw new Error("Invalid event type for "+It(t)+"#"+i+"; must be a non-empty string or array.")},Ot=function(e,t,i){if("function"!=typeof e)throw new Error("Invalid listener for "+It(t)+"#"+i+"; must be a function.")},Ut=function(e,t,i){var n,r,a,s=t.length<3||t[0]===e||t[0]===e.eventBusEl_;return s?(n=e.eventBusEl_,t.length>=3&&t.shift(),r=t[0],a=t[1]):(n=t[0],r=t[1],a=t[2]),Rt(n,e,i),Dt(r,e,i),Ot(a,e,i),{isTargetingSelf:s,target:n,type:r,listener:a=Ct(e,a)}},Mt=function(e,t,i,n){Rt(e,e,t),e.nodeName?At[t](e,i,n):e[t](i,n)},Ft={on:function(){for(var e=this,t=arguments.length,i=new Array(t),n=0;n=0;e--)this.children_[e].dispose&&this.children_[e].dispose();this.children_=null,this.childIndex_=null,this.childNameIndex_=null,this.parentComponent_=null,this.el_&&(this.el_.parentNode&&this.el_.parentNode.removeChild(this.el_),this.el_=null),this.player_=null}},t.isDisposed=function(){return Boolean(this.isDisposed_)},t.player=function(){return this.player_},t.options=function(e){return e?(this.options_=zt(this.options_,e),this.options_):this.options_},t.el=function(){return this.el_},t.createEl=function(e,t,i){return xe(e,t,i)},t.localize=function(e,t,i){void 0===i&&(i=e);var n=this.player_.language&&this.player_.language(),r=this.player_.languages&&this.player_.languages(),a=r&&r[n],s=n&&n.split("-")[0],o=r&&r[s],u=i;return a&&a[e]?u=a[e]:o&&o[e]&&(u=o[e]),t&&(u=u.replace(/\{(\d+)\}/g,(function(e,i){var n=t[i-1],r=n;return void 0===n&&(r=e),r}))),u},t.handleLanguagechange=function(){},t.contentEl=function(){return this.contentEl_||this.el_},t.id=function(){return this.id_},t.name=function(){return this.name_},t.children=function(){return this.children_},t.getChildById=function(e){return this.childIndex_[e]},t.getChild=function(e){if(e)return this.childNameIndex_[e]},t.getDescendant=function(){for(var e=arguments.length,t=new Array(e),i=0;i=0;i--)if(this.children_[i]===e){t=!0,this.children_.splice(i,1);break}if(t){e.parentComponent_=null,this.childIndex_[e.id()]=null,this.childNameIndex_[Ht(e.name())]=null,this.childNameIndex_[Vt(e.name())]=null;var n=e.el();n&&n.parentNode===this.contentEl()&&this.contentEl().removeChild(e.el())}}},t.initChildren=function(){var t=this,i=this.options_.children;if(i){var n,r=this.options_,a=e.getComponent("Tech");(n=Array.isArray(i)?i:Object.keys(i)).concat(Object.keys(this.options_).filter((function(e){return!n.some((function(t){return"string"==typeof t?e===t:e===t.name}))}))).map((function(e){var n,r;return"string"==typeof e?r=i[n=e]||t.options_[n]||{}:(n=e.name,r=e),{name:n,opts:r}})).filter((function(t){var i=e.getComponent(t.opts.componentClass||Ht(t.name));return i&&!a.isTech(i)})).forEach((function(e){var i=e.name,n=e.opts;if(void 0!==r[i]&&(n=r[i]),!1!==n){!0===n&&(n={}),n.playerOptions=t.options_.playerOptions;var a=t.addChild(i,n);a&&(t[i]=a)}}))}},t.buildCSSClass=function(){return""},t.ready=function(e,t){if(void 0===t&&(t=!1),e)return this.isReady_?void(t?e.call(this):this.setTimeout(e,1)):(this.readyQueue_=this.readyQueue_||[],void this.readyQueue_.push(e))},t.triggerReady=function(){this.isReady_=!0,this.setTimeout((function(){var e=this.readyQueue_;this.readyQueue_=[],e&&e.length>0&&e.forEach((function(e){e.call(this)}),this),this.trigger("ready")}),1)},t.$=function(e,t){return tt(e,t||this.contentEl())},t.$$=function(e,t){return it(e,t||this.contentEl())},t.hasClass=function(e){return Oe(this.el_,e)},t.addClass=function(e){Ue(this.el_,e)},t.removeClass=function(e){Me(this.el_,e)},t.toggleClass=function(e,t){Fe(this.el_,e,t)},t.show=function(){this.removeClass("vjs-hidden")},t.hide=function(){this.addClass("vjs-hidden")},t.lockShowing=function(){this.addClass("vjs-lock-showing")},t.unlockShowing=function(){this.removeClass("vjs-lock-showing")},t.getAttribute=function(e){return je(this.el_,e)},t.setAttribute=function(e,t){Ve(this.el_,e,t)},t.removeAttribute=function(e){He(this.el_,e)},t.width=function(e,t){return this.dimension("width",e,t)},t.height=function(e,t){return this.dimension("height",e,t)},t.dimensions=function(e,t){this.width(e,!0),this.height(t)},t.dimension=function(e,t,i){if(void 0!==t)return null!==t&&t==t||(t=0),-1!==(""+t).indexOf("%")||-1!==(""+t).indexOf("px")?this.el_.style[e]=t:this.el_.style[e]="auto"===t?"":t+"px",void(i||this.trigger("componentresize"));if(!this.el_)return 0;var n=this.el_.style[e],r=n.indexOf("px");return-1!==r?parseInt(n.slice(0,r),10):parseInt(this.el_["offset"+Ht(e)],10)},t.currentDimension=function(e){var t=0;if("width"!==e&&"height"!==e)throw new Error("currentDimension only accepts width or height value");if(t=ie(this.el_,e),0===(t=parseFloat(t))||isNaN(t)){var i="offset"+Ht(e);t=this.el_[i]}return t},t.currentDimensions=function(){return{width:this.currentDimension("width"),height:this.currentDimension("height")}},t.currentWidth=function(){return this.currentDimension("width")},t.currentHeight=function(){return this.currentDimension("height")},t.focus=function(){this.el_.focus()},t.blur=function(){this.el_.blur()},t.handleKeyDown=function(e){this.player_&&(e.stopPropagation(),this.player_.handleKeyDown(e))},t.handleKeyPress=function(e){this.handleKeyDown(e)},t.emitTapEvents=function(){var e,t=0,i=null;this.on("touchstart",(function(n){1===n.touches.length&&(i={pageX:n.touches[0].pageX,pageY:n.touches[0].pageY},t=C.default.performance.now(),e=!0)})),this.on("touchmove",(function(t){if(t.touches.length>1)e=!1;else if(i){var n=t.touches[0].pageX-i.pageX,r=t.touches[0].pageY-i.pageY;Math.sqrt(n*n+r*r)>10&&(e=!1)}}));var n=function(){e=!1};this.on("touchleave",n),this.on("touchcancel",n),this.on("touchend",(function(n){(i=null,!0===e)&&(C.default.performance.now()-t<200&&(n.preventDefault(),this.trigger("tap")))}))},t.enableTouchActivity=function(){if(this.player()&&this.player().reportUserActivity){var e,t=Ct(this.player(),this.player().reportUserActivity);this.on("touchstart",(function(){t(),this.clearInterval(e),e=this.setInterval(t,250)}));var i=function(i){t(),this.clearInterval(e)};this.on("touchmove",t),this.on("touchend",i),this.on("touchcancel",i)}},t.setTimeout=function(e,t){var i,n=this;return e=Ct(this,e),this.clearTimersOnDispose_(),i=C.default.setTimeout((function(){n.setTimeoutIds_.has(i)&&n.setTimeoutIds_.delete(i),e()}),t),this.setTimeoutIds_.add(i),i},t.clearTimeout=function(e){return this.setTimeoutIds_.has(e)&&(this.setTimeoutIds_.delete(e),C.default.clearTimeout(e)),e},t.setInterval=function(e,t){e=Ct(this,e),this.clearTimersOnDispose_();var i=C.default.setInterval(e,t);return this.setIntervalIds_.add(i),i},t.clearInterval=function(e){return this.setIntervalIds_.has(e)&&(this.setIntervalIds_.delete(e),C.default.clearInterval(e)),e},t.requestAnimationFrame=function(e){var t,i=this;return this.supportsRaf_?(this.clearTimersOnDispose_(),e=Ct(this,e),t=C.default.requestAnimationFrame((function(){i.rafIds_.has(t)&&i.rafIds_.delete(t),e()})),this.rafIds_.add(t),t):this.setTimeout(e,1e3/60)},t.requestNamedAnimationFrame=function(e,t){var i=this;if(!this.namedRafs_.has(e)){this.clearTimersOnDispose_(),t=Ct(this,t);var n=this.requestAnimationFrame((function(){t(),i.namedRafs_.has(e)&&i.namedRafs_.delete(e)}));return this.namedRafs_.set(e,n),e}},t.cancelNamedAnimationFrame=function(e){this.namedRafs_.has(e)&&(this.cancelAnimationFrame(this.namedRafs_.get(e)),this.namedRafs_.delete(e))},t.cancelAnimationFrame=function(e){return this.supportsRaf_?(this.rafIds_.has(e)&&(this.rafIds_.delete(e),C.default.cancelAnimationFrame(e)),e):this.clearTimeout(e)},t.clearTimersOnDispose_=function(){var e=this;this.clearingTimersOnDispose_||(this.clearingTimersOnDispose_=!0,this.one("dispose",(function(){[["namedRafs_","cancelNamedAnimationFrame"],["rafIds_","cancelAnimationFrame"],["setTimeoutIds_","clearTimeout"],["setIntervalIds_","clearInterval"]].forEach((function(t){var i=t[0],n=t[1];e[i].forEach((function(t,i){return e[n](i)}))})),e.clearingTimersOnDispose_=!1})))},e.registerComponent=function(t,i){if("string"!=typeof t||!t)throw new Error('Illegal component name, "'+t+'"; must be a non-empty string.');var n,r=e.getComponent("Tech"),a=r&&r.isTech(i),s=e===i||e.prototype.isPrototypeOf(i.prototype);if(a||!s)throw n=a?"techs must be registered using Tech.registerTech()":"must be a Component subclass",new Error('Illegal component, "'+t+'"; '+n+".");t=Ht(t),e.components_||(e.components_={});var o=e.getComponent("Player");if("Player"===t&&o&&o.players){var u=o.players,l=Object.keys(u);if(u&&l.length>0&&l.map((function(e){return u[e]})).every(Boolean))throw new Error("Can not register Player component after player has been created.")}return e.components_[t]=i,e.components_[Vt(t)]=i,i},e.getComponent=function(t){if(t&&e.components_)return e.components_[t]},e}();function Xt(e,t,i,n){return function(e,t,i){if("number"!=typeof t||t<0||t>i)throw new Error("Failed to execute '"+e+"' on 'TimeRanges': The index provided ("+t+") is non-numeric or out of bounds (0-"+i+").")}(e,n,i.length-1),i[n][t]}function Qt(e){var t;return t=void 0===e||0===e.length?{length:0,start:function(){throw new Error("This TimeRanges object is empty")},end:function(){throw new Error("This TimeRanges object is empty")}}:{length:e.length,start:Xt.bind(null,"start",0,e),end:Xt.bind(null,"end",1,e)},C.default.Symbol&&C.default.Symbol.iterator&&(t[C.default.Symbol.iterator]=function(){return(e||[]).values()}),t}function $t(e,t){return Array.isArray(e)?Qt(e):void 0===e||void 0===t?Qt():Qt([[e,t]])}function Jt(e,t){var i,n,r=0;if(!t)return 0;e&&e.length||(e=$t(0,0));for(var a=0;at&&(n=t),r+=n-i;return r/t}function Zt(e){if(e instanceof Zt)return e;"number"==typeof e?this.code=e:"string"==typeof e?this.message=e:ee(e)&&("number"==typeof e.code&&(this.code=e.code),Z(this,e)),this.message||(this.message=Zt.defaultMessages[this.code]||"")}Kt.prototype.supportsRaf_="function"==typeof C.default.requestAnimationFrame&&"function"==typeof C.default.cancelAnimationFrame,Kt.registerComponent("Component",Kt),Zt.prototype.code=0,Zt.prototype.message="",Zt.prototype.status=null,Zt.errorTypes=["MEDIA_ERR_CUSTOM","MEDIA_ERR_ABORTED","MEDIA_ERR_NETWORK","MEDIA_ERR_DECODE","MEDIA_ERR_SRC_NOT_SUPPORTED","MEDIA_ERR_ENCRYPTED"],Zt.defaultMessages={1:"You aborted the media playback",2:"A network error caused the media download to fail part-way.",3:"The media playback was aborted due to a corruption problem or because the media used features your browser did not support.",4:"The media could not be loaded, either because the server or network failed or because the format is not supported.",5:"The media is encrypted and we do not have the keys to decrypt it."};for(var ei=0;ei=0;n--)if(t[n].enabled){li(t,t[n]);break}return(i=e.call(this,t)||this).changing_=!1,i}L.default(t,e);var i=t.prototype;return i.addTrack=function(t){var i=this;t.enabled&&li(this,t),e.prototype.addTrack.call(this,t),t.addEventListener&&(t.enabledChange_=function(){i.changing_||(i.changing_=!0,li(i,t),i.changing_=!1,i.trigger("change"))},t.addEventListener("enabledchange",t.enabledChange_))},i.removeTrack=function(t){e.prototype.removeTrack.call(this,t),t.removeEventListener&&t.enabledChange_&&(t.removeEventListener("enabledchange",t.enabledChange_),t.enabledChange_=null)},t}(oi),di=function(e,t){for(var i=0;i=0;n--)if(t[n].selected){di(t,t[n]);break}return(i=e.call(this,t)||this).changing_=!1,Object.defineProperty(I.default(i),"selectedIndex",{get:function(){for(var e=0;e0&&(C.default.console&&C.default.console.groupCollapsed&&C.default.console.groupCollapsed("Text Track parsing errors for "+t.src),n.forEach((function(e){return K.error(e)})),C.default.console&&C.default.console.groupEnd&&C.default.console.groupEnd()),i.flush()},ki=function(e,t){var i={uri:e},n=wi(e);n&&(i.cors=n);var r="use-credentials"===t.tech_.crossOrigin();r&&(i.withCredentials=r),D.default(i,Ct(this,(function(e,i,n){if(e)return K.error(e,i);t.loaded_=!0,"function"!=typeof C.default.WebVTT?t.tech_&&t.tech_.any(["vttjsloaded","vttjserror"],(function(e){if("vttjserror"!==e.type)return Ci(n,t);K.error("vttjs failed to load, stopping trying to process "+t.src)})):Ci(n,t)})))},Pi=function(e){function t(t){var i;if(void 0===t&&(t={}),!t.tech)throw new Error("A tech was not provided.");var n=zt(t,{kind:vi[t.kind]||"subtitles",language:t.language||t.srclang||""}),r=yi[n.mode]||"disabled",a=n.default;"metadata"!==n.kind&&"chapters"!==n.kind||(r="hidden"),(i=e.call(this,n)||this).tech_=n.tech,i.cues_=[],i.activeCues_=[],i.preload_=!1!==i.tech_.preloadTextTracks;var s=new mi(i.cues_),o=new mi(i.activeCues_),u=!1,l=Ct(I.default(i),(function(){this.tech_.isReady_&&!this.tech_.isDisposed()&&(this.activeCues=this.activeCues,u&&(this.trigger("cuechange"),u=!1))}));return i.tech_.one("dispose",(function(){i.tech_.off("timeupdate",l)})),"disabled"!==r&&i.tech_.on("timeupdate",l),Object.defineProperties(I.default(i),{default:{get:function(){return a},set:function(){}},mode:{get:function(){return r},set:function(e){yi[e]&&r!==e&&(r=e,this.preload_||"disabled"===r||0!==this.cues.length||ki(this.src,this),this.tech_.off("timeupdate",l),"disabled"!==r&&this.tech_.on("timeupdate",l),this.trigger("modechange"))}},cues:{get:function(){return this.loaded_?s:null},set:function(){}},activeCues:{get:function(){if(!this.loaded_)return null;if(0===this.cues.length)return o;for(var e=this.tech_.currentTime(),t=[],i=0,n=this.cues.length;i=e||r.startTime===r.endTime&&r.startTime<=e&&r.startTime+.5>=e)&&t.push(r)}if(u=!1,t.length!==this.activeCues_.length)u=!0;else for(var a=0;a0)return void this.trigger("vttjsloaded");var t=k.default.createElement("script");t.src=this.options_["vtt.js"]||"https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js",t.onload=function(){e.trigger("vttjsloaded")},t.onerror=function(){e.trigger("vttjserror")},this.on("dispose",(function(){t.onload=null,t.onerror=null})),C.default.WebVTT=!0,this.el().parentNode.appendChild(t)}else this.ready(this.addWebVttScript_)},i.emulateTextTracks=function(){var e=this,t=this.textTracks(),i=this.remoteTextTracks(),n=function(e){return t.addTrack(e.track)},r=function(e){return t.removeTrack(e.track)};i.on("addtrack",n),i.on("removetrack",r),this.addWebVttScript_();var a=function(){return e.trigger("texttrackchange")},s=function(){a();for(var e=0;e=0;r--){var a=e[r];a[t]&&a[t](n,i)}}(e,i,o,s),o}var Vi={buffered:1,currentTime:1,duration:1,muted:1,played:1,paused:1,seekable:1,volume:1,ended:1},Hi={setCurrentTime:1,setMuted:1,setVolume:1},zi={play:1,pause:1};function Gi(e){return function(t,i){return t===Bi?Bi:i[e]?i[e](t):t}}var Wi={opus:"video/ogg",ogv:"video/ogg",mp4:"video/mp4",mov:"video/mp4",m4v:"video/mp4",mkv:"video/x-matroska",m4a:"audio/mp4",mp3:"audio/mpeg",aac:"audio/aac",caf:"audio/x-caf",flac:"audio/flac",oga:"audio/ogg",wav:"audio/wav",m3u8:"application/x-mpegURL",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",png:"image/png",svg:"image/svg+xml",webp:"image/webp"},Yi=function(e){void 0===e&&(e="");var t=Ei(e);return Wi[t.toLowerCase()]||""};function qi(e){if(!e.type){var t=Yi(e.src);t&&(e.type=t)}return e}var Ki=function(e){function t(t,i,n){var r,a=zt({createEl:!1},i);if(r=e.call(this,t,a,n)||this,i.playerOptions.sources&&0!==i.playerOptions.sources.length)t.src(i.playerOptions.sources);else for(var s=0,o=i.playerOptions.techOrder;s0;!this.player_.tech(!0)||(_e||fe)&&t||this.player_.tech(!0).focus(),this.player_.paused()?ii(this.player_.play()):this.player_.pause()}},t}(Xi);Kt.registerComponent("PosterImage",Qi);var $i={monospace:"monospace",sansSerif:"sans-serif",serif:"serif",monospaceSansSerif:'"Andale Mono", "Lucida Console", monospace',monospaceSerif:'"Courier New", monospace',proportionalSansSerif:"sans-serif",proportionalSerif:"serif",casual:'"Comic Sans MS", Impact, fantasy',script:'"Monotype Corsiva", cursive',smallcaps:'"Andale Mono", "Lucida Console", monospace, sans-serif'};function Ji(e,t){var i;if(4===e.length)i=e[1]+e[1]+e[2]+e[2]+e[3]+e[3];else{if(7!==e.length)throw new Error("Invalid color code provided, "+e+"; must be formatted as e.g. #f0e or #f604e2.");i=e.slice(1)}return"rgba("+parseInt(i.slice(0,2),16)+","+parseInt(i.slice(2,4),16)+","+parseInt(i.slice(4,6),16)+","+t+")"}function Zi(e,t,i){try{e.style[t]=i}catch(e){return}}var en=function(e){function t(t,i,n){var r;r=e.call(this,t,i,n)||this;var a=function(e){return r.updateDisplay(e)};return t.on("loadstart",(function(e){return r.toggleDisplay(e)})),t.on("texttrackchange",a),t.on("loadedmetadata",(function(e){return r.preselectTrack(e)})),t.ready(Ct(I.default(r),(function(){if(t.tech_&&t.tech_.featuresNativeTextTracks)this.hide();else{t.on("fullscreenchange",a),t.on("playerresize",a),C.default.addEventListener("orientationchange",a),t.on("dispose",(function(){return C.default.removeEventListener("orientationchange",a)}));for(var e=this.options_.playerOptions.tracks||[],i=0;i0;return ii(t),void(!this.player_.tech(!0)||(_e||fe)&&i||this.player_.tech(!0).focus())}var n=this.player_.getChild("controlBar"),r=n&&n.getChild("playToggle");if(r){var a=function(){return r.focus()};ti(t)?t.then(a,(function(){})):this.setTimeout(a,1)}else this.player_.tech(!0).focus()},i.handleKeyDown=function(t){this.mouseused_=!1,e.prototype.handleKeyDown.call(this,t)},i.handleMouseDown=function(e){this.mouseused_=!0},t}(nn);rn.prototype.controlText_="Play Video",Kt.registerComponent("BigPlayButton",rn);var an=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).controlText(i&&i.controlText||n.localize("Close")),n}L.default(t,e);var i=t.prototype;return i.buildCSSClass=function(){return"vjs-close-button "+e.prototype.buildCSSClass.call(this)},i.handleClick=function(e){this.trigger({type:"close",bubbles:!1})},i.handleKeyDown=function(t){R.default.isEventKey(t,"Esc")?(t.preventDefault(),t.stopPropagation(),this.trigger("click")):e.prototype.handleKeyDown.call(this,t)},t}(nn);Kt.registerComponent("CloseButton",an);var sn=function(e){function t(t,i){var n;return void 0===i&&(i={}),n=e.call(this,t,i)||this,i.replay=void 0===i.replay||i.replay,n.on(t,"play",(function(e){return n.handlePlay(e)})),n.on(t,"pause",(function(e){return n.handlePause(e)})),i.replay&&n.on(t,"ended",(function(e){return n.handleEnded(e)})),n}L.default(t,e);var i=t.prototype;return i.buildCSSClass=function(){return"vjs-play-control "+e.prototype.buildCSSClass.call(this)},i.handleClick=function(e){this.player_.paused()?ii(this.player_.play()):this.player_.pause()},i.handleSeeked=function(e){this.removeClass("vjs-ended"),this.player_.paused()?this.handlePause(e):this.handlePlay(e)},i.handlePlay=function(e){this.removeClass("vjs-ended"),this.removeClass("vjs-paused"),this.addClass("vjs-playing"),this.controlText("Pause")},i.handlePause=function(e){this.removeClass("vjs-playing"),this.addClass("vjs-paused"),this.controlText("Play")},i.handleEnded=function(e){var t=this;this.removeClass("vjs-playing"),this.addClass("vjs-ended"),this.controlText("Replay"),this.one(this.player_,"seeked",(function(e){return t.handleSeeked(e)}))},t}(nn);sn.prototype.controlText_="Play",Kt.registerComponent("PlayToggle",sn);var on=function(e,t){e=e<0?0:e;var i=Math.floor(e%60),n=Math.floor(e/60%60),r=Math.floor(e/3600),a=Math.floor(t/60%60),s=Math.floor(t/3600);return(isNaN(e)||e===1/0)&&(r=n=i="-"),(r=r>0||s>0?r+":":"")+(n=((r||a>=10)&&n<10?"0"+n:n)+":")+(i=i<10?"0"+i:i)},un=on;function ln(e,t){return void 0===t&&(t=e),un(e,t)}var hn=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).on(t,["timeupdate","ended"],(function(e){return n.updateContent(e)})),n.updateTextNode_(),n}L.default(t,e);var i=t.prototype;return i.createEl=function(){var t=this.buildCSSClass(),i=e.prototype.createEl.call(this,"div",{className:t+" vjs-time-control vjs-control"}),n=xe("span",{className:"vjs-control-text",textContent:this.localize(this.labelText_)+" "},{role:"presentation"});return i.appendChild(n),this.contentEl_=xe("span",{className:t+"-display"},{"aria-live":"off",role:"presentation"}),i.appendChild(this.contentEl_),i},i.dispose=function(){this.contentEl_=null,this.textNode_=null,e.prototype.dispose.call(this)},i.updateTextNode_=function(e){var t=this;void 0===e&&(e=0),e=ln(e),this.formattedTime_!==e&&(this.formattedTime_=e,this.requestNamedAnimationFrame("TimeDisplay#updateTextNode_",(function(){if(t.contentEl_){var e=t.textNode_;e&&t.contentEl_.firstChild!==e&&(e=null,K.warn("TimeDisplay#updateTextnode_: Prevented replacement of text node element since it was no longer a child of this node. Appending a new node instead.")),t.textNode_=k.default.createTextNode(t.formattedTime_),t.textNode_&&(e?t.contentEl_.replaceChild(t.textNode_,e):t.contentEl_.appendChild(t.textNode_))}})))},i.updateContent=function(e){},t}(Kt);hn.prototype.labelText_="Time",hn.prototype.controlText_="Time",Kt.registerComponent("TimeDisplay",hn);var dn=function(e){function t(){return e.apply(this,arguments)||this}L.default(t,e);var i=t.prototype;return i.buildCSSClass=function(){return"vjs-current-time"},i.updateContent=function(e){var t;t=this.player_.ended()?this.player_.duration():this.player_.scrubbing()?this.player_.getCache().currentTime:this.player_.currentTime(),this.updateTextNode_(t)},t}(hn);dn.prototype.labelText_="Current Time",dn.prototype.controlText_="Current Time",Kt.registerComponent("CurrentTimeDisplay",dn);var cn=function(e){function t(t,i){var n,r=function(e){return n.updateContent(e)};return(n=e.call(this,t,i)||this).on(t,"durationchange",r),n.on(t,"loadstart",r),n.on(t,"loadedmetadata",r),n}L.default(t,e);var i=t.prototype;return i.buildCSSClass=function(){return"vjs-duration"},i.updateContent=function(e){var t=this.player_.duration();this.updateTextNode_(t)},t}(hn);cn.prototype.labelText_="Duration",cn.prototype.controlText_="Duration",Kt.registerComponent("DurationDisplay",cn);var fn=function(e){function t(){return e.apply(this,arguments)||this}return L.default(t,e),t.prototype.createEl=function(){var t=e.prototype.createEl.call(this,"div",{className:"vjs-time-control vjs-time-divider"},{"aria-hidden":!0}),i=e.prototype.createEl.call(this,"div"),n=e.prototype.createEl.call(this,"span",{textContent:"/"});return i.appendChild(n),t.appendChild(i),t},t}(Kt);Kt.registerComponent("TimeDivider",fn);var pn=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).on(t,"durationchange",(function(e){return n.updateContent(e)})),n}L.default(t,e);var i=t.prototype;return i.buildCSSClass=function(){return"vjs-remaining-time"},i.createEl=function(){var t=e.prototype.createEl.call(this);return t.insertBefore(xe("span",{},{"aria-hidden":!0},"-"),this.contentEl_),t},i.updateContent=function(e){var t;"number"==typeof this.player_.duration()&&(t=this.player_.ended()?0:this.player_.remainingTimeDisplay?this.player_.remainingTimeDisplay():this.player_.remainingTime(),this.updateTextNode_(t))},t}(hn);pn.prototype.labelText_="Remaining Time",pn.prototype.controlText_="Remaining Time",Kt.registerComponent("RemainingTimeDisplay",pn);var mn=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).updateShowing(),n.on(n.player(),"durationchange",(function(e){return n.updateShowing(e)})),n}L.default(t,e);var i=t.prototype;return i.createEl=function(){var t=e.prototype.createEl.call(this,"div",{className:"vjs-live-control vjs-control"});return this.contentEl_=xe("div",{className:"vjs-live-display"},{"aria-live":"off"}),this.contentEl_.appendChild(xe("span",{className:"vjs-control-text",textContent:this.localize("Stream Type")+" "})),this.contentEl_.appendChild(k.default.createTextNode(this.localize("LIVE"))),t.appendChild(this.contentEl_),t},i.dispose=function(){this.contentEl_=null,e.prototype.dispose.call(this)},i.updateShowing=function(e){this.player().duration()===1/0?this.show():this.hide()},t}(Kt);Kt.registerComponent("LiveDisplay",mn);var _n=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).updateLiveEdgeStatus(),n.player_.liveTracker&&(n.updateLiveEdgeStatusHandler_=function(e){return n.updateLiveEdgeStatus(e)},n.on(n.player_.liveTracker,"liveedgechange",n.updateLiveEdgeStatusHandler_)),n}L.default(t,e);var i=t.prototype;return i.createEl=function(){var t=e.prototype.createEl.call(this,"button",{className:"vjs-seek-to-live-control vjs-control"});return this.textEl_=xe("span",{className:"vjs-seek-to-live-text",textContent:this.localize("LIVE")},{"aria-hidden":"true"}),t.appendChild(this.textEl_),t},i.updateLiveEdgeStatus=function(){!this.player_.liveTracker||this.player_.liveTracker.atLiveEdge()?(this.setAttribute("aria-disabled",!0),this.addClass("vjs-at-live-edge"),this.controlText("Seek to live, currently playing live")):(this.setAttribute("aria-disabled",!1),this.removeClass("vjs-at-live-edge"),this.controlText("Seek to live, currently behind live"))},i.handleClick=function(){this.player_.liveTracker.seekToLiveEdge()},i.dispose=function(){this.player_.liveTracker&&this.off(this.player_.liveTracker,"liveedgechange",this.updateLiveEdgeStatusHandler_),this.textEl_=null,e.prototype.dispose.call(this)},t}(nn);_n.prototype.controlText_="Seek to live, currently playing live",Kt.registerComponent("SeekToLive",_n);var gn=function(e,t,i){return e=Number(e),Math.min(i,Math.max(t,isNaN(e)?t:e))},vn=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).handleMouseDown_=function(e){return n.handleMouseDown(e)},n.handleMouseUp_=function(e){return n.handleMouseUp(e)},n.handleKeyDown_=function(e){return n.handleKeyDown(e)},n.handleClick_=function(e){return n.handleClick(e)},n.handleMouseMove_=function(e){return n.handleMouseMove(e)},n.update_=function(e){return n.update(e)},n.bar=n.getChild(n.options_.barName),n.vertical(!!n.options_.vertical),n.enable(),n}L.default(t,e);var i=t.prototype;return i.enabled=function(){return this.enabled_},i.enable=function(){this.enabled()||(this.on("mousedown",this.handleMouseDown_),this.on("touchstart",this.handleMouseDown_),this.on("keydown",this.handleKeyDown_),this.on("click",this.handleClick_),this.on(this.player_,"controlsvisible",this.update),this.playerEvent&&this.on(this.player_,this.playerEvent,this.update),this.removeClass("disabled"),this.setAttribute("tabindex",0),this.enabled_=!0)},i.disable=function(){if(this.enabled()){var e=this.bar.el_.ownerDocument;this.off("mousedown",this.handleMouseDown_),this.off("touchstart",this.handleMouseDown_),this.off("keydown",this.handleKeyDown_),this.off("click",this.handleClick_),this.off(this.player_,"controlsvisible",this.update_),this.off(e,"mousemove",this.handleMouseMove_),this.off(e,"mouseup",this.handleMouseUp_),this.off(e,"touchmove",this.handleMouseMove_),this.off(e,"touchend",this.handleMouseUp_),this.removeAttribute("tabindex"),this.addClass("disabled"),this.playerEvent&&this.off(this.player_,this.playerEvent,this.update),this.enabled_=!1}},i.createEl=function(t,i,n){return void 0===i&&(i={}),void 0===n&&(n={}),i.className=i.className+" vjs-slider",i=Z({tabIndex:0},i),n=Z({role:"slider","aria-valuenow":0,"aria-valuemin":0,"aria-valuemax":100,tabIndex:0},n),e.prototype.createEl.call(this,t,i,n)},i.handleMouseDown=function(e){var t=this.bar.el_.ownerDocument;"mousedown"===e.type&&e.preventDefault(),"touchstart"!==e.type||pe||e.preventDefault(),ze(),this.addClass("vjs-sliding"),this.trigger("slideractive"),this.on(t,"mousemove",this.handleMouseMove_),this.on(t,"mouseup",this.handleMouseUp_),this.on(t,"touchmove",this.handleMouseMove_),this.on(t,"touchend",this.handleMouseUp_),this.handleMouseMove(e)},i.handleMouseMove=function(e){},i.handleMouseUp=function(){var e=this.bar.el_.ownerDocument;Ge(),this.removeClass("vjs-sliding"),this.trigger("sliderinactive"),this.off(e,"mousemove",this.handleMouseMove_),this.off(e,"mouseup",this.handleMouseUp_),this.off(e,"touchmove",this.handleMouseMove_),this.off(e,"touchend",this.handleMouseUp_),this.update()},i.update=function(){var e=this;if(this.el_&&this.bar){var t=this.getProgress();return t===this.progress_||(this.progress_=t,this.requestNamedAnimationFrame("Slider#update",(function(){var i=e.vertical()?"height":"width";e.bar.el().style[i]=(100*t).toFixed(2)+"%"}))),t}},i.getProgress=function(){return Number(gn(this.getPercent(),0,1).toFixed(4))},i.calculateDistance=function(e){var t=qe(this.el_,e);return this.vertical()?t.y:t.x},i.handleKeyDown=function(t){R.default.isEventKey(t,"Left")||R.default.isEventKey(t,"Down")?(t.preventDefault(),t.stopPropagation(),this.stepBack()):R.default.isEventKey(t,"Right")||R.default.isEventKey(t,"Up")?(t.preventDefault(),t.stopPropagation(),this.stepForward()):e.prototype.handleKeyDown.call(this,t)},i.handleClick=function(e){e.stopPropagation(),e.preventDefault()},i.vertical=function(e){if(void 0===e)return this.vertical_||!1;this.vertical_=!!e,this.vertical_?this.addClass("vjs-slider-vertical"):this.addClass("vjs-slider-horizontal")},t}(Kt);Kt.registerComponent("Slider",vn);var yn=function(e,t){return gn(e/t*100,0,100).toFixed(2)+"%"},bn=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).partEls_=[],n.on(t,"progress",(function(e){return n.update(e)})),n}L.default(t,e);var i=t.prototype;return i.createEl=function(){var t=e.prototype.createEl.call(this,"div",{className:"vjs-load-progress"}),i=xe("span",{className:"vjs-control-text"}),n=xe("span",{textContent:this.localize("Loaded")}),r=k.default.createTextNode(": ");return this.percentageEl_=xe("span",{className:"vjs-control-text-loaded-percentage",textContent:"0%"}),t.appendChild(i),i.appendChild(n),i.appendChild(r),i.appendChild(this.percentageEl_),t},i.dispose=function(){this.partEls_=null,this.percentageEl_=null,e.prototype.dispose.call(this)},i.update=function(e){var t=this;this.requestNamedAnimationFrame("LoadProgressBar#update",(function(){var e=t.player_.liveTracker,i=t.player_.buffered(),n=e&&e.isLive()?e.seekableEnd():t.player_.duration(),r=t.player_.bufferedEnd(),a=t.partEls_,s=yn(r,n);t.percent_!==s&&(t.el_.style.width=s,Re(t.percentageEl_,s),t.percent_=s);for(var o=0;oi.length;d--)t.el_.removeChild(a[d-1]);a.length=i.length}))},t}(Kt);Kt.registerComponent("LoadProgressBar",bn);var Sn=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).update=kt(Ct(I.default(n),n.update),30),n}L.default(t,e);var i=t.prototype;return i.createEl=function(){return e.prototype.createEl.call(this,"div",{className:"vjs-time-tooltip"},{"aria-hidden":"true"})},i.update=function(e,t,i){var n=Ye(this.el_),r=We(this.player_.el()),a=e.width*t;if(r&&n){var s=e.left-r.left+a,o=e.width-a+(r.right-e.right),u=n.width/2;sn.width&&(u=n.width),u=Math.round(u),this.el_.style.right="-"+u+"px",this.write(i)}},i.write=function(e){Re(this.el_,e)},i.updateTime=function(e,t,i,n){var r=this;this.requestNamedAnimationFrame("TimeTooltip#updateTime",(function(){var a,s=r.player_.duration();if(r.player_.liveTracker&&r.player_.liveTracker.isLive()){var o=r.player_.liveTracker.liveWindow(),u=o-t*o;a=(u<1?"":"-")+ln(u,o)}else a=ln(i,s);r.update(e,t,a),n&&n()}))},t}(Kt);Kt.registerComponent("TimeTooltip",Sn);var Tn=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).update=kt(Ct(I.default(n),n.update),30),n}L.default(t,e);var i=t.prototype;return i.createEl=function(){return e.prototype.createEl.call(this,"div",{className:"vjs-play-progress vjs-slider-bar"},{"aria-hidden":"true"})},i.update=function(e,t){var i=this.getChild("timeTooltip");if(i){var n=this.player_.scrubbing()?this.player_.getCache().currentTime:this.player_.currentTime();i.updateTime(e,t,n)}},t}(Kt);Tn.prototype.options_={children:[]},Te||le||Tn.prototype.options_.children.push("timeTooltip"),Kt.registerComponent("PlayProgressBar",Tn);var En=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).update=kt(Ct(I.default(n),n.update),30),n}L.default(t,e);var i=t.prototype;return i.createEl=function(){return e.prototype.createEl.call(this,"div",{className:"vjs-mouse-display"})},i.update=function(e,t){var i=this,n=t*this.player_.duration();this.getChild("timeTooltip").updateTime(e,t,n,(function(){i.el_.style.left=e.width*t+"px"}))},t}(Kt);En.prototype.options_={children:["timeTooltip"]},Kt.registerComponent("MouseTimeDisplay",En);var wn=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).setEventHandlers_(),n}L.default(t,e);var i=t.prototype;return i.setEventHandlers_=function(){var e=this;this.update_=Ct(this,this.update),this.update=kt(this.update_,30),this.on(this.player_,["ended","durationchange","timeupdate"],this.update),this.player_.liveTracker&&this.on(this.player_.liveTracker,"liveedgechange",this.update),this.updateInterval=null,this.enableIntervalHandler_=function(t){return e.enableInterval_(t)},this.disableIntervalHandler_=function(t){return e.disableInterval_(t)},this.on(this.player_,["playing"],this.enableIntervalHandler_),this.on(this.player_,["ended","pause","waiting"],this.disableIntervalHandler_),"hidden"in k.default&&"visibilityState"in k.default&&this.on(k.default,"visibilitychange",this.toggleVisibility_)},i.toggleVisibility_=function(e){"hidden"===k.default.visibilityState?(this.cancelNamedAnimationFrame("SeekBar#update"),this.cancelNamedAnimationFrame("Slider#update"),this.disableInterval_(e)):(this.player_.ended()||this.player_.paused()||this.enableInterval_(),this.update())},i.enableInterval_=function(){this.updateInterval||(this.updateInterval=this.setInterval(this.update,30))},i.disableInterval_=function(e){this.player_.liveTracker&&this.player_.liveTracker.isLive()&&e&&"ended"!==e.type||this.updateInterval&&(this.clearInterval(this.updateInterval),this.updateInterval=null)},i.createEl=function(){return e.prototype.createEl.call(this,"div",{className:"vjs-progress-holder"},{"aria-label":this.localize("Progress Bar")})},i.update=function(t){var i=this;if("hidden"!==k.default.visibilityState){var n=e.prototype.update.call(this);return this.requestNamedAnimationFrame("SeekBar#update",(function(){var e=i.player_.ended()?i.player_.duration():i.getCurrentTime_(),t=i.player_.liveTracker,r=i.player_.duration();t&&t.isLive()&&(r=i.player_.liveTracker.liveCurrentTime()),i.percent_!==n&&(i.el_.setAttribute("aria-valuenow",(100*n).toFixed(2)),i.percent_=n),i.currentTime_===e&&i.duration_===r||(i.el_.setAttribute("aria-valuetext",i.localize("progress bar timing: currentTime={1} duration={2}",[ln(e,r),ln(r,r)],"{1} of {2}")),i.currentTime_=e,i.duration_=r),i.bar&&i.bar.update(We(i.el()),i.getProgress())})),n}},i.userSeek_=function(e){this.player_.liveTracker&&this.player_.liveTracker.isLive()&&this.player_.liveTracker.nextSeekedFromUser(),this.player_.currentTime(e)},i.getCurrentTime_=function(){return this.player_.scrubbing()?this.player_.getCache().currentTime:this.player_.currentTime()},i.getPercent=function(){var e,t=this.getCurrentTime_(),i=this.player_.liveTracker;return i&&i.isLive()?(e=(t-i.seekableStart())/i.liveWindow(),i.atLiveEdge()&&(e=1)):e=t/this.player_.duration(),e},i.handleMouseDown=function(t){Ze(t)&&(t.stopPropagation(),this.player_.scrubbing(!0),this.videoWasPlaying=!this.player_.paused(),this.player_.pause(),e.prototype.handleMouseDown.call(this,t))},i.handleMouseMove=function(e){if(Ze(e)){var t,i=this.calculateDistance(e),n=this.player_.liveTracker;if(n&&n.isLive()){if(i>=.99)return void n.seekToLiveEdge();var r=n.seekableStart(),a=n.liveCurrentTime();if((t=r+i*n.liveWindow())>=a&&(t=a),t<=r&&(t=r+.1),t===1/0)return}else(t=i*this.player_.duration())===this.player_.duration()&&(t-=.1);this.userSeek_(t)}},i.enable=function(){e.prototype.enable.call(this);var t=this.getChild("mouseTimeDisplay");t&&t.show()},i.disable=function(){e.prototype.disable.call(this);var t=this.getChild("mouseTimeDisplay");t&&t.hide()},i.handleMouseUp=function(t){e.prototype.handleMouseUp.call(this,t),t&&t.stopPropagation(),this.player_.scrubbing(!1),this.player_.trigger({type:"timeupdate",target:this,manuallyTriggered:!0}),this.videoWasPlaying?ii(this.player_.play()):this.update_()},i.stepForward=function(){this.userSeek_(this.player_.currentTime()+5)},i.stepBack=function(){this.userSeek_(this.player_.currentTime()-5)},i.handleAction=function(e){this.player_.paused()?this.player_.play():this.player_.pause()},i.handleKeyDown=function(t){var i=this.player_.liveTracker;if(R.default.isEventKey(t,"Space")||R.default.isEventKey(t,"Enter"))t.preventDefault(),t.stopPropagation(),this.handleAction(t);else if(R.default.isEventKey(t,"Home"))t.preventDefault(),t.stopPropagation(),this.userSeek_(0);else if(R.default.isEventKey(t,"End"))t.preventDefault(),t.stopPropagation(),i&&i.isLive()?this.userSeek_(i.liveCurrentTime()):this.userSeek_(this.player_.duration());else if(/^[0-9]$/.test(R.default(t))){t.preventDefault(),t.stopPropagation();var n=10*(R.default.codes[R.default(t)]-R.default.codes[0])/100;i&&i.isLive()?this.userSeek_(i.seekableStart()+i.liveWindow()*n):this.userSeek_(this.player_.duration()*n)}else R.default.isEventKey(t,"PgDn")?(t.preventDefault(),t.stopPropagation(),this.userSeek_(this.player_.currentTime()-60)):R.default.isEventKey(t,"PgUp")?(t.preventDefault(),t.stopPropagation(),this.userSeek_(this.player_.currentTime()+60)):e.prototype.handleKeyDown.call(this,t)},i.dispose=function(){this.disableInterval_(),this.off(this.player_,["ended","durationchange","timeupdate"],this.update),this.player_.liveTracker&&this.off(this.player_.liveTracker,"liveedgechange",this.update),this.off(this.player_,["playing"],this.enableIntervalHandler_),this.off(this.player_,["ended","pause","waiting"],this.disableIntervalHandler_),"hidden"in k.default&&"visibilityState"in k.default&&this.off(k.default,"visibilitychange",this.toggleVisibility_),e.prototype.dispose.call(this)},t}(vn);wn.prototype.options_={children:["loadProgressBar","playProgressBar"],barName:"playProgressBar"},Te||le||wn.prototype.options_.children.splice(1,0,"mouseTimeDisplay"),Kt.registerComponent("SeekBar",wn);var An=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).handleMouseMove=kt(Ct(I.default(n),n.handleMouseMove),30),n.throttledHandleMouseSeek=kt(Ct(I.default(n),n.handleMouseSeek),30),n.handleMouseUpHandler_=function(e){return n.handleMouseUp(e)},n.handleMouseDownHandler_=function(e){return n.handleMouseDown(e)},n.enable(),n}L.default(t,e);var i=t.prototype;return i.createEl=function(){return e.prototype.createEl.call(this,"div",{className:"vjs-progress-control vjs-control"})},i.handleMouseMove=function(e){var t=this.getChild("seekBar");if(t){var i=t.getChild("playProgressBar"),n=t.getChild("mouseTimeDisplay");if(i||n){var r=t.el(),a=Ye(r),s=qe(r,e).x;s=gn(s,0,1),n&&n.update(a,s),i&&i.update(a,t.getProgress())}}},i.handleMouseSeek=function(e){var t=this.getChild("seekBar");t&&t.handleMouseMove(e)},i.enabled=function(){return this.enabled_},i.disable=function(){if(this.children().forEach((function(e){return e.disable&&e.disable()})),this.enabled()&&(this.off(["mousedown","touchstart"],this.handleMouseDownHandler_),this.off(this.el_,"mousemove",this.handleMouseMove),this.removeListenersAddedOnMousedownAndTouchstart(),this.addClass("disabled"),this.enabled_=!1,this.player_.scrubbing())){var e=this.getChild("seekBar");this.player_.scrubbing(!1),e.videoWasPlaying&&ii(this.player_.play())}},i.enable=function(){this.children().forEach((function(e){return e.enable&&e.enable()})),this.enabled()||(this.on(["mousedown","touchstart"],this.handleMouseDownHandler_),this.on(this.el_,"mousemove",this.handleMouseMove),this.removeClass("disabled"),this.enabled_=!0)},i.removeListenersAddedOnMousedownAndTouchstart=function(){var e=this.el_.ownerDocument;this.off(e,"mousemove",this.throttledHandleMouseSeek),this.off(e,"touchmove",this.throttledHandleMouseSeek),this.off(e,"mouseup",this.handleMouseUpHandler_),this.off(e,"touchend",this.handleMouseUpHandler_)},i.handleMouseDown=function(e){var t=this.el_.ownerDocument,i=this.getChild("seekBar");i&&i.handleMouseDown(e),this.on(t,"mousemove",this.throttledHandleMouseSeek),this.on(t,"touchmove",this.throttledHandleMouseSeek),this.on(t,"mouseup",this.handleMouseUpHandler_),this.on(t,"touchend",this.handleMouseUpHandler_)},i.handleMouseUp=function(e){var t=this.getChild("seekBar");t&&t.handleMouseUp(e),this.removeListenersAddedOnMousedownAndTouchstart()},t}(Kt);An.prototype.options_={children:["seekBar"]},Kt.registerComponent("ProgressControl",An);var Cn=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).on(t,["enterpictureinpicture","leavepictureinpicture"],(function(e){return n.handlePictureInPictureChange(e)})),n.on(t,["disablepictureinpicturechanged","loadedmetadata"],(function(e){return n.handlePictureInPictureEnabledChange(e)})),n.disable(),n}L.default(t,e);var i=t.prototype;return i.buildCSSClass=function(){return"vjs-picture-in-picture-control "+e.prototype.buildCSSClass.call(this)},i.handlePictureInPictureEnabledChange=function(){k.default.pictureInPictureEnabled&&!1===this.player_.disablePictureInPicture()?this.enable():this.disable()},i.handlePictureInPictureChange=function(e){this.player_.isInPictureInPicture()?this.controlText("Exit Picture-in-Picture"):this.controlText("Picture-in-Picture"),this.handlePictureInPictureEnabledChange()},i.handleClick=function(e){this.player_.isInPictureInPicture()?this.player_.exitPictureInPicture():this.player_.requestPictureInPicture()},t}(nn);Cn.prototype.controlText_="Picture-in-Picture",Kt.registerComponent("PictureInPictureToggle",Cn);var kn=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).on(t,"fullscreenchange",(function(e){return n.handleFullscreenChange(e)})),!1===k.default[t.fsApi_.fullscreenEnabled]&&n.disable(),n}L.default(t,e);var i=t.prototype;return i.buildCSSClass=function(){return"vjs-fullscreen-control "+e.prototype.buildCSSClass.call(this)},i.handleFullscreenChange=function(e){this.player_.isFullscreen()?this.controlText("Non-Fullscreen"):this.controlText("Fullscreen")},i.handleClick=function(e){this.player_.isFullscreen()?this.player_.exitFullscreen():this.player_.requestFullscreen()},t}(nn);kn.prototype.controlText_="Fullscreen",Kt.registerComponent("FullscreenToggle",kn);var Pn=function(e){function t(){return e.apply(this,arguments)||this}return L.default(t,e),t.prototype.createEl=function(){var t=e.prototype.createEl.call(this,"div",{className:"vjs-volume-level"});return t.appendChild(e.prototype.createEl.call(this,"span",{className:"vjs-control-text"})),t},t}(Kt);Kt.registerComponent("VolumeLevel",Pn);var In=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).update=kt(Ct(I.default(n),n.update),30),n}L.default(t,e);var i=t.prototype;return i.createEl=function(){return e.prototype.createEl.call(this,"div",{className:"vjs-volume-tooltip"},{"aria-hidden":"true"})},i.update=function(e,t,i,n){if(!i){var r=We(this.el_),a=We(this.player_.el()),s=e.width*t;if(!a||!r)return;var o=e.left-a.left+s,u=e.width-s+(a.right-e.right),l=r.width/2;or.width&&(l=r.width),this.el_.style.right="-"+l+"px"}this.write(n+"%")},i.write=function(e){Re(this.el_,e)},i.updateVolume=function(e,t,i,n,r){var a=this;this.requestNamedAnimationFrame("VolumeLevelTooltip#updateVolume",(function(){a.update(e,t,i,n.toFixed(0)),r&&r()}))},t}(Kt);Kt.registerComponent("VolumeLevelTooltip",In);var Ln=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).update=kt(Ct(I.default(n),n.update),30),n}L.default(t,e);var i=t.prototype;return i.createEl=function(){return e.prototype.createEl.call(this,"div",{className:"vjs-mouse-display"})},i.update=function(e,t,i){var n=this,r=100*t;this.getChild("volumeLevelTooltip").updateVolume(e,t,i,r,(function(){i?n.el_.style.bottom=e.height*t+"px":n.el_.style.left=e.width*t+"px"}))},t}(Kt);Ln.prototype.options_={children:["volumeLevelTooltip"]},Kt.registerComponent("MouseVolumeLevelDisplay",Ln);var xn=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).on("slideractive",(function(e){return n.updateLastVolume_(e)})),n.on(t,"volumechange",(function(e){return n.updateARIAAttributes(e)})),t.ready((function(){return n.updateARIAAttributes()})),n}L.default(t,e);var i=t.prototype;return i.createEl=function(){return e.prototype.createEl.call(this,"div",{className:"vjs-volume-bar vjs-slider-bar"},{"aria-label":this.localize("Volume Level"),"aria-live":"polite"})},i.handleMouseDown=function(t){Ze(t)&&e.prototype.handleMouseDown.call(this,t)},i.handleMouseMove=function(e){var t=this.getChild("mouseVolumeLevelDisplay");if(t){var i=this.el(),n=We(i),r=this.vertical(),a=qe(i,e);a=r?a.y:a.x,a=gn(a,0,1),t.update(n,a,r)}Ze(e)&&(this.checkMuted(),this.player_.volume(this.calculateDistance(e)))},i.checkMuted=function(){this.player_.muted()&&this.player_.muted(!1)},i.getPercent=function(){return this.player_.muted()?0:this.player_.volume()},i.stepForward=function(){this.checkMuted(),this.player_.volume(this.player_.volume()+.1)},i.stepBack=function(){this.checkMuted(),this.player_.volume(this.player_.volume()-.1)},i.updateARIAAttributes=function(e){var t=this.player_.muted()?0:this.volumeAsPercentage_();this.el_.setAttribute("aria-valuenow",t),this.el_.setAttribute("aria-valuetext",t+"%")},i.volumeAsPercentage_=function(){return Math.round(100*this.player_.volume())},i.updateLastVolume_=function(){var e=this,t=this.player_.volume();this.one("sliderinactive",(function(){0===e.player_.volume()&&e.player_.lastVolume_(t)}))},t}(vn);xn.prototype.options_={children:["volumeLevel"],barName:"volumeLevel"},Te||le||xn.prototype.options_.children.splice(0,0,"mouseVolumeLevelDisplay"),xn.prototype.playerEvent="volumechange",Kt.registerComponent("VolumeBar",xn);var Rn=function(e){function t(t,i){var n;return void 0===i&&(i={}),i.vertical=i.vertical||!1,(void 0===i.volumeBar||te(i.volumeBar))&&(i.volumeBar=i.volumeBar||{},i.volumeBar.vertical=i.vertical),n=e.call(this,t,i)||this,function(e,t){t.tech_&&!t.tech_.featuresVolumeControl&&e.addClass("vjs-hidden"),e.on(t,"loadstart",(function(){t.tech_.featuresVolumeControl?e.removeClass("vjs-hidden"):e.addClass("vjs-hidden")}))}(I.default(n),t),n.throttledHandleMouseMove=kt(Ct(I.default(n),n.handleMouseMove),30),n.handleMouseUpHandler_=function(e){return n.handleMouseUp(e)},n.on("mousedown",(function(e){return n.handleMouseDown(e)})),n.on("touchstart",(function(e){return n.handleMouseDown(e)})),n.on("mousemove",(function(e){return n.handleMouseMove(e)})),n.on(n.volumeBar,["focus","slideractive"],(function(){n.volumeBar.addClass("vjs-slider-active"),n.addClass("vjs-slider-active"),n.trigger("slideractive")})),n.on(n.volumeBar,["blur","sliderinactive"],(function(){n.volumeBar.removeClass("vjs-slider-active"),n.removeClass("vjs-slider-active"),n.trigger("sliderinactive")})),n}L.default(t,e);var i=t.prototype;return i.createEl=function(){var t="vjs-volume-horizontal";return this.options_.vertical&&(t="vjs-volume-vertical"),e.prototype.createEl.call(this,"div",{className:"vjs-volume-control vjs-control "+t})},i.handleMouseDown=function(e){var t=this.el_.ownerDocument;this.on(t,"mousemove",this.throttledHandleMouseMove),this.on(t,"touchmove",this.throttledHandleMouseMove),this.on(t,"mouseup",this.handleMouseUpHandler_),this.on(t,"touchend",this.handleMouseUpHandler_)},i.handleMouseUp=function(e){var t=this.el_.ownerDocument;this.off(t,"mousemove",this.throttledHandleMouseMove),this.off(t,"touchmove",this.throttledHandleMouseMove),this.off(t,"mouseup",this.handleMouseUpHandler_),this.off(t,"touchend",this.handleMouseUpHandler_)},i.handleMouseMove=function(e){this.volumeBar.handleMouseMove(e)},t}(Kt);Rn.prototype.options_={children:["volumeBar"]},Kt.registerComponent("VolumeControl",Rn);var Dn=function(e){function t(t,i){var n;return n=e.call(this,t,i)||this,function(e,t){t.tech_&&!t.tech_.featuresMuteControl&&e.addClass("vjs-hidden"),e.on(t,"loadstart",(function(){t.tech_.featuresMuteControl?e.removeClass("vjs-hidden"):e.addClass("vjs-hidden")}))}(I.default(n),t),n.on(t,["loadstart","volumechange"],(function(e){return n.update(e)})),n}L.default(t,e);var i=t.prototype;return i.buildCSSClass=function(){return"vjs-mute-control "+e.prototype.buildCSSClass.call(this)},i.handleClick=function(e){var t=this.player_.volume(),i=this.player_.lastVolume_();if(0===t){var n=i<.1?.1:i;this.player_.volume(n),this.player_.muted(!1)}else this.player_.muted(!this.player_.muted())},i.update=function(e){this.updateIcon_(),this.updateControlText_()},i.updateIcon_=function(){var e=this.player_.volume(),t=3;Te&&this.player_.tech_&&this.player_.tech_.el_&&this.player_.muted(this.player_.tech_.el_.muted),0===e||this.player_.muted()?t=0:e<.33?t=1:e<.67&&(t=2);for(var i=0;i<4;i++)Me(this.el_,"vjs-vol-"+i);Ue(this.el_,"vjs-vol-"+t)},i.updateControlText_=function(){var e=this.player_.muted()||0===this.player_.volume()?"Unmute":"Mute";this.controlText()!==e&&this.controlText(e)},t}(nn);Dn.prototype.controlText_="Mute",Kt.registerComponent("MuteToggle",Dn);var On=function(e){function t(t,i){var n;return void 0===i&&(i={}),void 0!==i.inline?i.inline=i.inline:i.inline=!0,(void 0===i.volumeControl||te(i.volumeControl))&&(i.volumeControl=i.volumeControl||{},i.volumeControl.vertical=!i.inline),(n=e.call(this,t,i)||this).handleKeyPressHandler_=function(e){return n.handleKeyPress(e)},n.on(t,["loadstart"],(function(e){return n.volumePanelState_(e)})),n.on(n.muteToggle,"keyup",(function(e){return n.handleKeyPress(e)})),n.on(n.volumeControl,"keyup",(function(e){return n.handleVolumeControlKeyUp(e)})),n.on("keydown",(function(e){return n.handleKeyPress(e)})),n.on("mouseover",(function(e){return n.handleMouseOver(e)})),n.on("mouseout",(function(e){return n.handleMouseOut(e)})),n.on(n.volumeControl,["slideractive"],n.sliderActive_),n.on(n.volumeControl,["sliderinactive"],n.sliderInactive_),n}L.default(t,e);var i=t.prototype;return i.sliderActive_=function(){this.addClass("vjs-slider-active")},i.sliderInactive_=function(){this.removeClass("vjs-slider-active")},i.volumePanelState_=function(){this.volumeControl.hasClass("vjs-hidden")&&this.muteToggle.hasClass("vjs-hidden")&&this.addClass("vjs-hidden"),this.volumeControl.hasClass("vjs-hidden")&&!this.muteToggle.hasClass("vjs-hidden")&&this.addClass("vjs-mute-toggle-only")},i.createEl=function(){var t="vjs-volume-panel-horizontal";return this.options_.inline||(t="vjs-volume-panel-vertical"),e.prototype.createEl.call(this,"div",{className:"vjs-volume-panel vjs-control "+t})},i.dispose=function(){this.handleMouseOut(),e.prototype.dispose.call(this)},i.handleVolumeControlKeyUp=function(e){R.default.isEventKey(e,"Esc")&&this.muteToggle.focus()},i.handleMouseOver=function(e){this.addClass("vjs-hover"),yt(k.default,"keyup",this.handleKeyPressHandler_)},i.handleMouseOut=function(e){this.removeClass("vjs-hover"),bt(k.default,"keyup",this.handleKeyPressHandler_)},i.handleKeyPress=function(e){R.default.isEventKey(e,"Esc")&&this.handleMouseOut()},t}(Kt);On.prototype.options_={children:["muteToggle","volumeControl"]},Kt.registerComponent("VolumePanel",On);var Un=function(e){function t(t,i){var n;return n=e.call(this,t,i)||this,i&&(n.menuButton_=i.menuButton),n.focusedChild_=-1,n.on("keydown",(function(e){return n.handleKeyDown(e)})),n.boundHandleBlur_=function(e){return n.handleBlur(e)},n.boundHandleTapClick_=function(e){return n.handleTapClick(e)},n}L.default(t,e);var i=t.prototype;return i.addEventListenerForItem=function(e){e instanceof Kt&&(this.on(e,"blur",this.boundHandleBlur_),this.on(e,["tap","click"],this.boundHandleTapClick_))},i.removeEventListenerForItem=function(e){e instanceof Kt&&(this.off(e,"blur",this.boundHandleBlur_),this.off(e,["tap","click"],this.boundHandleTapClick_))},i.removeChild=function(t){"string"==typeof t&&(t=this.getChild(t)),this.removeEventListenerForItem(t),e.prototype.removeChild.call(this,t)},i.addItem=function(e){var t=this.addChild(e);t&&this.addEventListenerForItem(t)},i.createEl=function(){var t=this.options_.contentElType||"ul";this.contentEl_=xe(t,{className:"vjs-menu-content"}),this.contentEl_.setAttribute("role","menu");var i=e.prototype.createEl.call(this,"div",{append:this.contentEl_,className:"vjs-menu"});return i.appendChild(this.contentEl_),yt(i,"click",(function(e){e.preventDefault(),e.stopImmediatePropagation()})),i},i.dispose=function(){this.contentEl_=null,this.boundHandleBlur_=null,this.boundHandleTapClick_=null,e.prototype.dispose.call(this)},i.handleBlur=function(e){var t=e.relatedTarget||k.default.activeElement;if(!this.children().some((function(e){return e.el()===t}))){var i=this.menuButton_;i&&i.buttonPressed_&&t!==i.el().firstChild&&i.unpressButton()}},i.handleTapClick=function(e){if(this.menuButton_){this.menuButton_.unpressButton();var t=this.children();if(!Array.isArray(t))return;var i=t.filter((function(t){return t.el()===e.target}))[0];if(!i)return;"CaptionSettingsMenuItem"!==i.name()&&this.menuButton_.focus()}},i.handleKeyDown=function(e){R.default.isEventKey(e,"Left")||R.default.isEventKey(e,"Down")?(e.preventDefault(),e.stopPropagation(),this.stepForward()):(R.default.isEventKey(e,"Right")||R.default.isEventKey(e,"Up"))&&(e.preventDefault(),e.stopPropagation(),this.stepBack())},i.stepForward=function(){var e=0;void 0!==this.focusedChild_&&(e=this.focusedChild_+1),this.focus(e)},i.stepBack=function(){var e=0;void 0!==this.focusedChild_&&(e=this.focusedChild_-1),this.focus(e)},i.focus=function(e){void 0===e&&(e=0);var t=this.children().slice();t.length&&t[0].hasClass("vjs-menu-title")&&t.shift(),t.length>0&&(e<0?e=0:e>=t.length&&(e=t.length-1),this.focusedChild_=e,t[e].el_.focus())},t}(Kt);Kt.registerComponent("Menu",Un);var Mn=function(e){function t(t,i){var n;void 0===i&&(i={}),(n=e.call(this,t,i)||this).menuButton_=new nn(t,i),n.menuButton_.controlText(n.controlText_),n.menuButton_.el_.setAttribute("aria-haspopup","true");var r=nn.prototype.buildCSSClass();n.menuButton_.el_.className=n.buildCSSClass()+" "+r,n.menuButton_.removeClass("vjs-control"),n.addChild(n.menuButton_),n.update(),n.enabled_=!0;var a=function(e){return n.handleClick(e)};return n.handleMenuKeyUp_=function(e){return n.handleMenuKeyUp(e)},n.on(n.menuButton_,"tap",a),n.on(n.menuButton_,"click",a),n.on(n.menuButton_,"keydown",(function(e){return n.handleKeyDown(e)})),n.on(n.menuButton_,"mouseenter",(function(){n.addClass("vjs-hover"),n.menu.show(),yt(k.default,"keyup",n.handleMenuKeyUp_)})),n.on("mouseleave",(function(e){return n.handleMouseLeave(e)})),n.on("keydown",(function(e){return n.handleSubmenuKeyDown(e)})),n}L.default(t,e);var i=t.prototype;return i.update=function(){var e=this.createMenu();this.menu&&(this.menu.dispose(),this.removeChild(this.menu)),this.menu=e,this.addChild(e),this.buttonPressed_=!1,this.menuButton_.el_.setAttribute("aria-expanded","false"),this.items&&this.items.length<=this.hideThreshold_?this.hide():this.show()},i.createMenu=function(){var e=new Un(this.player_,{menuButton:this});if(this.hideThreshold_=0,this.options_.title){var t=xe("li",{className:"vjs-menu-title",textContent:Ht(this.options_.title),tabIndex:-1}),i=new Kt(this.player_,{el:t});e.addItem(i)}if(this.items=this.createItems(),this.items)for(var n=0;n-1&&"showing"===a.mode){i=!1;break}}i!==this.isSelected_&&this.selected(i)},i.handleSelectedLanguageChange=function(e){for(var t=this.player().textTracks(),i=!0,n=0,r=t.length;n-1&&"showing"===a.mode){i=!1;break}}i&&(this.player_.cache_.selectedLanguage={enabled:!1})},t}(jn);Kt.registerComponent("OffTextTrackMenuItem",Vn);var Hn=function(e){function t(t,i){return void 0===i&&(i={}),i.tracks=t.textTracks(),e.call(this,t,i)||this}return L.default(t,e),t.prototype.createItems=function(e,t){var i;void 0===e&&(e=[]),void 0===t&&(t=jn),this.label_&&(i=this.label_+" off"),e.push(new Vn(this.player_,{kinds:this.kinds_,kind:this.kind_,label:i})),this.hideThreshold_+=1;var n=this.player_.textTracks();Array.isArray(this.kinds_)||(this.kinds_=[this.kind_]);for(var r=0;r-1){var s=new t(this.player_,{track:a,kinds:this.kinds_,kind:this.kind_,selectable:!0,multiSelectable:!1});s.addClass("vjs-"+a.kind+"-menu-item"),e.push(s)}}return e},t}(Fn);Kt.registerComponent("TextTrackButton",Hn);var zn=function(e){function t(t,i){var n,r=i.track,a=i.cue,s=t.currentTime();return i.selectable=!0,i.multiSelectable=!1,i.label=a.text,i.selected=a.startTime<=s&&s=0;t--){var i=e[t];if(i.kind===this.kind_)return i}},i.getMenuCaption=function(){return this.track_&&this.track_.label?this.track_.label:this.localize(Ht(this.kind_))},i.createMenu=function(){return this.options_.title=this.getMenuCaption(),e.prototype.createMenu.call(this)},i.createItems=function(){var e=[];if(!this.track_)return e;var t=this.track_.cues;if(!t)return e;for(var i=0,n=t.length;i-1&&(n.label_="captions"),n.menuButton_.controlText(Ht(n.label_)),n}L.default(t,e);var i=t.prototype;return i.buildCSSClass=function(){return"vjs-subs-caps-button "+e.prototype.buildCSSClass.call(this)},i.buildWrapperCSSClass=function(){return"vjs-subs-caps-button "+e.prototype.buildWrapperCSSClass.call(this)},i.createItems=function(){var t=[];return this.player().tech_&&this.player().tech_.featuresNativeTextTracks||!this.player().getChild("textTrackSettings")||(t.push(new qn(this.player_,{kind:this.label_})),this.hideThreshold_+=1),t=e.prototype.createItems.call(this,t,Xn)},t}(Hn);Qn.prototype.kinds_=["captions","subtitles"],Qn.prototype.controlText_="Subtitles",Kt.registerComponent("SubsCapsButton",Qn);var $n=function(e){function t(t,i){var n,r=i.track,a=t.audioTracks();i.label=r.label||r.language||"Unknown",i.selected=r.enabled,(n=e.call(this,t,i)||this).track=r,n.addClass("vjs-"+r.kind+"-menu-item");var s=function(){for(var e=arguments.length,t=new Array(e),i=0;i=0;i--)t.push(new Zn(this.player(),{rate:e[i]+"x"}));return t},i.updateARIAAttributes=function(){this.el().setAttribute("aria-valuenow",this.player().playbackRate())},i.handleClick=function(e){for(var t=this.player().playbackRate(),i=this.playbackRates(),n=i[0],r=0;rt){n=i[r];break}this.player().playbackRate(n)},i.handlePlaybackRateschange=function(e){this.update()},i.playbackRates=function(){var e=this.player();return e.playbackRates&&e.playbackRates()||[]},i.playbackRateSupported=function(){return this.player().tech_&&this.player().tech_.featuresPlaybackRate&&this.playbackRates()&&this.playbackRates().length>0},i.updateVisibility=function(e){this.playbackRateSupported()?this.removeClass("vjs-hidden"):this.addClass("vjs-hidden")},i.updateLabel=function(e){this.playbackRateSupported()&&(this.labelEl_.textContent=this.player().playbackRate()+"x")},t}(Mn);er.prototype.controlText_="Playback Rate",Kt.registerComponent("PlaybackRateMenuButton",er);var tr=function(e){function t(){return e.apply(this,arguments)||this}L.default(t,e);var i=t.prototype;return i.buildCSSClass=function(){return"vjs-spacer "+e.prototype.buildCSSClass.call(this)},i.createEl=function(t,i,n){return void 0===t&&(t="div"),void 0===i&&(i={}),void 0===n&&(n={}),i.className||(i.className=this.buildCSSClass()),e.prototype.createEl.call(this,t,i,n)},t}(Kt);Kt.registerComponent("Spacer",tr);var ir=function(e){function t(){return e.apply(this,arguments)||this}L.default(t,e);var i=t.prototype;return i.buildCSSClass=function(){return"vjs-custom-control-spacer "+e.prototype.buildCSSClass.call(this)},i.createEl=function(){return e.prototype.createEl.call(this,"div",{className:this.buildCSSClass(),textContent:" "})},t}(tr);Kt.registerComponent("CustomControlSpacer",ir);var nr=function(e){function t(){return e.apply(this,arguments)||this}return L.default(t,e),t.prototype.createEl=function(){return e.prototype.createEl.call(this,"div",{className:"vjs-control-bar",dir:"ltr"})},t}(Kt);nr.prototype.options_={children:["playToggle","volumePanel","currentTimeDisplay","timeDivider","durationDisplay","progressControl","liveDisplay","seekToLive","remainingTimeDisplay","customControlSpacer","playbackRateMenuButton","chaptersButton","descriptionsButton","subsCapsButton","audioTrackButton","fullscreenToggle"]},"exitPictureInPicture"in k.default&&nr.prototype.options_.children.splice(nr.prototype.options_.children.length-1,0,"pictureInPictureToggle"),Kt.registerComponent("ControlBar",nr);var rr=function(e){function t(t,i){var n;return(n=e.call(this,t,i)||this).on(t,"error",(function(e){return n.open(e)})),n}L.default(t,e);var i=t.prototype;return i.buildCSSClass=function(){return"vjs-error-display "+e.prototype.buildCSSClass.call(this)},i.content=function(){var e=this.player().error();return e?this.localize(e.message):""},t}(si);rr.prototype.options_=P.default({},si.prototype.options_,{pauseOnOpen:!1,fillAlways:!0,temporary:!1,uncloseable:!0}),Kt.registerComponent("ErrorDisplay",rr);var ar=["#000","Black"],sr=["#00F","Blue"],or=["#0FF","Cyan"],ur=["#0F0","Green"],lr=["#F0F","Magenta"],hr=["#F00","Red"],dr=["#FFF","White"],cr=["#FF0","Yellow"],fr=["1","Opaque"],pr=["0.5","Semi-Transparent"],mr=["0","Transparent"],_r={backgroundColor:{selector:".vjs-bg-color > select",id:"captions-background-color-%s",label:"Color",options:[ar,dr,hr,ur,sr,cr,lr,or]},backgroundOpacity:{selector:".vjs-bg-opacity > select",id:"captions-background-opacity-%s",label:"Transparency",options:[fr,pr,mr]},color:{selector:".vjs-fg-color > select",id:"captions-foreground-color-%s",label:"Color",options:[dr,ar,hr,ur,sr,cr,lr,or]},edgeStyle:{selector:".vjs-edge-style > select",id:"%s",label:"Text Edge Style",options:[["none","None"],["raised","Raised"],["depressed","Depressed"],["uniform","Uniform"],["dropshadow","Dropshadow"]]},fontFamily:{selector:".vjs-font-family > select",id:"captions-font-family-%s",label:"Font Family",options:[["proportionalSansSerif","Proportional Sans-Serif"],["monospaceSansSerif","Monospace Sans-Serif"],["proportionalSerif","Proportional Serif"],["monospaceSerif","Monospace Serif"],["casual","Casual"],["script","Script"],["small-caps","Small Caps"]]},fontPercent:{selector:".vjs-font-percent > select",id:"captions-font-size-%s",label:"Font Size",options:[["0.50","50%"],["0.75","75%"],["1.00","100%"],["1.25","125%"],["1.50","150%"],["1.75","175%"],["2.00","200%"],["3.00","300%"],["4.00","400%"]],default:2,parser:function(e){return"1.00"===e?null:Number(e)}},textOpacity:{selector:".vjs-text-opacity > select",id:"captions-foreground-opacity-%s",label:"Transparency",options:[fr,pr]},windowColor:{selector:".vjs-window-color > select",id:"captions-window-color-%s",label:"Color"},windowOpacity:{selector:".vjs-window-opacity > select",id:"captions-window-opacity-%s",label:"Transparency",options:[mr,pr,fr]}};function gr(e,t){if(t&&(e=t(e)),e&&"none"!==e)return e}_r.windowColor.options=_r.backgroundColor.options;var vr=function(e){function t(t,i){var n;return i.temporary=!1,(n=e.call(this,t,i)||this).updateDisplay=n.updateDisplay.bind(I.default(n)),n.fill(),n.hasBeenOpened_=n.hasBeenFilled_=!0,n.endDialog=xe("p",{className:"vjs-control-text",textContent:n.localize("End of dialog window.")}),n.el().appendChild(n.endDialog),n.setDefaults(),void 0===i.persistTextTrackSettings&&(n.options_.persistTextTrackSettings=n.options_.playerOptions.persistTextTrackSettings),n.on(n.$(".vjs-done-button"),"click",(function(){n.saveSettings(),n.close()})),n.on(n.$(".vjs-default-button"),"click",(function(){n.setDefaults(),n.updateDisplay()})),J(_r,(function(e){n.on(n.$(e.selector),"change",n.updateDisplay)})),n.options_.persistTextTrackSettings&&n.restoreSettings(),n}L.default(t,e);var i=t.prototype;return i.dispose=function(){this.endDialog=null,e.prototype.dispose.call(this)},i.createElSelect_=function(e,t,i){var n=this;void 0===t&&(t=""),void 0===i&&(i="label");var r=_r[e],a=r.id.replace("%s",this.id_),s=[t,a].join(" ").trim();return["<"+i+' id="'+a+'" class="'+("label"===i?"vjs-label":"")+'">',this.localize(r.label),"",'").join("")},i.createElFgColor_=function(){var e="captions-text-legend-"+this.id_;return['
    ','',this.localize("Text"),"",this.createElSelect_("color",e),'',this.createElSelect_("textOpacity",e),"","
    "].join("")},i.createElBgColor_=function(){var e="captions-background-"+this.id_;return['
    ','',this.localize("Background"),"",this.createElSelect_("backgroundColor",e),'',this.createElSelect_("backgroundOpacity",e),"","
    "].join("")},i.createElWinColor_=function(){var e="captions-window-"+this.id_;return['
    ','',this.localize("Window"),"",this.createElSelect_("windowColor",e),'',this.createElSelect_("windowOpacity",e),"","
    "].join("")},i.createElColors_=function(){return xe("div",{className:"vjs-track-settings-colors",innerHTML:[this.createElFgColor_(),this.createElBgColor_(),this.createElWinColor_()].join("")})},i.createElFont_=function(){return xe("div",{className:"vjs-track-settings-font",innerHTML:['
    ',this.createElSelect_("fontPercent","","legend"),"
    ",'
    ',this.createElSelect_("edgeStyle","","legend"),"
    ",'
    ',this.createElSelect_("fontFamily","","legend"),"
    "].join("")})},i.createElControls_=function(){var e=this.localize("restore all settings to the default values");return xe("div",{className:"vjs-track-settings-controls",innerHTML:['",'"].join("")})},i.content=function(){return[this.createElColors_(),this.createElFont_(),this.createElControls_()]},i.label=function(){return this.localize("Caption Settings Dialog")},i.description=function(){return this.localize("Beginning of dialog window. Escape will cancel and close the window.")},i.buildCSSClass=function(){return e.prototype.buildCSSClass.call(this)+" vjs-text-track-settings"},i.getValues=function(){var e,t,i,n=this;return t=function(e,t,i){var r,a,s=(r=n.$(t.selector),a=t.parser,gr(r.options[r.options.selectedIndex].value,a));return void 0!==s&&(e[i]=s),e},void 0===(i={})&&(i=0),$(e=_r).reduce((function(i,n){return t(i,e[n],n)}),i)},i.setValues=function(e){var t=this;J(_r,(function(i,n){!function(e,t,i){if(t)for(var n=0;nthis.options_.liveTolerance;this.timeupdateSeen_&&n!==1/0||(a=!1),a!==this.behindLiveEdge_&&(this.behindLiveEdge_=a,this.trigger("liveedgechange"))}},i.handleDurationchange=function(){this.toggleTracking()},i.toggleTracking=function(){this.player_.duration()===1/0&&this.liveWindow()>=this.options_.trackingThreshold?(this.player_.options_.liveui&&this.player_.addClass("vjs-liveui"),this.startTracking()):(this.player_.removeClass("vjs-liveui"),this.stopTracking())},i.startTracking=function(){this.isTracking()||(this.timeupdateSeen_||(this.timeupdateSeen_=this.player_.hasStarted()),this.trackingInterval_=this.setInterval(this.trackLiveHandler_,30),this.trackLive_(),this.on(this.player_,["play","pause"],this.trackLiveHandler_),this.timeupdateSeen_?this.on(this.player_,"seeked",this.handleSeeked_):(this.one(this.player_,"play",this.handlePlay_),this.one(this.player_,"timeupdate",this.handleFirstTimeupdate_)))},i.handleFirstTimeupdate=function(){this.timeupdateSeen_=!0,this.on(this.player_,"seeked",this.handleSeeked_)},i.handleSeeked=function(){var e=Math.abs(this.liveCurrentTime()-this.player_.currentTime());this.seekedBehindLive_=this.nextSeekedFromUser_&&e>2,this.nextSeekedFromUser_=!1,this.trackLive_()},i.handlePlay=function(){this.one(this.player_,"timeupdate",this.seekToLiveEdge_)},i.reset_=function(){this.lastTime_=-1,this.pastSeekEnd_=0,this.lastSeekEnd_=-1,this.behindLiveEdge_=!0,this.timeupdateSeen_=!1,this.seekedBehindLive_=!1,this.nextSeekedFromUser_=!1,this.clearInterval(this.trackingInterval_),this.trackingInterval_=null,this.off(this.player_,["play","pause"],this.trackLiveHandler_),this.off(this.player_,"seeked",this.handleSeeked_),this.off(this.player_,"play",this.handlePlay_),this.off(this.player_,"timeupdate",this.handleFirstTimeupdate_),this.off(this.player_,"timeupdate",this.seekToLiveEdge_)},i.nextSeekedFromUser=function(){this.nextSeekedFromUser_=!0},i.stopTracking=function(){this.isTracking()&&(this.reset_(),this.trigger("liveedgechange"))},i.seekableEnd=function(){for(var e=this.player_.seekable(),t=[],i=e?e.length:0;i--;)t.push(e.end(i));return t.length?t.sort()[t.length-1]:1/0},i.seekableStart=function(){for(var e=this.player_.seekable(),t=[],i=e?e.length:0;i--;)t.push(e.start(i));return t.length?t.sort()[0]:0},i.liveWindow=function(){var e=this.liveCurrentTime();return e===1/0?0:e-this.seekableStart()},i.isLive=function(){return this.isTracking()},i.atLiveEdge=function(){return!this.behindLiveEdge()},i.liveCurrentTime=function(){return this.pastSeekEnd()+this.seekableEnd()},i.pastSeekEnd=function(){var e=this.seekableEnd();return-1!==this.lastSeekEnd_&&e!==this.lastSeekEnd_&&(this.pastSeekEnd_=0),this.lastSeekEnd_=e,this.pastSeekEnd_},i.behindLiveEdge=function(){return this.behindLiveEdge_},i.isTracking=function(){return"number"==typeof this.trackingInterval_},i.seekToLiveEdge=function(){this.seekedBehindLive_=!1,this.atLiveEdge()||(this.nextSeekedFromUser_=!1,this.player_.currentTime(this.liveCurrentTime()))},i.dispose=function(){this.off(k.default,"visibilitychange",this.handleVisibilityChange_),this.stopTracking(),e.prototype.dispose.call(this)},t}(Kt);Kt.registerComponent("LiveTracker",Sr);var Tr,Er=function(e){var t=e.el();if(t.hasAttribute("src"))return e.triggerSourceset(t.src),!0;var i=e.$$("source"),n=[],r="";if(!i.length)return!1;for(var a=0;a=2&&r.push("loadeddata"),e.readyState>=3&&r.push("canplay"),e.readyState>=4&&r.push("canplaythrough"),this.ready((function(){r.forEach((function(e){this.trigger(e)}),this)}))}},i.setScrubbing=function(e){this.isScrubbing_=e},i.scrubbing=function(){return this.isScrubbing_},i.setCurrentTime=function(e){try{this.isScrubbing_&&this.el_.fastSeek&&Ee?this.el_.fastSeek(e):this.el_.currentTime=e}catch(e){K(e,"Video is not ready. (Video.js)")}},i.duration=function(){var e=this;if(this.el_.duration===1/0&&le&&pe&&0===this.el_.currentTime){return this.on("timeupdate",(function t(){e.el_.currentTime>0&&(e.el_.duration===1/0&&e.trigger("durationchange"),e.off("timeupdate",t))})),NaN}return this.el_.duration||NaN},i.width=function(){return this.el_.offsetWidth},i.height=function(){return this.el_.offsetHeight},i.proxyWebkitFullscreen_=function(){var e=this;if("webkitDisplayingFullscreen"in this.el_){var t=function(){this.trigger("fullscreenchange",{isFullscreen:!1})},i=function(){"webkitPresentationMode"in this.el_&&"picture-in-picture"!==this.el_.webkitPresentationMode&&(this.one("webkitendfullscreen",t),this.trigger("fullscreenchange",{isFullscreen:!0,nativeIOSFullscreen:!0}))};this.on("webkitbeginfullscreen",i),this.on("dispose",(function(){e.off("webkitbeginfullscreen",i),e.off("webkitendfullscreen",t)}))}},i.supportsFullScreen=function(){if("function"==typeof this.el_.webkitEnterFullScreen){var e=C.default.navigator&&C.default.navigator.userAgent||"";if(/Android/.test(e)||!/Chrome|Mac OS X 10.5/.test(e))return!0}return!1},i.enterFullScreen=function(){var e=this.el_;if(e.paused&&e.networkState<=e.HAVE_METADATA)ii(this.el_.play()),this.setTimeout((function(){e.pause();try{e.webkitEnterFullScreen()}catch(e){this.trigger("fullscreenerror",e)}}),0);else try{e.webkitEnterFullScreen()}catch(e){this.trigger("fullscreenerror",e)}},i.exitFullScreen=function(){this.el_.webkitDisplayingFullscreen?this.el_.webkitExitFullScreen():this.trigger("fullscreenerror",new Error("The video is not fullscreen"))},i.requestPictureInPicture=function(){return this.el_.requestPictureInPicture()},i.src=function(e){if(void 0===e)return this.el_.src;this.setSrc(e)},i.reset=function(){t.resetMediaElement(this.el_)},i.currentSrc=function(){return this.currentSource_?this.currentSource_.src:this.el_.currentSrc},i.setControls=function(e){this.el_.controls=!!e},i.addTextTrack=function(t,i,n){return this.featuresNativeTextTracks?this.el_.addTextTrack(t,i,n):e.prototype.addTextTrack.call(this,t,i,n)},i.createRemoteTextTrack=function(t){if(!this.featuresNativeTextTracks)return e.prototype.createRemoteTextTrack.call(this,t);var i=k.default.createElement("track");return t.kind&&(i.kind=t.kind),t.label&&(i.label=t.label),(t.language||t.srclang)&&(i.srclang=t.language||t.srclang),t.default&&(i.default=t.default),t.id&&(i.id=t.id),t.src&&(i.src=t.src),i},i.addRemoteTextTrack=function(t,i){var n=e.prototype.addRemoteTextTrack.call(this,t,i);return this.featuresNativeTextTracks&&this.el().appendChild(n),n},i.removeRemoteTextTrack=function(t){if(e.prototype.removeRemoteTextTrack.call(this,t),this.featuresNativeTextTracks)for(var i=this.$$("track"),n=i.length;n--;)t!==i[n]&&t!==i[n].track||this.el().removeChild(i[n])},i.getVideoPlaybackQuality=function(){if("function"==typeof this.el().getVideoPlaybackQuality)return this.el().getVideoPlaybackQuality();var e={};return void 0!==this.el().webkitDroppedFrameCount&&void 0!==this.el().webkitDecodedFrameCount&&(e.droppedVideoFrames=this.el().webkitDroppedFrameCount,e.totalVideoFrames=this.el().webkitDecodedFrameCount),C.default.performance&&"function"==typeof C.default.performance.now?e.creationTime=C.default.performance.now():C.default.performance&&C.default.performance.timing&&"number"==typeof C.default.performance.timing.navigationStart&&(e.creationTime=C.default.Date.now()-C.default.performance.timing.navigationStart),e},t}(Ui);Ir(Lr,"TEST_VID",(function(){if(ke()){var e=k.default.createElement("video"),t=k.default.createElement("track");return t.kind="captions",t.srclang="en",t.label="English",e.appendChild(t),e}})),Lr.isSupported=function(){try{Lr.TEST_VID.volume=.5}catch(e){return!1}return!(!Lr.TEST_VID||!Lr.TEST_VID.canPlayType)},Lr.canPlayType=function(e){return Lr.TEST_VID.canPlayType(e)},Lr.canPlaySource=function(e,t){return Lr.canPlayType(e.type)},Lr.canControlVolume=function(){try{var e=Lr.TEST_VID.volume;return Lr.TEST_VID.volume=e/2+.1,e!==Lr.TEST_VID.volume}catch(e){return!1}},Lr.canMuteVolume=function(){try{var e=Lr.TEST_VID.muted;return Lr.TEST_VID.muted=!e,Lr.TEST_VID.muted?Ve(Lr.TEST_VID,"muted","muted"):He(Lr.TEST_VID,"muted"),e!==Lr.TEST_VID.muted}catch(e){return!1}},Lr.canControlPlaybackRate=function(){if(le&&pe&&me<58)return!1;try{var e=Lr.TEST_VID.playbackRate;return Lr.TEST_VID.playbackRate=e/2+.1,e!==Lr.TEST_VID.playbackRate}catch(e){return!1}},Lr.canOverrideAttributes=function(){try{var e=function(){};Object.defineProperty(k.default.createElement("video"),"src",{get:e,set:e}),Object.defineProperty(k.default.createElement("audio"),"src",{get:e,set:e}),Object.defineProperty(k.default.createElement("video"),"innerHTML",{get:e,set:e}),Object.defineProperty(k.default.createElement("audio"),"innerHTML",{get:e,set:e})}catch(e){return!1}return!0},Lr.supportsNativeTextTracks=function(){return Ee||Te&&pe},Lr.supportsNativeVideoTracks=function(){return!(!Lr.TEST_VID||!Lr.TEST_VID.videoTracks)},Lr.supportsNativeAudioTracks=function(){return!(!Lr.TEST_VID||!Lr.TEST_VID.audioTracks)},Lr.Events=["loadstart","suspend","abort","error","emptied","stalled","loadedmetadata","loadeddata","canplay","canplaythrough","playing","waiting","seeking","seeked","ended","durationchange","timeupdate","progress","play","pause","ratechange","resize","volumechange"],[["featuresVolumeControl","canControlVolume"],["featuresMuteControl","canMuteVolume"],["featuresPlaybackRate","canControlPlaybackRate"],["featuresSourceset","canOverrideAttributes"],["featuresNativeTextTracks","supportsNativeTextTracks"],["featuresNativeVideoTracks","supportsNativeVideoTracks"],["featuresNativeAudioTracks","supportsNativeAudioTracks"]].forEach((function(e){var t=e[0],i=e[1];Ir(Lr.prototype,t,(function(){return Lr[i]()}),!0)})),Lr.prototype.movingMediaElementInDOM=!Te,Lr.prototype.featuresFullscreenResize=!0,Lr.prototype.featuresProgressEvents=!0,Lr.prototype.featuresTimeupdateEvents=!0,Lr.patchCanPlayType=function(){he>=4&&!ce&&!pe&&(Tr=Lr.TEST_VID&&Lr.TEST_VID.constructor.prototype.canPlayType,Lr.TEST_VID.constructor.prototype.canPlayType=function(e){return e&&/^application\/(?:x-|vnd\.apple\.)mpegurl/i.test(e)?"maybe":Tr.call(this,e)})},Lr.unpatchCanPlayType=function(){var e=Lr.TEST_VID.constructor.prototype.canPlayType;return Tr&&(Lr.TEST_VID.constructor.prototype.canPlayType=Tr),e},Lr.patchCanPlayType(),Lr.disposeMediaElement=function(e){if(e){for(e.parentNode&&e.parentNode.removeChild(e);e.hasChildNodes();)e.removeChild(e.firstChild);e.removeAttribute("src"),"function"==typeof e.load&&function(){try{e.load()}catch(e){}}()}},Lr.resetMediaElement=function(e){if(e){for(var t=e.querySelectorAll("source"),i=t.length;i--;)e.removeChild(t[i]);e.removeAttribute("src"),"function"==typeof e.load&&function(){try{e.load()}catch(e){}}()}},["muted","defaultMuted","autoplay","controls","loop","playsinline"].forEach((function(e){Lr.prototype[e]=function(){return this.el_[e]||this.el_.hasAttribute(e)}})),["muted","defaultMuted","autoplay","loop","playsinline"].forEach((function(e){Lr.prototype["set"+Ht(e)]=function(t){this.el_[e]=t,t?this.el_.setAttribute(e,e):this.el_.removeAttribute(e)}})),["paused","currentTime","buffered","volume","poster","preload","error","seeking","seekable","ended","playbackRate","defaultPlaybackRate","disablePictureInPicture","played","networkState","readyState","videoWidth","videoHeight","crossOrigin"].forEach((function(e){Lr.prototype[e]=function(){return this.el_[e]}})),["volume","src","poster","preload","playbackRate","defaultPlaybackRate","disablePictureInPicture","crossOrigin"].forEach((function(e){Lr.prototype["set"+Ht(e)]=function(t){this.el_[e]=t}})),["pause","load","play"].forEach((function(e){Lr.prototype[e]=function(){return this.el_[e]()}})),Ui.withSourceHandlers(Lr),Lr.nativeSourceHandler={},Lr.nativeSourceHandler.canPlayType=function(e){try{return Lr.TEST_VID.canPlayType(e)}catch(e){return""}},Lr.nativeSourceHandler.canHandleSource=function(e,t){if(e.type)return Lr.nativeSourceHandler.canPlayType(e.type);if(e.src){var i=Ei(e.src);return Lr.nativeSourceHandler.canPlayType("video/"+i)}return""},Lr.nativeSourceHandler.handleSource=function(e,t,i){t.setSrc(e.src)},Lr.nativeSourceHandler.dispose=function(){},Lr.registerSourceHandler(Lr.nativeSourceHandler),Ui.registerTech("Html5",Lr);var xr=["progress","abort","suspend","emptied","stalled","loadedmetadata","loadeddata","timeupdate","resize","volumechange","texttrackchange"],Rr={canplay:"CanPlay",canplaythrough:"CanPlayThrough",playing:"Playing",seeked:"Seeked"},Dr=["tiny","xsmall","small","medium","large","xlarge","huge"],Or={};Dr.forEach((function(e){var t="x"===e.charAt(0)?"x-"+e.substring(1):e;Or[e]="vjs-layout-"+t}));var Ur={tiny:210,xsmall:320,small:425,medium:768,large:1440,xlarge:2560,huge:1/0},Mr=function(e){function t(i,n,r){var a;if(i.id=i.id||n.id||"vjs_video_"+ct(),(n=Z(t.getTagSettings(i),n)).initChildren=!1,n.createEl=!1,n.evented=!1,n.reportTouchActivity=!1,!n.language)if("function"==typeof i.closest){var s=i.closest("[lang]");s&&s.getAttribute&&(n.language=s.getAttribute("lang"))}else for(var o=i;o&&1===o.nodeType;){if(Ne(o).hasOwnProperty("lang")){n.language=o.getAttribute("lang");break}o=o.parentNode}if((a=e.call(this,null,n,r)||this).boundDocumentFullscreenChange_=function(e){return a.documentFullscreenChange_(e)},a.boundFullWindowOnEscKey_=function(e){return a.fullWindowOnEscKey(e)},a.boundUpdateStyleEl_=function(e){return a.updateStyleEl_(e)},a.boundApplyInitTime_=function(e){return a.applyInitTime_(e)},a.boundUpdateCurrentBreakpoint_=function(e){return a.updateCurrentBreakpoint_(e)},a.boundHandleTechClick_=function(e){return a.handleTechClick_(e)},a.boundHandleTechDoubleClick_=function(e){return a.handleTechDoubleClick_(e)},a.boundHandleTechTouchStart_=function(e){return a.handleTechTouchStart_(e)},a.boundHandleTechTouchMove_=function(e){return a.handleTechTouchMove_(e)},a.boundHandleTechTouchEnd_=function(e){return a.handleTechTouchEnd_(e)},a.boundHandleTechTap_=function(e){return a.handleTechTap_(e)},a.isFullscreen_=!1,a.log=X(a.id_),a.fsApi_=H,a.isPosterFromTech_=!1,a.queuedCallbacks_=[],a.isReady_=!1,a.hasStarted_=!1,a.userActive_=!1,a.debugEnabled_=!1,!a.options_||!a.options_.techOrder||!a.options_.techOrder.length)throw new Error("No techOrder specified. Did you overwrite videojs.options instead of just changing the properties you want to override?");if(a.tag=i,a.tagAttributes=i&&Ne(i),a.language(a.options_.language),n.languages){var u={};Object.getOwnPropertyNames(n.languages).forEach((function(e){u[e.toLowerCase()]=n.languages[e]})),a.languages_=u}else a.languages_=t.prototype.options_.languages;a.resetCache_(),a.poster_=n.poster||"",a.controls_=!!n.controls,i.controls=!1,i.removeAttribute("controls"),a.changingSrc_=!1,a.playCallbacks_=[],a.playTerminatedQueue_=[],i.hasAttribute("autoplay")?a.autoplay(!0):a.autoplay(a.options_.autoplay),n.plugins&&Object.keys(n.plugins).forEach((function(e){if("function"!=typeof a[e])throw new Error('plugin "'+e+'" does not exist')})),a.scrubbing_=!1,a.el_=a.createEl(),Bt(I.default(a),{eventBusKey:"el_"}),a.fsApi_.requestFullscreen&&(yt(k.default,a.fsApi_.fullscreenchange,a.boundDocumentFullscreenChange_),a.on(a.fsApi_.fullscreenchange,a.boundDocumentFullscreenChange_)),a.fluid_&&a.on(["playerreset","resize"],a.boundUpdateStyleEl_);var l=zt(a.options_);n.plugins&&Object.keys(n.plugins).forEach((function(e){a[e](n.plugins[e])})),n.debug&&a.debug(!0),a.options_.playerOptions=l,a.middleware_=[],a.playbackRates(n.playbackRates),a.initChildren(),a.isAudio("audio"===i.nodeName.toLowerCase()),a.controls()?a.addClass("vjs-controls-enabled"):a.addClass("vjs-controls-disabled"),a.el_.setAttribute("role","region"),a.isAudio()?a.el_.setAttribute("aria-label",a.localize("Audio Player")):a.el_.setAttribute("aria-label",a.localize("Video Player")),a.isAudio()&&a.addClass("vjs-audio"),a.flexNotSupported_()&&a.addClass("vjs-no-flex"),ye&&a.addClass("vjs-touch-enabled"),Te||a.addClass("vjs-workinghover"),t.players[a.id_]=I.default(a);var h="7.15.4".split(".")[0];return a.addClass("vjs-v"+h),a.userActive(!0),a.reportUserActivity(),a.one("play",(function(e){return a.listenForUserActivity_(e)})),a.on("stageclick",(function(e){return a.handleStageClick_(e)})),a.on("keydown",(function(e){return a.handleKeyDown(e)})),a.on("languagechange",(function(e){return a.handleLanguagechange(e)})),a.breakpoints(a.options_.breakpoints),a.responsive(a.options_.responsive),a}L.default(t,e);var i=t.prototype;return i.dispose=function(){var i=this;this.trigger("dispose"),this.off("dispose"),bt(k.default,this.fsApi_.fullscreenchange,this.boundDocumentFullscreenChange_),bt(k.default,"keydown",this.boundFullWindowOnEscKey_),this.styleEl_&&this.styleEl_.parentNode&&(this.styleEl_.parentNode.removeChild(this.styleEl_),this.styleEl_=null),t.players[this.id_]=null,this.tag&&this.tag.player&&(this.tag.player=null),this.el_&&this.el_.player&&(this.el_.player=null),this.tech_&&(this.tech_.dispose(),this.isPosterFromTech_=!1,this.poster_=""),this.playerElIngest_&&(this.playerElIngest_=null),this.tag&&(this.tag=null),Fi[this.id()]=null,Oi.names.forEach((function(e){var t=Oi[e],n=i[t.getterName]();n&&n.off&&n.off()})),e.prototype.dispose.call(this)},i.createEl=function(){var t,i=this.tag,n=this.playerElIngest_=i.parentNode&&i.parentNode.hasAttribute&&i.parentNode.hasAttribute("data-vjs-player"),r="video-js"===this.tag.tagName.toLowerCase();n?t=this.el_=i.parentNode:r||(t=this.el_=e.prototype.createEl.call(this,"div"));var a=Ne(i);if(r){for(t=this.el_=i,i=this.tag=k.default.createElement("video");t.children.length;)i.appendChild(t.firstChild);Oe(t,"video-js")||Ue(t,"video-js"),t.appendChild(i),n=this.playerElIngest_=t,Object.keys(t).forEach((function(e){try{i[e]=t[e]}catch(e){}}))}if(i.setAttribute("tabindex","-1"),a.tabindex="-1",(_e||pe&&ve)&&(i.setAttribute("role","application"),a.role="application"),i.removeAttribute("width"),i.removeAttribute("height"),"width"in a&&delete a.width,"height"in a&&delete a.height,Object.getOwnPropertyNames(a).forEach((function(e){r&&"class"===e||t.setAttribute(e,a[e]),r&&i.setAttribute(e,a[e])})),i.playerId=i.id,i.id+="_html5_api",i.className="vjs-tech",i.player=t.player=this,this.addClass("vjs-paused"),!0!==C.default.VIDEOJS_NO_DYNAMIC_STYLE){this.styleEl_=lt("vjs-styles-dimensions");var s=tt(".vjs-styles-defaults"),o=tt("head");o.insertBefore(this.styleEl_,s?s.nextSibling:o.firstChild)}this.fill_=!1,this.fluid_=!1,this.width(this.options_.width),this.height(this.options_.height),this.fill(this.options_.fill),this.fluid(this.options_.fluid),this.aspectRatio(this.options_.aspectRatio),this.crossOrigin(this.options_.crossOrigin||this.options_.crossorigin);for(var u=i.getElementsByTagName("a"),l=0;l0?this.videoWidth()+":"+this.videoHeight():"16:9").split(":"),r=n[1]/n[0];e=void 0!==this.width_?this.width_:void 0!==this.height_?this.height_/r:this.videoWidth()||300,t=void 0!==this.height_?this.height_:e*r,i=/^[^a-zA-Z]/.test(this.id())?"dimensions-"+this.id():this.id()+"-dimensions",this.addClass(i),ht(this.styleEl_,"\n ."+i+" {\n width: "+e+"px;\n height: "+t+"px;\n }\n\n ."+i+".vjs-fluid {\n padding-top: "+100*r+"%;\n }\n ")}else{var a="number"==typeof this.width_?this.width_:this.options_.width,s="number"==typeof this.height_?this.height_:this.options_.height,o=this.tech_&&this.tech_.el();o&&(a>=0&&(o.width=a),s>=0&&(o.height=s))}},i.loadTech_=function(e,t){var i=this;this.tech_&&this.unloadTech_();var n=Ht(e),r=e.charAt(0).toLowerCase()+e.slice(1);"Html5"!==n&&this.tag&&(Ui.getTech("Html5").disposeMediaElement(this.tag),this.tag.player=null,this.tag=null),this.techName_=n,this.isReady_=!1;var a=this.autoplay();("string"==typeof this.autoplay()||!0===this.autoplay()&&this.options_.normalizeAutoplay)&&(a=!1);var s={source:t,autoplay:a,nativeControlsForTouch:this.options_.nativeControlsForTouch,playerId:this.id(),techId:this.id()+"_"+r+"_api",playsinline:this.options_.playsinline,preload:this.options_.preload,loop:this.options_.loop,disablePictureInPicture:this.options_.disablePictureInPicture,muted:this.options_.muted,poster:this.poster(),language:this.language(),playerElIngest:this.playerElIngest_||!1,"vtt.js":this.options_["vtt.js"],canOverridePoster:!!this.options_.techCanOverridePoster,enableSourceset:this.options_.enableSourceset,Promise:this.options_.Promise};Oi.names.forEach((function(e){var t=Oi[e];s[t.getterName]=i[t.privateName]})),Z(s,this.options_[n]),Z(s,this.options_[r]),Z(s,this.options_[e.toLowerCase()]),this.tag&&(s.tag=this.tag),t&&t.src===this.cache_.src&&this.cache_.currentTime>0&&(s.startTime=this.cache_.currentTime);var o=Ui.getTech(e);if(!o)throw new Error("No Tech named '"+n+"' exists! '"+n+"' should be registered using videojs.registerTech()'");this.tech_=new o(s),this.tech_.ready(Ct(this,this.handleTechReady_),!0),ai(this.textTracksJson_||[],this.tech_),xr.forEach((function(e){i.on(i.tech_,e,(function(t){return i["handleTech"+Ht(e)+"_"](t)}))})),Object.keys(Rr).forEach((function(e){i.on(i.tech_,e,(function(t){0===i.tech_.playbackRate()&&i.tech_.seeking()?i.queuedCallbacks_.push({callback:i["handleTech"+Rr[e]+"_"].bind(i),event:t}):i["handleTech"+Rr[e]+"_"](t)}))})),this.on(this.tech_,"loadstart",(function(e){return i.handleTechLoadStart_(e)})),this.on(this.tech_,"sourceset",(function(e){return i.handleTechSourceset_(e)})),this.on(this.tech_,"waiting",(function(e){return i.handleTechWaiting_(e)})),this.on(this.tech_,"ended",(function(e){return i.handleTechEnded_(e)})),this.on(this.tech_,"seeking",(function(e){return i.handleTechSeeking_(e)})),this.on(this.tech_,"play",(function(e){return i.handleTechPlay_(e)})),this.on(this.tech_,"firstplay",(function(e){return i.handleTechFirstPlay_(e)})),this.on(this.tech_,"pause",(function(e){return i.handleTechPause_(e)})),this.on(this.tech_,"durationchange",(function(e){return i.handleTechDurationChange_(e)})),this.on(this.tech_,"fullscreenchange",(function(e,t){return i.handleTechFullscreenChange_(e,t)})),this.on(this.tech_,"fullscreenerror",(function(e,t){return i.handleTechFullscreenError_(e,t)})),this.on(this.tech_,"enterpictureinpicture",(function(e){return i.handleTechEnterPictureInPicture_(e)})),this.on(this.tech_,"leavepictureinpicture",(function(e){return i.handleTechLeavePictureInPicture_(e)})),this.on(this.tech_,"error",(function(e){return i.handleTechError_(e)})),this.on(this.tech_,"posterchange",(function(e){return i.handleTechPosterChange_(e)})),this.on(this.tech_,"textdata",(function(e){return i.handleTechTextData_(e)})),this.on(this.tech_,"ratechange",(function(e){return i.handleTechRateChange_(e)})),this.on(this.tech_,"loadedmetadata",this.boundUpdateStyleEl_),this.usingNativeControls(this.techGet_("controls")),this.controls()&&!this.usingNativeControls()&&this.addTechControlsListeners_(),this.tech_.el().parentNode===this.el()||"Html5"===n&&this.tag||De(this.tech_.el(),this.el()),this.tag&&(this.tag.player=null,this.tag=null)},i.unloadTech_=function(){var e=this;Oi.names.forEach((function(t){var i=Oi[t];e[i.privateName]=e[i.getterName]()})),this.textTracksJson_=ri(this.tech_),this.isReady_=!1,this.tech_.dispose(),this.tech_=!1,this.isPosterFromTech_&&(this.poster_="",this.trigger("posterchange")),this.isPosterFromTech_=!1},i.tech=function(e){return void 0===e&&K.warn("Using the tech directly can be dangerous. I hope you know what you're doing.\nSee https://github.com/videojs/video.js/issues/2617 for more info.\n"),this.tech_},i.addTechControlsListeners_=function(){this.removeTechControlsListeners_(),this.on(this.tech_,"click",this.boundHandleTechClick_),this.on(this.tech_,"dblclick",this.boundHandleTechDoubleClick_),this.on(this.tech_,"touchstart",this.boundHandleTechTouchStart_),this.on(this.tech_,"touchmove",this.boundHandleTechTouchMove_),this.on(this.tech_,"touchend",this.boundHandleTechTouchEnd_),this.on(this.tech_,"tap",this.boundHandleTechTap_)},i.removeTechControlsListeners_=function(){this.off(this.tech_,"tap",this.boundHandleTechTap_),this.off(this.tech_,"touchstart",this.boundHandleTechTouchStart_),this.off(this.tech_,"touchmove",this.boundHandleTechTouchMove_),this.off(this.tech_,"touchend",this.boundHandleTechTouchEnd_),this.off(this.tech_,"click",this.boundHandleTechClick_),this.off(this.tech_,"dblclick",this.boundHandleTechDoubleClick_)},i.handleTechReady_=function(){this.triggerReady(),this.cache_.volume&&this.techCall_("setVolume",this.cache_.volume),this.handleTechPosterChange_(),this.handleTechDurationChange_()},i.handleTechLoadStart_=function(){this.removeClass("vjs-ended"),this.removeClass("vjs-seeking"),this.error(null),this.handleTechDurationChange_(),this.paused()?(this.hasStarted(!1),this.trigger("loadstart")):(this.trigger("loadstart"),this.trigger("firstplay")),this.manualAutoplay_(!0===this.autoplay()&&this.options_.normalizeAutoplay?"play":this.autoplay())},i.manualAutoplay_=function(e){var t=this;if(this.tech_&&"string"==typeof e){var i,n=function(){var e=t.muted();t.muted(!0);var i=function(){t.muted(e)};t.playTerminatedQueue_.push(i);var n=t.play();if(ti(n))return n.catch((function(e){throw i(),new Error("Rejection at manualAutoplay. Restoring muted value. "+(e||""))}))};if("any"!==e||this.muted()?i="muted"!==e||this.muted()?this.play():n():ti(i=this.play())&&(i=i.catch(n)),ti(i))return i.then((function(){t.trigger({type:"autoplay-success",autoplay:e})})).catch((function(){t.trigger({type:"autoplay-failure",autoplay:e})}))}},i.updateSourceCaches_=function(e){void 0===e&&(e="");var t=e,i="";"string"!=typeof t&&(t=e.src,i=e.type),this.cache_.source=this.cache_.source||{},this.cache_.sources=this.cache_.sources||[],t&&!i&&(i=function(e,t){if(!t)return"";if(e.cache_.source.src===t&&e.cache_.source.type)return e.cache_.source.type;var i=e.cache_.sources.filter((function(e){return e.src===t}));if(i.length)return i[0].type;for(var n=e.$$("source"),r=0;r0&&0===this.cache_.lastPlaybackRate&&(this.queuedCallbacks_.forEach((function(e){return e.callback(e.event)})),this.queuedCallbacks_=[]),this.cache_.lastPlaybackRate=this.tech_.playbackRate(),this.trigger("ratechange")},i.handleTechWaiting_=function(){var e=this;this.addClass("vjs-waiting"),this.trigger("waiting");var t=this.currentTime();this.on("timeupdate",(function i(){t!==e.currentTime()&&(e.removeClass("vjs-waiting"),e.off("timeupdate",i))}))},i.handleTechCanPlay_=function(){this.removeClass("vjs-waiting"),this.trigger("canplay")},i.handleTechCanPlayThrough_=function(){this.removeClass("vjs-waiting"),this.trigger("canplaythrough")},i.handleTechPlaying_=function(){this.removeClass("vjs-waiting"),this.trigger("playing")},i.handleTechSeeking_=function(){this.addClass("vjs-seeking"),this.trigger("seeking")},i.handleTechSeeked_=function(){this.removeClass("vjs-seeking"),this.removeClass("vjs-ended"),this.trigger("seeked")},i.handleTechFirstPlay_=function(){this.options_.starttime&&(K.warn("Passing the `starttime` option to the player will be deprecated in 6.0"),this.currentTime(this.options_.starttime)),this.addClass("vjs-has-started"),this.trigger("firstplay")},i.handleTechPause_=function(){this.removeClass("vjs-playing"),this.addClass("vjs-paused"),this.trigger("pause")},i.handleTechEnded_=function(){this.addClass("vjs-ended"),this.removeClass("vjs-waiting"),this.options_.loop?(this.currentTime(0),this.play()):this.paused()||this.pause(),this.trigger("ended")},i.handleTechDurationChange_=function(){this.duration(this.techGet_("duration"))},i.handleTechClick_=function(e){this.controls_&&(this.paused()?ii(this.play()):this.pause())},i.handleTechDoubleClick_=function(e){this.controls_&&(Array.prototype.some.call(this.$$(".vjs-control-bar, .vjs-modal-dialog"),(function(t){return t.contains(e.target)}))||void 0!==this.options_&&void 0!==this.options_.userActions&&void 0!==this.options_.userActions.doubleClick&&!1===this.options_.userActions.doubleClick||(void 0!==this.options_&&void 0!==this.options_.userActions&&"function"==typeof this.options_.userActions.doubleClick?this.options_.userActions.doubleClick.call(this,e):this.isFullscreen()?this.exitFullscreen():this.requestFullscreen()))},i.handleTechTap_=function(){this.userActive(!this.userActive())},i.handleTechTouchStart_=function(){this.userWasActive=this.userActive()},i.handleTechTouchMove_=function(){this.userWasActive&&this.reportUserActivity()},i.handleTechTouchEnd_=function(e){e.cancelable&&e.preventDefault()},i.handleStageClick_=function(){this.reportUserActivity()},i.toggleFullscreenClass_=function(){this.isFullscreen()?this.addClass("vjs-fullscreen"):this.removeClass("vjs-fullscreen")},i.documentFullscreenChange_=function(e){var t=e.target.player;if(!t||t===this){var i=this.el(),n=k.default[this.fsApi_.fullscreenElement]===i;!n&&i.matches?n=i.matches(":"+this.fsApi_.fullscreen):!n&&i.msMatchesSelector&&(n=i.msMatchesSelector(":"+this.fsApi_.fullscreen)),this.isFullscreen(n)}},i.handleTechFullscreenChange_=function(e,t){t&&(t.nativeIOSFullscreen&&this.toggleClass("vjs-ios-native-fs"),this.isFullscreen(t.isFullscreen))},i.handleTechFullscreenError_=function(e,t){this.trigger("fullscreenerror",t)},i.togglePictureInPictureClass_=function(){this.isInPictureInPicture()?this.addClass("vjs-picture-in-picture"):this.removeClass("vjs-picture-in-picture")},i.handleTechEnterPictureInPicture_=function(e){this.isInPictureInPicture(!0)},i.handleTechLeavePictureInPicture_=function(e){this.isInPictureInPicture(!1)},i.handleTechError_=function(){var e=this.tech_.error();this.error(e)},i.handleTechTextData_=function(){var e=null;arguments.length>1&&(e=arguments[1]),this.trigger("textdata",e)},i.getCache=function(){return this.cache_},i.resetCache_=function(){this.cache_={currentTime:0,initTime:0,inactivityTimeout:this.options_.inactivityTimeout,duration:NaN,lastVolume:1,lastPlaybackRate:this.defaultPlaybackRate(),media:null,src:"",source:{},sources:[],playbackRates:[],volume:1}},i.techCall_=function(e,t){this.ready((function(){if(e in Hi)return function(e,t,i,n){return t[i](e.reduce(Gi(i),n))}(this.middleware_,this.tech_,e,t);if(e in zi)return ji(this.middleware_,this.tech_,e,t);try{this.tech_&&this.tech_[e](t)}catch(e){throw K(e),e}}),!0)},i.techGet_=function(e){if(this.tech_&&this.tech_.isReady_){if(e in Vi)return function(e,t,i){return e.reduceRight(Gi(i),t[i]())}(this.middleware_,this.tech_,e);if(e in zi)return ji(this.middleware_,this.tech_,e);try{return this.tech_[e]()}catch(t){if(void 0===this.tech_[e])throw K("Video.js: "+e+" method not defined for "+this.techName_+" playback technology.",t),t;if("TypeError"===t.name)throw K("Video.js: "+e+" unavailable on "+this.techName_+" playback technology element.",t),this.tech_.isReady_=!1,t;throw K(t),t}}},i.play=function(){var e=this,t=this.options_.Promise||C.default.Promise;return t?new t((function(t){e.play_(t)})):this.play_()},i.play_=function(e){var t=this;void 0===e&&(e=ii),this.playCallbacks_.push(e);var i=Boolean(!this.changingSrc_&&(this.src()||this.currentSrc()));if(this.waitToPlay_&&(this.off(["ready","loadstart"],this.waitToPlay_),this.waitToPlay_=null),!this.isReady_||!i)return this.waitToPlay_=function(e){t.play_()},this.one(["ready","loadstart"],this.waitToPlay_),void(i||!Ee&&!Te||this.load());var n=this.techGet_("play");null===n?this.runPlayTerminatedQueue_():this.runPlayCallbacks_(n)},i.runPlayTerminatedQueue_=function(){var e=this.playTerminatedQueue_.slice(0);this.playTerminatedQueue_=[],e.forEach((function(e){e()}))},i.runPlayCallbacks_=function(e){var t=this.playCallbacks_.slice(0);this.playCallbacks_=[],this.playTerminatedQueue_=[],t.forEach((function(t){t(e)}))},i.pause=function(){this.techCall_("pause")},i.paused=function(){return!1!==this.techGet_("paused")},i.played=function(){return this.techGet_("played")||$t(0,0)},i.scrubbing=function(e){if(void 0===e)return this.scrubbing_;this.scrubbing_=!!e,this.techCall_("setScrubbing",this.scrubbing_),e?this.addClass("vjs-scrubbing"):this.removeClass("vjs-scrubbing")},i.currentTime=function(e){return void 0!==e?(e<0&&(e=0),this.isReady_&&!this.changingSrc_&&this.tech_&&this.tech_.isReady_?(this.techCall_("setCurrentTime",e),void(this.cache_.initTime=0)):(this.cache_.initTime=e,this.off("canplay",this.boundApplyInitTime_),void this.one("canplay",this.boundApplyInitTime_))):(this.cache_.currentTime=this.techGet_("currentTime")||0,this.cache_.currentTime)},i.applyInitTime_=function(){this.currentTime(this.cache_.initTime)},i.duration=function(e){if(void 0===e)return void 0!==this.cache_.duration?this.cache_.duration:NaN;(e=parseFloat(e))<0&&(e=1/0),e!==this.cache_.duration&&(this.cache_.duration=e,e===1/0?this.addClass("vjs-live"):this.removeClass("vjs-live"),isNaN(e)||this.trigger("durationchange"))},i.remainingTime=function(){return this.duration()-this.currentTime()},i.remainingTimeDisplay=function(){return Math.floor(this.duration())-Math.floor(this.currentTime())},i.buffered=function(){var e=this.techGet_("buffered");return e&&e.length||(e=$t(0,0)),e},i.bufferedPercent=function(){return Jt(this.buffered(),this.duration())},i.bufferedEnd=function(){var e=this.buffered(),t=this.duration(),i=e.end(e.length-1);return i>t&&(i=t),i},i.volume=function(e){var t;return void 0!==e?(t=Math.max(0,Math.min(1,parseFloat(e))),this.cache_.volume=t,this.techCall_("setVolume",t),void(t>0&&this.lastVolume_(t))):(t=parseFloat(this.techGet_("volume")),isNaN(t)?1:t)},i.muted=function(e){if(void 0===e)return this.techGet_("muted")||!1;this.techCall_("setMuted",e)},i.defaultMuted=function(e){return void 0!==e?this.techCall_("setDefaultMuted",e):this.techGet_("defaultMuted")||!1},i.lastVolume_=function(e){if(void 0===e||0===e)return this.cache_.lastVolume;this.cache_.lastVolume=e},i.supportsFullScreen=function(){return this.techGet_("supportsFullScreen")||!1},i.isFullscreen=function(e){if(void 0!==e){var t=this.isFullscreen_;return this.isFullscreen_=Boolean(e),this.isFullscreen_!==t&&this.fsApi_.prefixed&&this.trigger("fullscreenchange"),void this.toggleFullscreenClass_()}return this.isFullscreen_},i.requestFullscreen=function(e){var t=this.options_.Promise||C.default.Promise;if(t){var i=this;return new t((function(t,n){function r(){i.off("fullscreenerror",s),i.off("fullscreenchange",a)}function a(){r(),t()}function s(e,t){r(),n(t)}i.one("fullscreenchange",a),i.one("fullscreenerror",s);var o=i.requestFullscreenHelper_(e);o&&(o.then(r,r),o.then(t,n))}))}return this.requestFullscreenHelper_()},i.requestFullscreenHelper_=function(e){var t,i=this;if(this.fsApi_.prefixed||(t=this.options_.fullscreen&&this.options_.fullscreen.options||{},void 0!==e&&(t=e)),this.fsApi_.requestFullscreen){var n=this.el_[this.fsApi_.requestFullscreen](t);return n&&n.then((function(){return i.isFullscreen(!0)}),(function(){return i.isFullscreen(!1)})),n}this.tech_.supportsFullScreen()&&!0==!this.options_.preferFullWindow?this.techCall_("enterFullScreen"):this.enterFullWindow()},i.exitFullscreen=function(){var e=this.options_.Promise||C.default.Promise;if(e){var t=this;return new e((function(e,i){function n(){t.off("fullscreenerror",a),t.off("fullscreenchange",r)}function r(){n(),e()}function a(e,t){n(),i(t)}t.one("fullscreenchange",r),t.one("fullscreenerror",a);var s=t.exitFullscreenHelper_();s&&(s.then(n,n),s.then(e,i))}))}return this.exitFullscreenHelper_()},i.exitFullscreenHelper_=function(){var e=this;if(this.fsApi_.requestFullscreen){var t=k.default[this.fsApi_.exitFullscreen]();return t&&ii(t.then((function(){return e.isFullscreen(!1)}))),t}this.tech_.supportsFullScreen()&&!0==!this.options_.preferFullWindow?this.techCall_("exitFullScreen"):this.exitFullWindow()},i.enterFullWindow=function(){this.isFullscreen(!0),this.isFullWindow=!0,this.docOrigOverflow=k.default.documentElement.style.overflow,yt(k.default,"keydown",this.boundFullWindowOnEscKey_),k.default.documentElement.style.overflow="hidden",Ue(k.default.body,"vjs-full-window"),this.trigger("enterFullWindow")},i.fullWindowOnEscKey=function(e){R.default.isEventKey(e,"Esc")&&!0===this.isFullscreen()&&(this.isFullWindow?this.exitFullWindow():this.exitFullscreen())},i.exitFullWindow=function(){this.isFullscreen(!1),this.isFullWindow=!1,bt(k.default,"keydown",this.boundFullWindowOnEscKey_),k.default.documentElement.style.overflow=this.docOrigOverflow,Me(k.default.body,"vjs-full-window"),this.trigger("exitFullWindow")},i.disablePictureInPicture=function(e){if(void 0===e)return this.techGet_("disablePictureInPicture");this.techCall_("setDisablePictureInPicture",e),this.options_.disablePictureInPicture=e,this.trigger("disablepictureinpicturechanged")},i.isInPictureInPicture=function(e){return void 0!==e?(this.isInPictureInPicture_=!!e,void this.togglePictureInPictureClass_()):!!this.isInPictureInPicture_},i.requestPictureInPicture=function(){if("pictureInPictureEnabled"in k.default&&!1===this.disablePictureInPicture())return this.techGet_("requestPictureInPicture")},i.exitPictureInPicture=function(){if("pictureInPictureEnabled"in k.default)return k.default.exitPictureInPicture()},i.handleKeyDown=function(e){var t=this.options_.userActions;if(t&&t.hotkeys){(function(e){var t=e.tagName.toLowerCase();if(e.isContentEditable)return!0;if("input"===t)return-1===["button","checkbox","hidden","radio","reset","submit"].indexOf(e.type);return-1!==["textarea"].indexOf(t)})(this.el_.ownerDocument.activeElement)||("function"==typeof t.hotkeys?t.hotkeys.call(this,e):this.handleHotkeys(e))}},i.handleHotkeys=function(e){var t=this.options_.userActions?this.options_.userActions.hotkeys:{},i=t.fullscreenKey,n=void 0===i?function(e){return R.default.isEventKey(e,"f")}:i,r=t.muteKey,a=void 0===r?function(e){return R.default.isEventKey(e,"m")}:r,s=t.playPauseKey,o=void 0===s?function(e){return R.default.isEventKey(e,"k")||R.default.isEventKey(e,"Space")}:s;if(n.call(this,e)){e.preventDefault(),e.stopPropagation();var u=Kt.getComponent("FullscreenToggle");!1!==k.default[this.fsApi_.fullscreenEnabled]&&u.prototype.handleClick.call(this,e)}else if(a.call(this,e)){e.preventDefault(),e.stopPropagation(),Kt.getComponent("MuteToggle").prototype.handleClick.call(this,e)}else if(o.call(this,e)){e.preventDefault(),e.stopPropagation(),Kt.getComponent("PlayToggle").prototype.handleClick.call(this,e)}},i.canPlayType=function(e){for(var t,i=0,n=this.options_.techOrder;i1?i.handleSrc_(n.slice(1)):(i.changingSrc_=!1,i.setTimeout((function(){this.error({code:4,message:this.localize(this.options_.notSupportedMessage)})}),0),void i.triggerReady());a=r,s=i.tech_,a.forEach((function(e){return e.setTech&&e.setTech(s)}))})),this.options_.retryOnError&&n.length>1){var r=function(){i.error(null),i.handleSrc_(n.slice(1),!0)},a=function(){i.off("error",r)};this.one("error",r),this.one("playing",a),this.resetRetryOnError_=function(){i.off("error",r),i.off("playing",a)}}}else this.setTimeout((function(){this.error({code:4,message:this.localize(this.options_.notSupportedMessage)})}),0)},i.src=function(e){return this.handleSrc_(e,!1)},i.src_=function(e){var t,i,n=this,r=this.selectSource([e]);return!r||(t=r.tech,i=this.techName_,Ht(t)!==Ht(i)?(this.changingSrc_=!0,this.loadTech_(r.tech,r.source),this.tech_.ready((function(){n.changingSrc_=!1})),!1):(this.ready((function(){this.tech_.constructor.prototype.hasOwnProperty("setSource")?this.techCall_("setSource",e):this.techCall_("src",e.src),this.changingSrc_=!1}),!0),!1))},i.load=function(){this.techCall_("load")},i.reset=function(){var e=this,t=this.options_.Promise||C.default.Promise;this.paused()||!t?this.doReset_():ii(this.play().then((function(){return e.doReset_()})))},i.doReset_=function(){this.tech_&&this.tech_.clearTracks("text"),this.resetCache_(),this.poster(""),this.loadTech_(this.options_.techOrder[0],null),this.techCall_("reset"),this.resetControlBarUI_(),Lt(this)&&this.trigger("playerreset")},i.resetControlBarUI_=function(){this.resetProgressBar_(),this.resetPlaybackRate_(),this.resetVolumeBar_()},i.resetProgressBar_=function(){this.currentTime(0);var e=this.controlBar,t=e.durationDisplay,i=e.remainingTimeDisplay;t&&t.updateContent(),i&&i.updateContent()},i.resetPlaybackRate_=function(){this.playbackRate(this.defaultPlaybackRate()),this.handleTechRateChange_()},i.resetVolumeBar_=function(){this.volume(1),this.trigger("volumechange")},i.currentSources=function(){var e=this.currentSource(),t=[];return 0!==Object.keys(e).length&&t.push(e),this.cache_.sources||t},i.currentSource=function(){return this.cache_.source||{}},i.currentSrc=function(){return this.currentSource()&&this.currentSource().src||""},i.currentType=function(){return this.currentSource()&&this.currentSource().type||""},i.preload=function(e){return void 0!==e?(this.techCall_("setPreload",e),void(this.options_.preload=e)):this.techGet_("preload")},i.autoplay=function(e){if(void 0===e)return this.options_.autoplay||!1;var t;"string"==typeof e&&/(any|play|muted)/.test(e)||!0===e&&this.options_.normalizeAutoplay?(this.options_.autoplay=e,this.manualAutoplay_("string"==typeof e?e:"play"),t=!1):this.options_.autoplay=!!e,t=void 0===t?this.options_.autoplay:t,this.tech_&&this.techCall_("setAutoplay",t)},i.playsinline=function(e){return void 0!==e?(this.techCall_("setPlaysinline",e),this.options_.playsinline=e,this):this.techGet_("playsinline")},i.loop=function(e){return void 0!==e?(this.techCall_("setLoop",e),void(this.options_.loop=e)):this.techGet_("loop")},i.poster=function(e){if(void 0===e)return this.poster_;e||(e=""),e!==this.poster_&&(this.poster_=e,this.techCall_("setPoster",e),this.isPosterFromTech_=!1,this.trigger("posterchange"))},i.handleTechPosterChange_=function(){if((!this.poster_||this.options_.techCanOverridePoster)&&this.tech_&&this.tech_.poster){var e=this.tech_.poster()||"";e!==this.poster_&&(this.poster_=e,this.isPosterFromTech_=!0,this.trigger("posterchange"))}},i.controls=function(e){if(void 0===e)return!!this.controls_;e=!!e,this.controls_!==e&&(this.controls_=e,this.usingNativeControls()&&this.techCall_("setControls",e),this.controls_?(this.removeClass("vjs-controls-disabled"),this.addClass("vjs-controls-enabled"),this.trigger("controlsenabled"),this.usingNativeControls()||this.addTechControlsListeners_()):(this.removeClass("vjs-controls-enabled"),this.addClass("vjs-controls-disabled"),this.trigger("controlsdisabled"),this.usingNativeControls()||this.removeTechControlsListeners_()))},i.usingNativeControls=function(e){if(void 0===e)return!!this.usingNativeControls_;e=!!e,this.usingNativeControls_!==e&&(this.usingNativeControls_=e,this.usingNativeControls_?(this.addClass("vjs-using-native-controls"),this.trigger("usingnativecontrols")):(this.removeClass("vjs-using-native-controls"),this.trigger("usingcustomcontrols")))},i.error=function(e){var t=this;if(void 0===e)return this.error_||null;if(j("beforeerror").forEach((function(i){var n=i(t,e);ee(n)&&!Array.isArray(n)||"string"==typeof n||"number"==typeof n||null===n?e=n:t.log.error("please return a value that MediaError expects in beforeerror hooks")})),this.options_.suppressNotSupportedError&&e&&4===e.code){var i=function(){this.error(e)};return this.options_.suppressNotSupportedError=!1,this.any(["click","touchstart"],i),void this.one("loadstart",(function(){this.off(["click","touchstart"],i)}))}if(null===e)return this.error_=e,this.removeClass("vjs-error"),void(this.errorDisplay&&this.errorDisplay.close());this.error_=new Zt(e),this.addClass("vjs-error"),K.error("(CODE:"+this.error_.code+" "+Zt.errorTypes[this.error_.code]+")",this.error_.message,this.error_),this.trigger("error"),j("error").forEach((function(e){return e(t,t.error_)}))},i.reportUserActivity=function(e){this.userActivity_=!0},i.userActive=function(e){if(void 0===e)return this.userActive_;if((e=!!e)!==this.userActive_){if(this.userActive_=e,this.userActive_)return this.userActivity_=!0,this.removeClass("vjs-user-inactive"),this.addClass("vjs-user-active"),void this.trigger("useractive");this.tech_&&this.tech_.one("mousemove",(function(e){e.stopPropagation(),e.preventDefault()})),this.userActivity_=!1,this.removeClass("vjs-user-active"),this.addClass("vjs-user-inactive"),this.trigger("userinactive")}},i.listenForUserActivity_=function(){var e,t,i,n=Ct(this,this.reportUserActivity),r=function(t){n(),this.clearInterval(e)};this.on("mousedown",(function(){n(),this.clearInterval(e),e=this.setInterval(n,250)})),this.on("mousemove",(function(e){e.screenX===t&&e.screenY===i||(t=e.screenX,i=e.screenY,n())})),this.on("mouseup",r),this.on("mouseleave",r);var a,s=this.getChild("controlBar");!s||Te||le||(s.on("mouseenter",(function(e){0!==this.player().options_.inactivityTimeout&&(this.player().cache_.inactivityTimeout=this.player().options_.inactivityTimeout),this.player().options_.inactivityTimeout=0})),s.on("mouseleave",(function(e){this.player().options_.inactivityTimeout=this.player().cache_.inactivityTimeout}))),this.on("keydown",n),this.on("keyup",n),this.setInterval((function(){if(this.userActivity_){this.userActivity_=!1,this.userActive(!0),this.clearTimeout(a);var e=this.options_.inactivityTimeout;e<=0||(a=this.setTimeout((function(){this.userActivity_||this.userActive(!1)}),e))}}),250)},i.playbackRate=function(e){if(void 0===e)return this.tech_&&this.tech_.featuresPlaybackRate?this.cache_.lastPlaybackRate||this.techGet_("playbackRate"):1;this.techCall_("setPlaybackRate",e)},i.defaultPlaybackRate=function(e){return void 0!==e?this.techCall_("setDefaultPlaybackRate",e):this.tech_&&this.tech_.featuresPlaybackRate?this.techGet_("defaultPlaybackRate"):1},i.isAudio=function(e){if(void 0===e)return!!this.isAudio_;this.isAudio_=!!e},i.addTextTrack=function(e,t,i){if(this.tech_)return this.tech_.addTextTrack(e,t,i)},i.addRemoteTextTrack=function(e,t){if(this.tech_)return this.tech_.addRemoteTextTrack(e,t)},i.removeRemoteTextTrack=function(e){void 0===e&&(e={});var t=e.track;if(t||(t=e),this.tech_)return this.tech_.removeRemoteTextTrack(t)},i.getVideoPlaybackQuality=function(){return this.techGet_("getVideoPlaybackQuality")},i.videoWidth=function(){return this.tech_&&this.tech_.videoWidth&&this.tech_.videoWidth()||0},i.videoHeight=function(){return this.tech_&&this.tech_.videoHeight&&this.tech_.videoHeight()||0},i.language=function(e){if(void 0===e)return this.language_;this.language_!==String(e).toLowerCase()&&(this.language_=String(e).toLowerCase(),Lt(this)&&this.trigger("languagechange"))},i.languages=function(){return zt(t.prototype.options_.languages,this.languages_)},i.toJSON=function(){var e=zt(this.options_),t=e.tracks;e.tracks=[];for(var i=0;i"):function(){}},Jr=function(e,t){var i,n=[];if(e&&e.length)for(i=0;i=t}))},ea=function(e,t){return Jr(e,(function(e){return e-1/30>=t}))},ta=function(e){var t=[];if(!e||!e.length)return"";for(var i=0;i "+e.end(i));return t.join(", ")},ia=function(e){for(var t=[],i=0;i0;return i&&t.serverControl&&t.serverControl.partHoldBack?t.serverControl.partHoldBack:i&&t.partTargetDuration?3*t.partTargetDuration:t.serverControl&&t.serverControl.holdBack?t.serverControl.holdBack:t.targetDuration?3*t.targetDuration:0},la=function(e,t,i){if(void 0===t&&(t=e.mediaSequence+e.segments.length),tr){var s=[r,n];n=s[0],r=s[1]}if(n<0){for(var o=n;oDate.now()},pa=function(e){return e.excludeUntil&&e.excludeUntil===1/0},ma=function(e){var t=fa(e);return!e.disabled&&!t},_a=function(e,t){return t.attributes&&t.attributes[e]},ga=function(e,t){if(1===e.playlists.length)return!0;var i=t.attributes.BANDWIDTH||Number.MAX_VALUE;return 0===e.playlists.filter((function(e){return!!ma(e)&&(e.attributes.BANDWIDTH||0)0)for(var c=l-1;c>=0;c--){var f=u[c];if(o+=f.duration,s){if(o<0)continue}else if(o+1/30<=0)continue;return{partIndex:f.partIndex,segmentIndex:f.segmentIndex,startTime:a-da({defaultDuration:t.targetDuration,durationList:u,startIndex:l,endIndex:c})}}return{partIndex:u[0]&&u[0].partIndex||null,segmentIndex:u[0]&&u[0].segmentIndex||0,startTime:i}}if(l<0){for(var p=l;p<0;p++)if((o-=t.targetDuration)<0)return{partIndex:u[0]&&u[0].partIndex||null,segmentIndex:u[0]&&u[0].segmentIndex||0,startTime:i};l=0}for(var m=l;m0)continue}else if(o-1/30>=0)continue;return{partIndex:_.partIndex,segmentIndex:_.segmentIndex,startTime:a+da({defaultDuration:t.targetDuration,durationList:u,startIndex:l,endIndex:m})}}return{segmentIndex:u[u.length-1].segmentIndex,partIndex:u[u.length-1].partIndex,startTime:i}},isEnabled:ma,isDisabled:function(e){return e.disabled},isBlacklisted:fa,isIncompatible:pa,playlistEnd:ca,isAes:function(e){for(var t=0;t-1&&s!==a.length-1&&i.push("_HLS_part="+s),(s>-1||a.length)&&r--}i.unshift("_HLS_msn="+r)}return t.serverControl&&t.serverControl.canSkipUntil&&i.unshift("_HLS_skip="+(t.serverControl.canSkipDateranges?"v2":"YES")),i.forEach((function(t,i){e+=""+(0===i?"?":"&")+t})),e}(i,t)),this.state="HAVE_CURRENT_METADATA",this.request=this.vhs_.xhr({uri:i,withCredentials:this.withCredentials},(function(t,i){if(e.request)return t?e.playlistRequestError(e.request,e.media(),"HAVE_METADATA"):void e.haveMetadata({playlistString:e.request.responseText,url:e.media().uri,id:e.media().id})}))}},i.playlistRequestError=function(e,t,i){var n=t.uri,r=t.id;this.request=null,i&&(this.state=i),this.error={playlist:this.master.playlists[r],status:e.status,message:"HLS playlist request error at URL: "+n+".",responseText:e.responseText,code:e.status>=500?4:2},this.trigger("error")},i.parseManifest_=function(e){var t=this,i=e.url;return function(e){var t=e.onwarn,i=e.oninfo,n=e.manifestString,r=e.customTagParsers,a=void 0===r?[]:r,s=e.customTagMappers,o=void 0===s?[]:s,u=e.experimentalLLHLS,l=new m.Parser;t&&l.on("warn",t),i&&l.on("info",i),a.forEach((function(e){return l.addParser(e)})),o.forEach((function(e){return l.addTagMapper(e)})),l.push(n),l.end();var h=l.manifest;if(u||(["preloadSegment","skip","serverControl","renditionReports","partInf","partTargetDuration"].forEach((function(e){h.hasOwnProperty(e)&&delete h[e]})),h.segments&&h.segments.forEach((function(e){["parts","preloadHints"].forEach((function(t){e.hasOwnProperty(t)&&delete e[t]}))}))),!h.targetDuration){var d=10;h.segments&&h.segments.length&&(d=h.segments.reduce((function(e,t){return Math.max(e,t.duration)}),0)),t&&t("manifest has no targetDuration defaulting to "+d),h.targetDuration=d}var c=sa(h);if(c.length&&!h.partTargetDuration){var f=c.reduce((function(e,t){return Math.max(e,t.duration)}),0);t&&(t("manifest has no partTargetDuration defaulting to "+f),Ta.error("LL-HLS manifest has parts but lacks required #EXT-X-PART-INF:PART-TARGET value. See https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-09#section-4.4.3.7. Playback is not guaranteed.")),h.partTargetDuration=f}return h}({onwarn:function(e){var n=e.message;return t.logger_("m3u8-parser warn for "+i+": "+n)},oninfo:function(e){var n=e.message;return t.logger_("m3u8-parser info for "+i+": "+n)},manifestString:e.manifestString,customTagParsers:this.customTagParsers,customTagMappers:this.customTagMappers,experimentalLLHLS:this.experimentalLLHLS})},i.haveMetadata=function(e){var t=e.playlistString,i=e.playlistObject,n=e.url,r=e.id;this.request=null,this.state="HAVE_METADATA";var a=i||this.parseManifest_({url:n,manifestString:t});a.lastRequest=Date.now(),Aa({playlist:a,uri:n,id:r});var s=Da(this.master,a);this.targetDuration=a.partTargetDuration||a.targetDuration,s?(this.master=s,this.media_=this.master.playlists[r]):this.trigger("playlistunchanged"),this.updateMediaUpdateTimeout_(Oa(this.media(),!!s)),this.trigger("loadedplaylist")},i.dispose=function(){this.trigger("dispose"),this.stopRequest(),C.default.clearTimeout(this.mediaUpdateTimeout),C.default.clearTimeout(this.finalRenditionTimeout),this.off()},i.stopRequest=function(){if(this.request){var e=this.request;this.request=null,e.onreadystatechange=null,e.abort()}},i.media=function(e,t){var i=this;if(!e)return this.media_;if("HAVE_NOTHING"===this.state)throw new Error("Cannot switch media playlist from "+this.state);if("string"==typeof e){if(!this.master.playlists[e])throw new Error("Unknown playlist URI: "+e);e=this.master.playlists[e]}if(C.default.clearTimeout(this.finalRenditionTimeout),t){var n=(e.partTargetDuration||e.targetDuration)/2*1e3||5e3;this.finalRenditionTimeout=C.default.setTimeout(this.media.bind(this,e,!1),n)}else{var r=this.state,a=!this.media_||e.id!==this.media_.id,s=this.master.playlists[e.id];if(s&&s.endList||e.endList&&e.segments.length)return this.request&&(this.request.onreadystatechange=null,this.request.abort(),this.request=null),this.state="HAVE_METADATA",this.media_=e,void(a&&(this.trigger("mediachanging"),"HAVE_MASTER"===r?this.trigger("loadedmetadata"):this.trigger("mediachange")));if(this.updateMediaUpdateTimeout_(Oa(e,!0)),a){if(this.state="SWITCHING_MEDIA",this.request){if(e.resolvedUri===this.request.url)return;this.request.onreadystatechange=null,this.request.abort(),this.request=null}this.media_&&this.trigger("mediachanging"),this.request=this.vhs_.xhr({uri:e.resolvedUri,withCredentials:this.withCredentials},(function(t,n){if(i.request){if(e.lastRequest=Date.now(),e.resolvedUri=Qr(i.handleManifestRedirects,e.resolvedUri,n),t)return i.playlistRequestError(i.request,e,r);i.haveMetadata({playlistString:n.responseText,url:e.uri,id:e.id}),"HAVE_MASTER"===r?i.trigger("loadedmetadata"):i.trigger("mediachange")}}))}}},i.pause=function(){this.mediaUpdateTimeout&&(C.default.clearTimeout(this.mediaUpdateTimeout),this.mediaUpdateTimeout=null),this.stopRequest(),"HAVE_NOTHING"===this.state&&(this.started=!1),"SWITCHING_MEDIA"===this.state?this.media_?this.state="HAVE_METADATA":this.state="HAVE_MASTER":"HAVE_CURRENT_METADATA"===this.state&&(this.state="HAVE_METADATA")},i.load=function(e){var t=this;this.mediaUpdateTimeout&&(C.default.clearTimeout(this.mediaUpdateTimeout),this.mediaUpdateTimeout=null);var i=this.media();if(e){var n=i?(i.partTargetDuration||i.targetDuration)/2*1e3:5e3;this.mediaUpdateTimeout=C.default.setTimeout((function(){t.mediaUpdateTimeout=null,t.load()}),n)}else this.started?i&&!i.endList?this.trigger("mediaupdatetimeout"):this.trigger("loadedplaylist"):this.start()},i.updateMediaUpdateTimeout_=function(e){var t=this;this.mediaUpdateTimeout&&(C.default.clearTimeout(this.mediaUpdateTimeout),this.mediaUpdateTimeout=null),this.media()&&!this.media().endList&&(this.mediaUpdateTimeout=C.default.setTimeout((function(){t.mediaUpdateTimeout=null,t.trigger("mediaupdatetimeout"),t.updateMediaUpdateTimeout_(e)}),e))},i.start=function(){var e=this;if(this.started=!0,"object"==typeof this.src)return this.src.uri||(this.src.uri=C.default.location.href),this.src.resolvedUri=this.src.uri,void setTimeout((function(){e.setupInitialPlaylist(e.src)}),0);this.request=this.vhs_.xhr({uri:this.src,withCredentials:this.withCredentials},(function(t,i){if(e.request){if(e.request=null,t)return e.error={status:i.status,message:"HLS playlist request error at URL: "+e.src+".",responseText:i.responseText,code:2},"HAVE_NOTHING"===e.state&&(e.started=!1),e.trigger("error");e.src=Qr(e.handleManifestRedirects,e.src,i);var n=e.parseManifest_({manifestString:i.responseText,url:e.src});e.setupInitialPlaylist(n)}}))},i.srcUri=function(){return"string"==typeof this.src?this.src:this.src.uri},i.setupInitialPlaylist=function(e){if(this.state="HAVE_MASTER",e.playlists)return this.master=e,Ca(this.master,this.srcUri()),e.playlists.forEach((function(e){e.segments=xa(e),e.segments.forEach((function(t){La(t,e.resolvedUri)}))})),this.trigger("loadedplaylist"),void(this.request||this.media(this.master.playlists[0]));var t=this.srcUri()||C.default.location.href;this.master=function(e,t){var i=Ea(0,t),n={mediaGroups:{AUDIO:{},VIDEO:{},"CLOSED-CAPTIONS":{},SUBTITLES:{}},uri:C.default.location.href,resolvedUri:C.default.location.href,playlists:[{uri:t,id:i,resolvedUri:t,attributes:{}}]};return n.playlists[i]=n.playlists[0],n.playlists[t]=n.playlists[0],n}(0,t),this.haveMetadata({playlistObject:e,url:t,id:this.master.playlists[0].id}),this.trigger("loadedmetadata")},t}(Pa),Ma=Yr.xhr,Fa=Yr.mergeOptions,Ba=function(e,t,i,n){var r="arraybuffer"===e.responseType?e.response:e.responseText;!t&&r&&(e.responseTime=Date.now(),e.roundTripTime=e.responseTime-e.requestTime,e.bytesReceived=r.byteLength||r.length,e.bandwidth||(e.bandwidth=Math.floor(e.bytesReceived/e.roundTripTime*8*1e3))),i.headers&&(e.responseHeaders=i.headers),t&&"ETIMEDOUT"===t.code&&(e.timedout=!0),t||e.aborted||200===i.statusCode||206===i.statusCode||0===i.statusCode||(t=new Error("XHR Failed with a response of: "+(e&&(r||e.responseText)))),n(t,e)},Na=function(){var e=function e(t,i){t=Fa({timeout:45e3},t);var n=e.beforeRequest||Yr.Vhs.xhr.beforeRequest;if(n&&"function"==typeof n){var r=n(t);r&&(t=r)}var a=(!0===Yr.Vhs.xhr.original?Ma:Yr.Vhs.xhr)(t,(function(e,t){return Ba(a,e,t,i)})),s=a.abort;return a.abort=function(){return a.aborted=!0,s.apply(a,arguments)},a.uri=t.uri,a.requestTime=Date.now(),a};return e.original=!0,e},ja=function(e){var t,i,n={};return e.byterange&&(n.Range=(t=e.byterange,i=t.offset+t.length-1,"bytes="+t.offset+"-"+i)),n},Va=function(e,t){return e.start(t)+"-"+e.end(t)},Ha=function(e,t){var i=e.toString(16);return"00".substring(0,2-i.length)+i+(t%2?" ":"")},za=function(e){return e>=32&&e<126?String.fromCharCode(e):"."},Ga=function(e){var t={};return Object.keys(e).forEach((function(i){var n=e[i];ArrayBuffer.isView(n)?t[i]={bytes:n.buffer,byteOffset:n.byteOffset,byteLength:n.byteLength}:t[i]=n})),t},Wa=function(e){var t=e.byterange||{length:1/0,offset:0};return[t.length,t.offset,e.resolvedUri].join(",")},Ya=function(e){return e.resolvedUri},qa=function(e){for(var t=Array.prototype.slice.call(e),i="",n=0;nn){if(e>n+.25*a.duration)return null;i=a}return{segment:i,estimatedStart:i.videoTimingInfo?i.videoTimingInfo.transmuxedPresentationStart:n-i.duration,type:i.videoTimingInfo?"accurate":"estimate"}}(n,t);if(!a)return r({message:"valid programTime was not found"});if("estimate"===a.type)return r({message:"Accurate programTime could not be determined. Please seek to e.seekTime and try again",seekTime:a.estimatedStart});var s={mediaSeconds:n},o=function(e,t){if(!t.dateTimeObject)return null;var i=t.videoTimingInfo.transmuxerPrependedSeconds,n=e-(t.videoTimingInfo.transmuxedPresentationStart+i);return new Date(t.dateTimeObject.getTime()+1e3*n)}(n,a.segment);return o&&(s.programDateTime=o.toISOString()),r(null,s)},Qa=function e(t){var i=t.programTime,n=t.playlist,r=t.retryCount,a=void 0===r?2:r,s=t.seekTo,o=t.pauseAfterSeek,u=void 0===o||o,l=t.tech,h=t.callback;if(!h)throw new Error("seekToProgramTime: callback must be provided");if(void 0===i||!n||!s)return h({message:"seekToProgramTime: programTime, seekTo and playlist must be provided"});if(!n.endList&&!l.hasStarted_)return h({message:"player must be playing a live stream to start buffering"});if(!function(e){if(!e.segments||0===e.segments.length)return!1;for(var t=0;tnew Date(o.getTime()+1e3*u)?null:(i>o&&(n=s),{segment:n,estimatedStart:n.videoTimingInfo?n.videoTimingInfo.transmuxedPresentationStart:Sa.duration(t,t.mediaSequence+t.segments.indexOf(n)),type:n.videoTimingInfo?"accurate":"estimate"})}(i,n);if(!d)return h({message:i+" was not found in the stream"});var c=d.segment,f=function(e,t){var i,n;try{i=new Date(e),n=new Date(t)}catch(e){}var r=i.getTime();return(n.getTime()-r)/1e3}(c.dateTimeObject,i);if("estimate"===d.type)return 0===a?h({message:i+" is not buffered yet. Try again"}):(s(d.estimatedStart+f),void l.one("seeked",(function(){e({programTime:i,playlist:n,retryCount:a-1,seekTo:s,pauseAfterSeek:u,tech:l,callback:h})})));var p=c.start+f;l.one("seeked",(function(){return h(null,l.currentTime())})),u&&l.pause(),s(p)},$a=function(e,t){if(4===e.readyState)return t()},Ja=Yr.EventTarget,Za=Yr.mergeOptions,es=function(e,t){if(!Ra(e,t))return!1;if(e.sidx&&t.sidx&&(e.sidx.offset!==t.sidx.offset||e.sidx.length!==t.sidx.length))return!1;if(!e.sidx&&t.sidx||e.sidx&&!t.sidx)return!1;if(e.segments&&!t.segments||!e.segments&&t.segments)return!1;if(!e.segments&&!t.segments)return!0;for(var i=0;i=h+l)return s(t,{response:o.subarray(l,l+h),status:i.status,uri:i.uri});n.request=n.vhs_.xhr({uri:a,responseType:"arraybuffer",headers:ja({byterange:e.sidx.byterange})},s)}))}else this.mediaRequest_=C.default.setTimeout((function(){return i(!1)}),0)},i.dispose=function(){this.trigger("dispose"),this.stopRequest(),this.loadedPlaylists_={},C.default.clearTimeout(this.minimumUpdatePeriodTimeout_),C.default.clearTimeout(this.mediaRequest_),C.default.clearTimeout(this.mediaUpdateTimeout),this.mediaUpdateTimeout=null,this.mediaRequest_=null,this.minimumUpdatePeriodTimeout_=null,this.masterPlaylistLoader_.createMupOnMedia_&&(this.off("loadedmetadata",this.masterPlaylistLoader_.createMupOnMedia_),this.masterPlaylistLoader_.createMupOnMedia_=null),this.off()},i.hasPendingRequest=function(){return this.request||this.mediaRequest_},i.stopRequest=function(){if(this.request){var e=this.request;this.request=null,e.onreadystatechange=null,e.abort()}},i.media=function(e){var t=this;if(!e)return this.media_;if("HAVE_NOTHING"===this.state)throw new Error("Cannot switch media playlist from "+this.state);var i=this.state;if("string"==typeof e){if(!this.masterPlaylistLoader_.master.playlists[e])throw new Error("Unknown playlist URI: "+e);e=this.masterPlaylistLoader_.master.playlists[e]}var n=!this.media_||e.id!==this.media_.id;if(n&&this.loadedPlaylists_[e.id]&&this.loadedPlaylists_[e.id].endList)return this.state="HAVE_METADATA",this.media_=e,void(n&&(this.trigger("mediachanging"),this.trigger("mediachange")));n&&(this.media_&&this.trigger("mediachanging"),this.addSidxSegments_(e,i,(function(n){t.haveMetadata({startingState:i,playlist:e})})))},i.haveMetadata=function(e){var t=e.startingState,i=e.playlist;this.state="HAVE_METADATA",this.loadedPlaylists_[i.id]=i,this.mediaRequest_=null,this.refreshMedia_(i.id),"HAVE_MASTER"===t?this.trigger("loadedmetadata"):this.trigger("mediachange")},i.pause=function(){this.masterPlaylistLoader_.createMupOnMedia_&&(this.off("loadedmetadata",this.masterPlaylistLoader_.createMupOnMedia_),this.masterPlaylistLoader_.createMupOnMedia_=null),this.stopRequest(),C.default.clearTimeout(this.mediaUpdateTimeout),this.mediaUpdateTimeout=null,this.isMaster_&&(C.default.clearTimeout(this.masterPlaylistLoader_.minimumUpdatePeriodTimeout_),this.masterPlaylistLoader_.minimumUpdatePeriodTimeout_=null),"HAVE_NOTHING"===this.state&&(this.started=!1)},i.load=function(e){var t=this;C.default.clearTimeout(this.mediaUpdateTimeout),this.mediaUpdateTimeout=null;var i=this.media();if(e){var n=i?i.targetDuration/2*1e3:5e3;this.mediaUpdateTimeout=C.default.setTimeout((function(){return t.load()}),n)}else this.started?i&&!i.endList?(this.isMaster_&&!this.minimumUpdatePeriodTimeout_&&(this.trigger("minimumUpdatePeriod"),this.updateMinimumUpdatePeriodTimeout_()),this.trigger("mediaupdatetimeout")):this.trigger("loadedplaylist"):this.start()},i.start=function(){var e=this;this.started=!0,this.isMaster_?this.requestMaster_((function(t,i){e.haveMaster_(),e.hasPendingRequest()||e.media_||e.media(e.masterPlaylistLoader_.master.playlists[0])})):this.mediaRequest_=C.default.setTimeout((function(){return e.haveMaster_()}),0)},i.requestMaster_=function(e){var t=this;this.request=this.vhs_.xhr({uri:this.masterPlaylistLoader_.srcUrl,withCredentials:this.withCredentials},(function(i,n){if(!t.requestErrored_(i,n)){var r=n.responseText!==t.masterPlaylistLoader_.masterXml_;return t.masterPlaylistLoader_.masterXml_=n.responseText,n.responseHeaders&&n.responseHeaders.date?t.masterLoaded_=Date.parse(n.responseHeaders.date):t.masterLoaded_=Date.now(),t.masterPlaylistLoader_.srcUrl=Qr(t.handleManifestRedirects,t.masterPlaylistLoader_.srcUrl,n),r?(t.handleMaster_(),void t.syncClientServerClock_((function(){return e(n,r)}))):e(n,r)}"HAVE_NOTHING"===t.state&&(t.started=!1)}))},i.syncClientServerClock_=function(e){var t=this,i=v.parseUTCTiming(this.masterPlaylistLoader_.masterXml_);return null===i?(this.masterPlaylistLoader_.clientOffset_=this.masterLoaded_-Date.now(),e()):"DIRECT"===i.method?(this.masterPlaylistLoader_.clientOffset_=i.value-Date.now(),e()):void(this.request=this.vhs_.xhr({uri:Xr(this.masterPlaylistLoader_.srcUrl,i.value),method:i.method,withCredentials:this.withCredentials},(function(n,r){if(t.request){if(n)return t.masterPlaylistLoader_.clientOffset_=t.masterLoaded_-Date.now(),e();var a;a="HEAD"===i.method?r.responseHeaders&&r.responseHeaders.date?Date.parse(r.responseHeaders.date):t.masterLoaded_:Date.parse(r.responseText),t.masterPlaylistLoader_.clientOffset_=a-Date.now(),e()}})))},i.haveMaster_=function(){this.state="HAVE_MASTER",this.isMaster_?this.trigger("loadedplaylist"):this.media_||this.media(this.childPlaylist_)},i.handleMaster_=function(){this.mediaRequest_=null;var e,t,i,n,r,a,s=(e={masterXml:this.masterPlaylistLoader_.masterXml_,srcUrl:this.masterPlaylistLoader_.srcUrl,clientOffset:this.masterPlaylistLoader_.clientOffset_,sidxMapping:this.masterPlaylistLoader_.sidxMapping_},t=e.masterXml,i=e.srcUrl,n=e.clientOffset,r=e.sidxMapping,a=v.parse(t,{manifestUri:i,clientOffset:n,sidxMapping:r}),Ca(a,i),a),o=this.masterPlaylistLoader_.master;o&&(s=function(e,t,i){for(var n=!0,r=Za(e,{duration:t.duration,minimumUpdatePeriod:t.minimumUpdatePeriod}),a=0;a-1)},this.trigger=function(t){var i,n,r,a;if(i=e[t])if(2===arguments.length)for(r=i.length,n=0;n>>1,e.samplingfrequencyindex<<7|e.channelcount<<3,6,1,2]))},m=function(e){return t(T.hdlr,P[e])},p=function(e){var i=new Uint8Array([0,0,0,0,0,0,0,2,0,0,0,3,0,1,95,144,e.duration>>>24&255,e.duration>>>16&255,e.duration>>>8&255,255&e.duration,85,196,0,0]);return e.samplerate&&(i[12]=e.samplerate>>>24&255,i[13]=e.samplerate>>>16&255,i[14]=e.samplerate>>>8&255,i[15]=255&e.samplerate),t(T.mdhd,i)},f=function(e){return t(T.mdia,p(e),m(e.type),s(e))},a=function(e){return t(T.mfhd,new Uint8Array([0,0,0,0,(4278190080&e)>>24,(16711680&e)>>16,(65280&e)>>8,255&e]))},s=function(e){return t(T.minf,"video"===e.type?t(T.vmhd,I):t(T.smhd,L),i(),g(e))},o=function(e,i){for(var n=[],r=i.length;r--;)n[r]=y(i[r]);return t.apply(null,[T.moof,a(e)].concat(n))},u=function(e){for(var i=e.length,n=[];i--;)n[i]=d(e[i]);return t.apply(null,[T.moov,h(4294967295)].concat(n).concat(l(e)))},l=function(e){for(var i=e.length,n=[];i--;)n[i]=b(e[i]);return t.apply(null,[T.mvex].concat(n))},h=function(e){var i=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,2,0,1,95,144,(4278190080&e)>>24,(16711680&e)>>16,(65280&e)>>8,255&e,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255]);return t(T.mvhd,i)},_=function(e){var i,n,r=e.samples||[],a=new Uint8Array(4+r.length);for(n=0;n>>8),s.push(255&r[i].byteLength),s=s.concat(Array.prototype.slice.call(r[i]));for(i=0;i>>8),o.push(255&a[i].byteLength),o=o.concat(Array.prototype.slice.call(a[i]));if(n=[T.avc1,new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,(65280&e.width)>>8,255&e.width,(65280&e.height)>>8,255&e.height,0,72,0,0,0,72,0,0,0,0,0,0,0,1,19,118,105,100,101,111,106,115,45,99,111,110,116,114,105,98,45,104,108,115,0,0,0,0,0,0,0,0,0,0,0,0,0,24,17,17]),t(T.avcC,new Uint8Array([1,e.profileIdc,e.profileCompatibility,e.levelIdc,255].concat([r.length],s,[a.length],o))),t(T.btrt,new Uint8Array([0,28,156,128,0,45,198,192,0,45,198,192]))],e.sarRatio){var u=e.sarRatio[0],l=e.sarRatio[1];n.push(t(T.pasp,new Uint8Array([(4278190080&u)>>24,(16711680&u)>>16,(65280&u)>>8,255&u,(4278190080&l)>>24,(16711680&l)>>16,(65280&l)>>8,255&l])))}return t.apply(null,n)},F=function(e){return t(T.mp4a,new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,(65280&e.channelcount)>>8,255&e.channelcount,(65280&e.samplesize)>>8,255&e.samplesize,0,0,0,0,(65280&e.samplerate)>>8,255&e.samplerate,0,0]),n(e))},c=function(e){var i=new Uint8Array([0,0,0,7,0,0,0,0,0,0,0,0,(4278190080&e.id)>>24,(16711680&e.id)>>16,(65280&e.id)>>8,255&e.id,0,0,0,0,(4278190080&e.duration)>>24,(16711680&e.duration)>>16,(65280&e.duration)>>8,255&e.duration,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,(65280&e.width)>>8,255&e.width,0,0,(65280&e.height)>>8,255&e.height,0,0]);return t(T.tkhd,i)},y=function(e){var i,n,r,a,s,o;return i=t(T.tfhd,new Uint8Array([0,0,0,58,(4278190080&e.id)>>24,(16711680&e.id)>>16,(65280&e.id)>>8,255&e.id,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0])),s=Math.floor(e.baseMediaDecodeTime/(H+1)),o=Math.floor(e.baseMediaDecodeTime%(H+1)),n=t(T.tfdt,new Uint8Array([1,0,0,0,s>>>24&255,s>>>16&255,s>>>8&255,255&s,o>>>24&255,o>>>16&255,o>>>8&255,255&o])),92,"audio"===e.type?(r=S(e,92),t(T.traf,i,n,r)):(a=_(e),r=S(e,a.length+92),t(T.traf,i,n,r,a))},d=function(e){return e.duration=e.duration||4294967295,t(T.trak,c(e),f(e))},b=function(e){var i=new Uint8Array([0,0,0,0,(4278190080&e.id)>>24,(16711680&e.id)>>16,(65280&e.id)>>8,255&e.id,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1]);return"video"!==e.type&&(i[i.length-1]=0),t(T.trex,i)},j=function(e,t){var i=0,n=0,r=0,a=0;return e.length&&(void 0!==e[0].duration&&(i=1),void 0!==e[0].size&&(n=2),void 0!==e[0].flags&&(r=4),void 0!==e[0].compositionTimeOffset&&(a=8)),[0,0,i|n|r|a,1,(4278190080&e.length)>>>24,(16711680&e.length)>>>16,(65280&e.length)>>>8,255&e.length,(4278190080&t)>>>24,(16711680&t)>>>16,(65280&t)>>>8,255&t]},N=function(e,i){var n,r,a,s,o,u;for(i+=20+16*(s=e.samples||[]).length,a=j(s,i),(r=new Uint8Array(a.length+16*s.length)).set(a),n=a.length,u=0;u>>24,r[n++]=(16711680&o.duration)>>>16,r[n++]=(65280&o.duration)>>>8,r[n++]=255&o.duration,r[n++]=(4278190080&o.size)>>>24,r[n++]=(16711680&o.size)>>>16,r[n++]=(65280&o.size)>>>8,r[n++]=255&o.size,r[n++]=o.flags.isLeading<<2|o.flags.dependsOn,r[n++]=o.flags.isDependedOn<<6|o.flags.hasRedundancy<<4|o.flags.paddingValue<<1|o.flags.isNonSyncSample,r[n++]=61440&o.flags.degradationPriority,r[n++]=15&o.flags.degradationPriority,r[n++]=(4278190080&o.compositionTimeOffset)>>>24,r[n++]=(16711680&o.compositionTimeOffset)>>>16,r[n++]=(65280&o.compositionTimeOffset)>>>8,r[n++]=255&o.compositionTimeOffset;return t(T.trun,r)},B=function(e,i){var n,r,a,s,o,u;for(i+=20+8*(s=e.samples||[]).length,a=j(s,i),(n=new Uint8Array(a.length+8*s.length)).set(a),r=a.length,u=0;u>>24,n[r++]=(16711680&o.duration)>>>16,n[r++]=(65280&o.duration)>>>8,n[r++]=255&o.duration,n[r++]=(4278190080&o.size)>>>24,n[r++]=(16711680&o.size)>>>16,n[r++]=(65280&o.size)>>>8,n[r++]=255&o.size;return t(T.trun,n)},S=function(e,t){return"audio"===e.type?B(e,t):N(e,t)};r=function(){return t(T.ftyp,E,w,E,A)};var z,G,W,Y,q,K,X,Q,$=function(e){return t(T.mdat,e)},J=o,Z=function(e){var t,i=r(),n=u(e);return(t=new Uint8Array(i.byteLength+n.byteLength)).set(i),t.set(n,i.byteLength),t},ee=function(e,t){var i={size:0,flags:{isLeading:0,dependsOn:1,isDependedOn:0,hasRedundancy:0,degradationPriority:0,isNonSyncSample:1}};return i.dataOffset=t,i.compositionTimeOffset=e.pts-e.dts,i.duration=e.duration,i.size=4*e.length,i.size+=e.byteLength,e.keyFrame&&(i.flags.dependsOn=2,i.flags.isNonSyncSample=0),i},te=function(e){var t,i,n=[],r=[];for(r.byteLength=0,r.nalCount=0,r.duration=0,n.byteLength=0,t=0;t1&&(t=e.shift(),e.byteLength-=t.byteLength,e.nalCount-=t.nalCount,e[0][0].dts=t.dts,e[0][0].pts=t.pts,e[0][0].duration+=t.duration),e},re=function(e,t){var i,n,r,a,s,o=t||0,u=[];for(i=0;ihe/2))){for((s=le()[e.samplerate])||(s=t[0].data),o=0;o=i?e:(t.minSegmentDts=1/0,e.filter((function(e){return e.dts>=i&&(t.minSegmentDts=Math.min(t.minSegmentDts,e.dts),t.minSegmentPts=t.minSegmentDts,!0)})))},ve=function(e){var t,i,n=[];for(t=0;t=this.virtualRowCount&&"function"==typeof this.beforeRowOverflow&&this.beforeRowOverflow(e),this.rows.length>0&&(this.rows.push(""),this.rowIdx++);this.rows.length>this.virtualRowCount;)this.rows.shift(),this.rowIdx--},Re.prototype.isEmpty=function(){return 0===this.rows.length||1===this.rows.length&&""===this.rows[0]},Re.prototype.addText=function(e){this.rows[this.rowIdx]+=e},Re.prototype.backspace=function(){if(!this.isEmpty()){var e=this.rows[this.rowIdx];this.rows[this.rowIdx]=e.substr(0,e.length-1)}};var De=function(e){this.serviceNum=e,this.text="",this.currentWindow=new Re(-1),this.windows=[]};De.prototype.init=function(e,t){this.startPts=e;for(var i=0;i<8;i++)this.windows[i]=new Re(i),"function"==typeof t&&(this.windows[i].beforeRowOverflow=t)},De.prototype.setCurrentWindow=function(e){this.currentWindow=this.windows[e]};var Oe=function e(){e.prototype.init.call(this);var t=this;this.current708Packet=null,this.services={},this.push=function(e){3===e.type?(t.new708Packet(),t.add708Bytes(e)):(null===t.current708Packet&&t.new708Packet(),t.add708Bytes(e))}};Oe.prototype=new V,Oe.prototype.new708Packet=function(){null!==this.current708Packet&&this.push708Packet(),this.current708Packet={data:[],ptsVals:[]}},Oe.prototype.add708Bytes=function(e){var t=e.ccData,i=t>>>8,n=255&t;this.current708Packet.ptsVals.push(e.pts),this.current708Packet.data.push(i),this.current708Packet.data.push(n)},Oe.prototype.push708Packet=function(){var e=this.current708Packet,t=e.data,i=null,n=null,r=0,a=t[r++];for(e.seq=a>>6,e.sizeCode=63&a;r>5)&&n>0&&(i=a=t[r++]),this.pushServiceBlock(i,r,n),n>0&&(r+=n-1)},Oe.prototype.pushServiceBlock=function(e,t,i){var n,r=t,a=this.current708Packet.data,s=this.services[e];for(s||(s=this.initService(e,r));r>5,a.rowLock=(16&n)>>4,a.columnLock=(8&n)>>3,a.priority=7&n,n=i[++e],a.relativePositioning=(128&n)>>7,a.anchorVertical=127&n,n=i[++e],a.anchorHorizontal=n,n=i[++e],a.anchorPoint=(240&n)>>4,a.rowCount=15&n,n=i[++e],a.columnCount=63&n,n=i[++e],a.windowStyle=(56&n)>>3,a.penStyle=7&n,a.virtualRowCount=a.rowCount+1,e},Oe.prototype.setWindowAttributes=function(e,t){var i=this.current708Packet.data,n=i[e],r=t.currentWindow.winAttr;return n=i[++e],r.fillOpacity=(192&n)>>6,r.fillRed=(48&n)>>4,r.fillGreen=(12&n)>>2,r.fillBlue=3&n,n=i[++e],r.borderType=(192&n)>>6,r.borderRed=(48&n)>>4,r.borderGreen=(12&n)>>2,r.borderBlue=3&n,n=i[++e],r.borderType+=(128&n)>>5,r.wordWrap=(64&n)>>6,r.printDirection=(48&n)>>4,r.scrollDirection=(12&n)>>2,r.justify=3&n,n=i[++e],r.effectSpeed=(240&n)>>4,r.effectDirection=(12&n)>>2,r.displayEffect=3&n,e},Oe.prototype.flushDisplayed=function(e,t){for(var i=[],n=0;n<8;n++)t.windows[n].visible&&!t.windows[n].isEmpty()&&i.push(t.windows[n].getText());t.endPts=e,t.text=i.join("\n\n"),this.pushCaption(t),t.startPts=e},Oe.prototype.pushCaption=function(e){""!==e.text&&(this.trigger("data",{startPts:e.startPts,endPts:e.endPts,text:e.text,stream:"cc708_"+e.serviceNum}),e.text="",e.startPts=e.endPts)},Oe.prototype.displayWindows=function(e,t){var i=this.current708Packet.data[++e],n=this.getPts(e);this.flushDisplayed(n,t);for(var r=0;r<8;r++)i&1<>4,r.offset=(12&n)>>2,r.penSize=3&n,n=i[++e],r.italics=(128&n)>>7,r.underline=(64&n)>>6,r.edgeType=(56&n)>>3,r.fontStyle=7&n,e},Oe.prototype.setPenColor=function(e,t){var i=this.current708Packet.data,n=i[e],r=t.currentWindow.penColor;return n=i[++e],r.fgOpacity=(192&n)>>6,r.fgRed=(48&n)>>4,r.fgGreen=(12&n)>>2,r.fgBlue=3&n,n=i[++e],r.bgOpacity=(192&n)>>6,r.bgRed=(48&n)>>4,r.bgGreen=(12&n)>>2,r.bgBlue=3&n,n=i[++e],r.edgeRed=(48&n)>>4,r.edgeGreen=(12&n)>>2,r.edgeBlue=3&n,e},Oe.prototype.setPenLocation=function(e,t){var i=this.current708Packet.data,n=i[e],r=t.currentWindow.penLoc;return t.currentWindow.pendingNewLine=!0,n=i[++e],r.row=15&n,n=i[++e],r.column=63&n,e},Oe.prototype.reset=function(e,t){var i=this.getPts(e);return this.flushDisplayed(i,t),this.initService(t.serviceNum,e)};var Ue={42:225,92:233,94:237,95:243,96:250,123:231,124:247,125:209,126:241,127:9608,304:174,305:176,306:189,307:191,308:8482,309:162,310:163,311:9834,312:224,313:160,314:232,315:226,316:234,317:238,318:244,319:251,544:193,545:201,546:211,547:218,548:220,549:252,550:8216,551:161,552:42,553:39,554:8212,555:169,556:8480,557:8226,558:8220,559:8221,560:192,561:194,562:199,563:200,564:202,565:203,566:235,567:206,568:207,569:239,570:212,571:217,572:249,573:219,574:171,575:187,800:195,801:227,802:205,803:204,804:236,805:210,806:242,807:213,808:245,809:123,810:125,811:92,812:94,813:95,814:124,815:126,816:196,817:228,818:214,819:246,820:223,821:165,822:164,823:9474,824:197,825:229,826:216,827:248,828:9484,829:9488,830:9492,831:9496},Me=function(e){return null===e?"":(e=Ue[e]||e,String.fromCharCode(e))},Fe=[4352,4384,4608,4640,5376,5408,5632,5664,5888,5920,4096,4864,4896,5120,5152],Be=function(){for(var e=[],t=15;t--;)e.push("");return e},Ne=function e(t,i){e.prototype.init.call(this),this.field_=t||0,this.dataChannel_=i||0,this.name_="CC"+(1+(this.field_<<1|this.dataChannel_)),this.setConstants(),this.reset(),this.push=function(e){var t,i,n,r,a;if((t=32639&e.ccData)!==this.lastControlCode_){if(4096==(61440&t)?this.lastControlCode_=t:t!==this.PADDING_&&(this.lastControlCode_=null),n=t>>>8,r=255&t,t!==this.PADDING_)if(t===this.RESUME_CAPTION_LOADING_)this.mode_="popOn";else if(t===this.END_OF_CAPTION_)this.mode_="popOn",this.clearFormatting(e.pts),this.flushDisplayed(e.pts),i=this.displayed_,this.displayed_=this.nonDisplayed_,this.nonDisplayed_=i,this.startPts_=e.pts;else if(t===this.ROLL_UP_2_ROWS_)this.rollUpRows_=2,this.setRollUp(e.pts);else if(t===this.ROLL_UP_3_ROWS_)this.rollUpRows_=3,this.setRollUp(e.pts);else if(t===this.ROLL_UP_4_ROWS_)this.rollUpRows_=4,this.setRollUp(e.pts);else if(t===this.CARRIAGE_RETURN_)this.clearFormatting(e.pts),this.flushDisplayed(e.pts),this.shiftRowsUp_(),this.startPts_=e.pts;else if(t===this.BACKSPACE_)"popOn"===this.mode_?this.nonDisplayed_[this.row_]=this.nonDisplayed_[this.row_].slice(0,-1):this.displayed_[this.row_]=this.displayed_[this.row_].slice(0,-1);else if(t===this.ERASE_DISPLAYED_MEMORY_)this.flushDisplayed(e.pts),this.displayed_=Be();else if(t===this.ERASE_NON_DISPLAYED_MEMORY_)this.nonDisplayed_=Be();else if(t===this.RESUME_DIRECT_CAPTIONING_)"paintOn"!==this.mode_&&(this.flushDisplayed(e.pts),this.displayed_=Be()),this.mode_="paintOn",this.startPts_=e.pts;else if(this.isSpecialCharacter(n,r))a=Me((n=(3&n)<<8)|r),this[this.mode_](e.pts,a),this.column_++;else if(this.isExtCharacter(n,r))"popOn"===this.mode_?this.nonDisplayed_[this.row_]=this.nonDisplayed_[this.row_].slice(0,-1):this.displayed_[this.row_]=this.displayed_[this.row_].slice(0,-1),a=Me((n=(3&n)<<8)|r),this[this.mode_](e.pts,a),this.column_++;else if(this.isMidRowCode(n,r))this.clearFormatting(e.pts),this[this.mode_](e.pts," "),this.column_++,14==(14&r)&&this.addFormatting(e.pts,["i"]),1==(1&r)&&this.addFormatting(e.pts,["u"]);else if(this.isOffsetControlCode(n,r))this.column_+=3&r;else if(this.isPAC(n,r)){var s=Fe.indexOf(7968&t);"rollUp"===this.mode_&&(s-this.rollUpRows_+1<0&&(s=this.rollUpRows_-1),this.setRollUp(e.pts,s)),s!==this.row_&&(this.clearFormatting(e.pts),this.row_=s),1&r&&-1===this.formatting_.indexOf("u")&&this.addFormatting(e.pts,["u"]),16==(16&t)&&(this.column_=4*((14&t)>>1)),this.isColorPAC(r)&&14==(14&r)&&this.addFormatting(e.pts,["i"])}else this.isNormalChar(n)&&(0===r&&(r=null),a=Me(n),a+=Me(r),this[this.mode_](e.pts,a),this.column_+=a.length)}else this.lastControlCode_=null}};Ne.prototype=new V,Ne.prototype.flushDisplayed=function(e){var t=this.displayed_.map((function(e,t){try{return e.trim()}catch(e){return this.trigger("log",{level:"warn",message:"Skipping a malformed 608 caption at index "+t+"."}),""}}),this).join("\n").replace(/^\n+|\n+$/g,"");t.length&&this.trigger("data",{startPts:this.startPts_,endPts:e,text:t,stream:this.name_})},Ne.prototype.reset=function(){this.mode_="popOn",this.topRow_=0,this.startPts_=0,this.displayed_=Be(),this.nonDisplayed_=Be(),this.lastControlCode_=null,this.column_=0,this.row_=14,this.rollUpRows_=2,this.formatting_=[]},Ne.prototype.setConstants=function(){0===this.dataChannel_?(this.BASE_=16,this.EXT_=17,this.CONTROL_=(20|this.field_)<<8,this.OFFSET_=23):1===this.dataChannel_&&(this.BASE_=24,this.EXT_=25,this.CONTROL_=(28|this.field_)<<8,this.OFFSET_=31),this.PADDING_=0,this.RESUME_CAPTION_LOADING_=32|this.CONTROL_,this.END_OF_CAPTION_=47|this.CONTROL_,this.ROLL_UP_2_ROWS_=37|this.CONTROL_,this.ROLL_UP_3_ROWS_=38|this.CONTROL_,this.ROLL_UP_4_ROWS_=39|this.CONTROL_,this.CARRIAGE_RETURN_=45|this.CONTROL_,this.RESUME_DIRECT_CAPTIONING_=41|this.CONTROL_,this.BACKSPACE_=33|this.CONTROL_,this.ERASE_DISPLAYED_MEMORY_=44|this.CONTROL_,this.ERASE_NON_DISPLAYED_MEMORY_=46|this.CONTROL_},Ne.prototype.isSpecialCharacter=function(e,t){return e===this.EXT_&&t>=48&&t<=63},Ne.prototype.isExtCharacter=function(e,t){return(e===this.EXT_+1||e===this.EXT_+2)&&t>=32&&t<=63},Ne.prototype.isMidRowCode=function(e,t){return e===this.EXT_&&t>=32&&t<=47},Ne.prototype.isOffsetControlCode=function(e,t){return e===this.OFFSET_&&t>=33&&t<=35},Ne.prototype.isPAC=function(e,t){return e>=this.BASE_&&e=64&&t<=127},Ne.prototype.isColorPAC=function(e){return e>=64&&e<=79||e>=96&&e<=127},Ne.prototype.isNormalChar=function(e){return e>=32&&e<=127},Ne.prototype.setRollUp=function(e,t){if("rollUp"!==this.mode_&&(this.row_=14,this.mode_="rollUp",this.flushDisplayed(e),this.nonDisplayed_=Be(),this.displayed_=Be()),void 0!==t&&t!==this.row_)for(var i=0;i"}),"");this[this.mode_](e,i)},Ne.prototype.clearFormatting=function(e){if(this.formatting_.length){var t=this.formatting_.reverse().reduce((function(e,t){return e+""}),"");this.formatting_=[],this[this.mode_](e,t)}},Ne.prototype.popOn=function(e,t){var i=this.nonDisplayed_[this.row_];i+=t,this.nonDisplayed_[this.row_]=i},Ne.prototype.rollUp=function(e,t){var i=this.displayed_[this.row_];i+=t,this.displayed_[this.row_]=i},Ne.prototype.shiftRowsUp_=function(){var e;for(e=0;et&&(i=-1);Math.abs(t-e)>4294967296;)e+=8589934592*i;return e},ze=function e(t){var i,n;e.prototype.init.call(this),this.type_=t||"shared",this.push=function(e){"shared"!==this.type_&&e.type!==this.type_||(void 0===n&&(n=e.dts),e.dts=He(e.dts,n),e.pts=He(e.pts,n),i=e.dts,this.trigger("data",e))},this.flush=function(){n=i,this.trigger("done")},this.endTimeline=function(){this.flush(),this.trigger("endedtimeline")},this.discontinuity=function(){n=void 0,i=void 0},this.reset=function(){this.discontinuity(),this.trigger("reset")}};ze.prototype=new V;var Ge,We=ze,Ye=He,qe=function(e,t,i){var n,r="";for(n=t;n>>2;h*=4,h+=3&l[7],o.timeStamp=h,void 0===t.pts&&void 0===t.dts&&(t.pts=o.timeStamp,t.dts=o.timeStamp),this.trigger("timestamp",o)}t.frames.push(o),i+=10,i+=s}while(i>>4>1&&(n+=t[n]+1),0===i.pid)i.type="pat",e(t.subarray(n),i),this.trigger("data",i);else if(i.pid===this.pmtPid)for(i.type="pmt",e(t.subarray(n),i),this.trigger("data",i);this.packetsWaitingForPmt.length;)this.processPes_.apply(this,this.packetsWaitingForPmt.shift());else void 0===this.programMapTable?this.packetsWaitingForPmt.push([t,n,i]):this.processPes_(t,n,i)},this.processPes_=function(e,t,i){i.pid===this.programMapTable.video?i.streamType=Ve.H264_STREAM_TYPE:i.pid===this.programMapTable.audio?i.streamType=Ve.ADTS_STREAM_TYPE:i.streamType=this.programMapTable["timed-metadata"][i.pid],i.type="pes",i.data=e.subarray(t),this.trigger("data",i)}}).prototype=new V,Je.STREAM_TYPES={h264:27,adts:15},(Ze=function(){var e,t=this,i=!1,n={data:[],size:0},r={data:[],size:0},a={data:[],size:0},s=function(e,i,n){var r,a,s=new Uint8Array(e.size),o={type:i},u=0,l=0;if(e.data.length&&!(e.size<9)){for(o.trackId=e.data[0].pid,u=0;u>>3,d.pts*=4,d.pts+=(6&h[13])>>>1,d.dts=d.pts,64&c&&(d.dts=(14&h[14])<<27|(255&h[15])<<20|(254&h[16])<<12|(255&h[17])<<5|(254&h[18])>>>3,d.dts*=4,d.dts+=(6&h[18])>>>1)),d.data=h.subarray(9+h[8])),r="video"===i||o.packetLength<=e.size,(n||r)&&(e.size=0,e.data.length=0),r&&t.trigger("data",o)}};Ze.prototype.init.call(this),this.push=function(o){({pat:function(){},pes:function(){var e,t;switch(o.streamType){case Ve.H264_STREAM_TYPE:e=n,t="video";break;case Ve.ADTS_STREAM_TYPE:e=r,t="audio";break;case Ve.METADATA_STREAM_TYPE:e=a,t="timed-metadata";break;default:return}o.payloadUnitStartIndicator&&s(e,t,!0),e.data.push(o),e.size+=o.data.byteLength},pmt:function(){var n={type:"metadata",tracks:[]};null!==(e=o.programMapTable).video&&n.tracks.push({timelineStartInfo:{baseMediaDecodeTime:0},id:+e.video,codec:"avc",type:"video"}),null!==e.audio&&n.tracks.push({timelineStartInfo:{baseMediaDecodeTime:0},id:+e.audio,codec:"adts",type:"audio"}),i=!0,t.trigger("data",n)}})[o.type]()},this.reset=function(){n.size=0,n.data.length=0,r.size=0,r.data.length=0,this.trigger("reset")},this.flushStreams_=function(){s(n,"video"),s(r,"audio"),s(a,"timed-metadata")},this.flush=function(){if(!i&&e){var n={type:"metadata",tracks:[]};null!==e.video&&n.tracks.push({timelineStartInfo:{baseMediaDecodeTime:0},id:+e.video,codec:"avc",type:"video"}),null!==e.audio&&n.tracks.push({timelineStartInfo:{baseMediaDecodeTime:0},id:+e.audio,codec:"adts",type:"audio"}),t.trigger("data",n)}i=!1,this.flushStreams_(),this.trigger("done")}}).prototype=new V;var it={PAT_PID:0,MP2T_PACKET_LENGTH:188,TransportPacketStream:$e,TransportParseStream:Je,ElementaryStream:Ze,TimestampRolloverStream:tt,CaptionStream:je.CaptionStream,Cea608Stream:je.Cea608Stream,Cea708Stream:je.Cea708Stream,MetadataStream:et};for(var nt in Ve)Ve.hasOwnProperty(nt)&&(it[nt]=Ve[nt]);var rt,at=it,st=he,ot=[96e3,88200,64e3,48e3,44100,32e3,24e3,22050,16e3,12e3,11025,8e3,7350];(rt=function(e){var t,i=0;rt.prototype.init.call(this),this.skipWarn_=function(e,t){this.trigger("log",{level:"warn",message:"adts skiping bytes "+e+" to "+t+" in frame "+i+" outside syncword"})},this.push=function(n){var r,a,s,o,u,l=0;if(e||(i=0),"audio"===n.type){var h;for(t&&t.length?(s=t,(t=new Uint8Array(s.byteLength+n.data.byteLength)).set(s),t.set(n.data,s.byteLength)):t=n.data;l+7>5,u=(o=1024*(1+(3&t[l+6])))*st/ot[(60&t[l+2])>>>2],t.byteLength-l>>6&3),channelcount:(1&t[l+2])<<2|(192&t[l+3])>>>6,samplerate:ot[(60&t[l+2])>>>2],samplingfrequencyindex:(60&t[l+2])>>>2,samplesize:16,data:t.subarray(l+7+a,l+r)}),i++,l+=r}else"number"!=typeof h&&(h=l),l++;"number"==typeof h&&(this.skipWarn_(h,l),h=null),t=t.subarray(l)}},this.flush=function(){i=0,this.trigger("done")},this.reset=function(){t=void 0,this.trigger("reset")},this.endTimeline=function(){t=void 0,this.trigger("endedtimeline")}}).prototype=new V;var ut,lt,ht,dt=rt,ct=function(e){var t=e.byteLength,i=0,n=0;this.length=function(){return 8*t},this.bitsAvailable=function(){return 8*t+n},this.loadWord=function(){var r=e.byteLength-t,a=new Uint8Array(4),s=Math.min(4,t);if(0===s)throw new Error("no bytes available");a.set(e.subarray(r,r+s)),i=new DataView(a.buffer).getUint32(0),n=8*s,t-=s},this.skipBits=function(e){var r;n>e?(i<<=e,n-=e):(e-=n,e-=8*(r=Math.floor(e/8)),t-=r,this.loadWord(),i<<=e,n-=e)},this.readBits=function(e){var r=Math.min(n,e),a=i>>>32-r;return(n-=r)>0?i<<=r:t>0&&this.loadWord(),(r=e-r)>0?a<>>e))return i<<=e,n-=e,e;return this.loadWord(),e+this.skipLeadingZeros()},this.skipUnsignedExpGolomb=function(){this.skipBits(1+this.skipLeadingZeros())},this.skipExpGolomb=function(){this.skipBits(1+this.skipLeadingZeros())},this.readUnsignedExpGolomb=function(){var e=this.skipLeadingZeros();return this.readBits(e+1)-1},this.readExpGolomb=function(){var e=this.readUnsignedExpGolomb();return 1&e?1+e>>>1:-1*(e>>>1)},this.readBoolean=function(){return 1===this.readBits(1)},this.readUnsignedByte=function(){return this.readBits(8)},this.loadWord()};(lt=function(){var e,t,i=0;lt.prototype.init.call(this),this.push=function(n){var r;t?((r=new Uint8Array(t.byteLength+n.data.byteLength)).set(t),r.set(n.data,t.byteLength),t=r):t=n.data;for(var a=t.byteLength;i3&&this.trigger("data",t.subarray(i+3)),t=null,i=0,this.trigger("done")},this.endTimeline=function(){this.flush(),this.trigger("endedtimeline")}}).prototype=new V,ht={100:!0,110:!0,122:!0,244:!0,44:!0,83:!0,86:!0,118:!0,128:!0,138:!0,139:!0,134:!0},(ut=function(){var e,t,i,n,r,a,s,o=new lt;ut.prototype.init.call(this),e=this,this.push=function(e){"video"===e.type&&(t=e.trackId,i=e.pts,n=e.dts,o.push(e))},o.on("data",(function(s){var o={trackId:t,pts:i,dts:n,data:s,nalUnitTypeCode:31&s[0]};switch(o.nalUnitTypeCode){case 5:o.nalUnitType="slice_layer_without_partitioning_rbsp_idr";break;case 6:o.nalUnitType="sei_rbsp",o.escapedRBSP=r(s.subarray(1));break;case 7:o.nalUnitType="seq_parameter_set_rbsp",o.escapedRBSP=r(s.subarray(1)),o.config=a(o.escapedRBSP);break;case 8:o.nalUnitType="pic_parameter_set_rbsp";break;case 9:o.nalUnitType="access_unit_delimiter_rbsp"}e.trigger("data",o)})),o.on("done",(function(){e.trigger("done")})),o.on("partialdone",(function(){e.trigger("partialdone")})),o.on("reset",(function(){e.trigger("reset")})),o.on("endedtimeline",(function(){e.trigger("endedtimeline")})),this.flush=function(){o.flush()},this.partialFlush=function(){o.partialFlush()},this.reset=function(){o.reset()},this.endTimeline=function(){o.endTimeline()},s=function(e,t){var i,n=8,r=8;for(i=0;i=0?i:0,(16&e[t+5])>>4?i+20:i+10},gt=function(e){return e[0]<<21|e[1]<<14|e[2]<<7|e[3]},vt={isLikelyAacData:function(e){var t=function e(t,i){return t.length-i<10||t[i]!=="I".charCodeAt(0)||t[i+1]!=="D".charCodeAt(0)||t[i+2]!=="3".charCodeAt(0)?i:e(t,i+=_t(t,i))}(e,0);return e.length>=t+2&&255==(255&e[t])&&240==(240&e[t+1])&&16==(22&e[t+1])},parseId3TagSize:_t,parseAdtsSize:function(e,t){var i=(224&e[t+5])>>5,n=e[t+4]<<3;return 6144&e[t+3]|n|i},parseType:function(e,t){return e[t]==="I".charCodeAt(0)&&e[t+1]==="D".charCodeAt(0)&&e[t+2]==="3".charCodeAt(0)?"timed-metadata":!0&e[t]&&240==(240&e[t+1])?"audio":null},parseSampleRate:function(e){for(var t=0;t+5>>2];t++}return null},parseAacTimestamp:function(e){var t,i,n;t=10,64&e[5]&&(t+=4,t+=gt(e.subarray(10,14)));do{if((i=gt(e.subarray(t+4,t+8)))<1)return null;if("PRIV"===String.fromCharCode(e[t],e[t+1],e[t+2],e[t+3])){n=e.subarray(t+10,t+i+10);for(var r=0;r>>2;return s*=4,s+=3&a[7]}break}}t+=10,t+=i}while(t=3;)if(e[u]!=="I".charCodeAt(0)||e[u+1]!=="D".charCodeAt(0)||e[u+2]!=="3".charCodeAt(0))if(255!=(255&e[u])||240!=(240&e[u+1]))u++;else{if(e.length-u<7)break;if(u+(o=vt.parseAdtsSize(e,u))>e.length)break;a={type:"audio",data:e.subarray(u,u+o),pts:t,dts:t},this.trigger("data",a),u+=o}else{if(e.length-u<10)break;if(u+(o=vt.parseId3TagSize(e,u))>e.length)break;r={type:"timed-metadata",data:e.subarray(u,u+o)},this.trigger("data",r),u+=o}n=e.length-u,e=n>0?e.subarray(u):new Uint8Array},this.reset=function(){e=new Uint8Array,this.trigger("reset")},this.endTimeline=function(){e=new Uint8Array,this.trigger("endedtimeline")}}).prototype=new V;var yt,bt,St,Tt,Et=ft,wt=["audioobjecttype","channelcount","samplerate","samplingfrequencyindex","samplesize"],At=["width","height","profileIdc","levelIdc","profileCompatibility","sarRatio"],Ct=pt.H264Stream,kt=vt.isLikelyAacData,Pt=he,It=function(e,t){var i;if(e.length!==t.length)return!1;for(i=0;i=-1e4&&i<=45e3&&(!n||o>i)&&(n=a,o=i));return n?n.gop:null},this.alignGopsAtStart_=function(e){var t,i,n,r,a,o,u,l;for(a=e.byteLength,o=e.nalCount,u=e.duration,t=i=0;tn.pts?t++:(i++,a-=r.byteLength,o-=r.nalCount,u-=r.duration);return 0===i?e:i===e.length?null:((l=e.slice(i)).byteLength=a,l.duration=u,l.nalCount=o,l.pts=l[0].pts,l.dts=l[0].dts,l)},this.alignGopsAtEnd_=function(e){var t,i,n,r,a,o,u;for(t=s.length-1,i=e.length-1,a=null,o=!1;t>=0&&i>=0;){if(n=s[t],r=e[i],n.pts===r.pts){o=!0;break}n.pts>r.pts?t--:(t===s.length-1&&(a=i),i--)}if(!o&&null===a)return null;if(0===(u=o?i:a))return e;var l=e.slice(u),h=l.reduce((function(e,t){return e.byteLength+=t.byteLength,e.duration+=t.duration,e.nalCount+=t.nalCount,e}),{byteLength:0,duration:0,nalCount:0});return l.byteLength=h.byteLength,l.duration=h.duration,l.nalCount=h.nalCount,l.pts=l[0].pts,l.dts=l[0].dts,l},this.alignGopsWith=function(e){s=e}}).prototype=new V,(Tt=function(e,t){this.numberOfTracks=0,this.metadataStream=t,void 0!==(e=e||{}).remux?this.remuxTracks=!!e.remux:this.remuxTracks=!0,"boolean"==typeof e.keepOriginalTimestamps?this.keepOriginalTimestamps=e.keepOriginalTimestamps:this.keepOriginalTimestamps=!1,this.pendingTracks=[],this.videoTrack=null,this.pendingBoxes=[],this.pendingCaptions=[],this.pendingMetadata=[],this.pendingBytes=0,this.emittedTracks=0,Tt.prototype.init.call(this),this.push=function(e){return e.text?this.pendingCaptions.push(e):e.frames?this.pendingMetadata.push(e):(this.pendingTracks.push(e.track),this.pendingBytes+=e.boxes.byteLength,"video"===e.track.type&&(this.videoTrack=e.track,this.pendingBoxes.push(e.boxes)),void("audio"===e.track.type&&(this.audioTrack=e.track,this.pendingBoxes.unshift(e.boxes))))}}).prototype=new V,Tt.prototype.flush=function(e){var t,i,n,r,a=0,s={captions:[],captionStreams:{},metadata:[],info:{}},o=0;if(this.pendingTracks.length=this.numberOfTracks&&(this.trigger("done"),this.emittedTracks=0))}if(this.videoTrack?(o=this.videoTrack.timelineStartInfo.pts,At.forEach((function(e){s.info[e]=this.videoTrack[e]}),this)):this.audioTrack&&(o=this.audioTrack.timelineStartInfo.pts,wt.forEach((function(e){s.info[e]=this.audioTrack[e]}),this)),this.videoTrack||this.audioTrack){for(1===this.pendingTracks.length?s.type=this.pendingTracks[0].type:s.type="combined",this.emittedTracks+=this.pendingTracks.length,n=Z(this.pendingTracks),s.initSegment=new Uint8Array(n.byteLength),s.initSegment.set(n),s.data=new Uint8Array(this.pendingBytes),r=0;r=this.numberOfTracks&&(this.trigger("done"),this.emittedTracks=0)},Tt.prototype.setRemux=function(e){this.remuxTracks=e},(St=function(e){var t,i,n=this,r=!0;St.prototype.init.call(this),e=e||{},this.baseMediaDecodeTime=e.baseMediaDecodeTime||0,this.transmuxPipeline_={},this.setupAacPipeline=function(){var r={};this.transmuxPipeline_=r,r.type="aac",r.metadataStream=new at.MetadataStream,r.aacStream=new Et,r.audioTimestampRolloverStream=new at.TimestampRolloverStream("audio"),r.timedMetadataTimestampRolloverStream=new at.TimestampRolloverStream("timed-metadata"),r.adtsStream=new dt,r.coalesceStream=new Tt(e,r.metadataStream),r.headOfPipeline=r.aacStream,r.aacStream.pipe(r.audioTimestampRolloverStream).pipe(r.adtsStream),r.aacStream.pipe(r.timedMetadataTimestampRolloverStream).pipe(r.metadataStream).pipe(r.coalesceStream),r.metadataStream.on("timestamp",(function(e){r.aacStream.setTimestamp(e.timeStamp)})),r.aacStream.on("data",(function(a){"timed-metadata"!==a.type&&"audio"!==a.type||r.audioSegmentStream||(i=i||{timelineStartInfo:{baseMediaDecodeTime:n.baseMediaDecodeTime},codec:"adts",type:"audio"},r.coalesceStream.numberOfTracks++,r.audioSegmentStream=new bt(i,e),r.audioSegmentStream.on("log",n.getLogTrigger_("audioSegmentStream")),r.audioSegmentStream.on("timingInfo",n.trigger.bind(n,"audioTimingInfo")),r.adtsStream.pipe(r.audioSegmentStream).pipe(r.coalesceStream),n.trigger("trackinfo",{hasAudio:!!i,hasVideo:!!t}))})),r.coalesceStream.on("data",this.trigger.bind(this,"data")),r.coalesceStream.on("done",this.trigger.bind(this,"done"))},this.setupTsPipeline=function(){var r={};this.transmuxPipeline_=r,r.type="ts",r.metadataStream=new at.MetadataStream,r.packetStream=new at.TransportPacketStream,r.parseStream=new at.TransportParseStream,r.elementaryStream=new at.ElementaryStream,r.timestampRolloverStream=new at.TimestampRolloverStream,r.adtsStream=new dt,r.h264Stream=new Ct,r.captionStream=new at.CaptionStream(e),r.coalesceStream=new Tt(e,r.metadataStream),r.headOfPipeline=r.packetStream,r.packetStream.pipe(r.parseStream).pipe(r.elementaryStream).pipe(r.timestampRolloverStream),r.timestampRolloverStream.pipe(r.h264Stream),r.timestampRolloverStream.pipe(r.adtsStream),r.timestampRolloverStream.pipe(r.metadataStream).pipe(r.coalesceStream),r.h264Stream.pipe(r.captionStream).pipe(r.coalesceStream),r.elementaryStream.on("data",(function(a){var s;if("metadata"===a.type){for(s=a.tracks.length;s--;)t||"video"!==a.tracks[s].type?i||"audio"!==a.tracks[s].type||((i=a.tracks[s]).timelineStartInfo.baseMediaDecodeTime=n.baseMediaDecodeTime):(t=a.tracks[s]).timelineStartInfo.baseMediaDecodeTime=n.baseMediaDecodeTime;t&&!r.videoSegmentStream&&(r.coalesceStream.numberOfTracks++,r.videoSegmentStream=new yt(t,e),r.videoSegmentStream.on("log",n.getLogTrigger_("videoSegmentStream")),r.videoSegmentStream.on("timelineStartInfo",(function(t){i&&!e.keepOriginalTimestamps&&(i.timelineStartInfo=t,r.audioSegmentStream.setEarliestDts(t.dts-n.baseMediaDecodeTime))})),r.videoSegmentStream.on("processedGopsInfo",n.trigger.bind(n,"gopInfo")),r.videoSegmentStream.on("segmentTimingInfo",n.trigger.bind(n,"videoSegmentTimingInfo")),r.videoSegmentStream.on("baseMediaDecodeTime",(function(e){i&&r.audioSegmentStream.setVideoBaseMediaDecodeTime(e)})),r.videoSegmentStream.on("timingInfo",n.trigger.bind(n,"videoTimingInfo")),r.h264Stream.pipe(r.videoSegmentStream).pipe(r.coalesceStream)),i&&!r.audioSegmentStream&&(r.coalesceStream.numberOfTracks++,r.audioSegmentStream=new bt(i,e),r.audioSegmentStream.on("log",n.getLogTrigger_("audioSegmentStream")),r.audioSegmentStream.on("timingInfo",n.trigger.bind(n,"audioTimingInfo")),r.audioSegmentStream.on("segmentTimingInfo",n.trigger.bind(n,"audioSegmentTimingInfo")),r.adtsStream.pipe(r.audioSegmentStream).pipe(r.coalesceStream)),n.trigger("trackinfo",{hasAudio:!!i,hasVideo:!!t})}})),r.coalesceStream.on("data",this.trigger.bind(this,"data")),r.coalesceStream.on("id3Frame",(function(e){e.dispatchType=r.metadataStream.dispatchType,n.trigger("id3Frame",e)})),r.coalesceStream.on("caption",this.trigger.bind(this,"caption")),r.coalesceStream.on("done",this.trigger.bind(this,"done"))},this.setBaseMediaDecodeTime=function(n){var r=this.transmuxPipeline_;e.keepOriginalTimestamps||(this.baseMediaDecodeTime=n),i&&(i.timelineStartInfo.dts=void 0,i.timelineStartInfo.pts=void 0,Se(i),r.audioTimestampRolloverStream&&r.audioTimestampRolloverStream.discontinuity()),t&&(r.videoSegmentStream&&(r.videoSegmentStream.gopCache_=[]),t.timelineStartInfo.dts=void 0,t.timelineStartInfo.pts=void 0,Se(t),r.captionStream.reset()),r.timestampRolloverStream&&r.timestampRolloverStream.discontinuity()},this.setAudioAppendStart=function(e){i&&this.transmuxPipeline_.audioSegmentStream.setAudioAppendStart(e)},this.setRemux=function(t){var i=this.transmuxPipeline_;e.remux=t,i&&i.coalesceStream&&i.coalesceStream.setRemux(t)},this.alignGopsWith=function(e){t&&this.transmuxPipeline_.videoSegmentStream&&this.transmuxPipeline_.videoSegmentStream.alignGopsWith(e)},this.getLogTrigger_=function(e){var t=this;return function(i){i.stream=e,t.trigger("log",i)}},this.push=function(e){if(r){var t=kt(e);if(t&&"aac"!==this.transmuxPipeline_.type?this.setupAacPipeline():t||"ts"===this.transmuxPipeline_.type||this.setupTsPipeline(),this.transmuxPipeline_)for(var i=Object.keys(this.transmuxPipeline_),n=0;n>>0},Mt=function(e){var t="";return t+=String.fromCharCode(e[0]),t+=String.fromCharCode(e[1]),t+=String.fromCharCode(e[2]),t+=String.fromCharCode(e[3])},Ft=Ut,Bt=function e(t,i){var n,r,a,s,o,u=[];if(!i.length)return null;for(n=0;n1?n+r:t.byteLength,a===i[0]&&(1===i.length?u.push(t.subarray(n+8,s)):(o=e(t.subarray(n+8,s),i.slice(1))).length&&(u=u.concat(o))),n=s;return u},Nt=Ut,jt=function(e){var t={version:e[0],flags:new Uint8Array(e.subarray(1,4)),baseMediaDecodeTime:Nt(e[4]<<24|e[5]<<16|e[6]<<8|e[7])};return 1===t.version&&(t.baseMediaDecodeTime*=Math.pow(2,32),t.baseMediaDecodeTime+=Nt(e[8]<<24|e[9]<<16|e[10]<<8|e[11])),t},Vt=function(e){return{isLeading:(12&e[0])>>>2,dependsOn:3&e[0],isDependedOn:(192&e[1])>>>6,hasRedundancy:(48&e[1])>>>4,paddingValue:(14&e[1])>>>1,isNonSyncSample:1&e[1],degradationPriority:e[2]<<8|e[3]}},Ht=function(e){var t,i={version:e[0],flags:new Uint8Array(e.subarray(1,4)),samples:[]},n=new DataView(e.buffer,e.byteOffset,e.byteLength),r=1&i.flags[2],a=4&i.flags[2],s=1&i.flags[1],o=2&i.flags[1],u=4&i.flags[1],l=8&i.flags[1],h=n.getUint32(4),d=8;for(r&&(i.dataOffset=n.getInt32(d),d+=4),a&&h&&(t={flags:Vt(e.subarray(d,d+4))},d+=4,s&&(t.duration=n.getUint32(d),d+=4),o&&(t.size=n.getUint32(d),d+=4),l&&(1===i.version?t.compositionTimeOffset=n.getInt32(d):t.compositionTimeOffset=n.getUint32(d),d+=4),i.samples.push(t),h--);h--;)t={},s&&(t.duration=n.getUint32(d),d+=4),o&&(t.size=n.getUint32(d),d+=4),u&&(t.flags=Vt(e.subarray(d,d+4)),d+=4),l&&(1===i.version?t.compositionTimeOffset=n.getInt32(d):t.compositionTimeOffset=n.getUint32(d),d+=4),i.samples.push(t);return i},zt=function(e){var t,i=new DataView(e.buffer,e.byteOffset,e.byteLength),n={version:e[0],flags:new Uint8Array(e.subarray(1,4)),trackId:i.getUint32(4)},r=1&n.flags[2],a=2&n.flags[2],s=8&n.flags[2],o=16&n.flags[2],u=32&n.flags[2],l=65536&n.flags[0],h=131072&n.flags[0];return t=8,r&&(t+=4,n.baseDataOffset=i.getUint32(12),t+=4),a&&(n.sampleDescriptionIndex=i.getUint32(t),t+=4),s&&(n.defaultSampleDuration=i.getUint32(t),t+=4),o&&(n.defaultSampleSize=i.getUint32(t),t+=4),u&&(n.defaultSampleFlags=i.getUint32(t)),l&&(n.durationIsEmpty=!0),!r&&h&&(n.baseDataOffsetIsMoof=!0),n},Gt=ke,Wt=je.CaptionStream,Yt=function(e,t){for(var i=e,n=0;n0?jt(l[0]).baseMediaDecodeTime:0,d=Bt(a,["trun"]);t===u&&d.length>0&&(i=function(e,t,i){var n,r,a,s,o=new DataView(e.buffer,e.byteOffset,e.byteLength),u={logs:[],seiNals:[]};for(r=0;r+40;){var u=t.shift();this.parse(u,a,s)}return(o=function(e,t,i){if(null===t)return null;var n=qt(e,t)[t]||{};return{seiNals:n.seiNals,logs:n.logs,timescale:i}}(e,i,n))&&o.logs&&(r.logs=r.logs.concat(o.logs)),null!==o&&o.seiNals?(this.pushNals(o.seiNals),this.flushStream(),r):r.logs.length?{logs:r.logs,captions:[],captionStreams:[]}:null},this.pushNals=function(t){if(!this.isInitialized()||!t||0===t.length)return null;t.forEach((function(t){e.push(t)}))},this.flushStream=function(){if(!this.isInitialized())return null;a?e.partialFlush():e.flush()},this.clearParsedCaptions=function(){r.captions=[],r.captionStreams={},r.logs=[]},this.resetCaptionStream=function(){if(!this.isInitialized())return null;e.reset()},this.clearAllCaptions=function(){this.clearParsedCaptions(),this.resetCaptionStream()},this.reset=function(){t=[],i=null,n=null,r?this.clearParsedCaptions():r={captions:[],captionStreams:{},logs:[]},this.resetCaptionStream()},this.reset()},Xt=Ut,Qt=function(e){return("00"+e.toString(16)).slice(-2)};xt=function(e,t){var i,n,r;return i=Bt(t,["moof","traf"]),n=[].concat.apply([],i.map((function(t){return Bt(t,["tfhd"]).map((function(i){var n,r,a;return n=Xt(i[4]<<24|i[5]<<16|i[6]<<8|i[7]),r=e[n]||9e4,(a="number"!=typeof(a=Bt(t,["tfdt"]).map((function(e){var t,i;return t=e[0],i=Xt(e[4]<<24|e[5]<<16|e[6]<<8|e[7]),1===t&&(i*=Math.pow(2,32),i+=Xt(e[8]<<24|e[9]<<16|e[10]<<8|e[11])),i}))[0])||isNaN(a)?1/0:a)/r}))}))),r=Math.min.apply(null,n),isFinite(r)?r:0},Rt=function(e){var t=Bt(e,["moov","trak"]),i=[];return t.forEach((function(e){var t,n,r={},a=Bt(e,["tkhd"])[0];a&&(n=(t=new DataView(a.buffer,a.byteOffset,a.byteLength)).getUint8(0),r.id=0===n?t.getUint32(12):t.getUint32(20));var s=Bt(e,["mdia","hdlr"])[0];if(s){var o=Mt(s.subarray(8,12));r.type="vide"===o?"video":"soun"===o?"audio":o}var u=Bt(e,["mdia","minf","stbl","stsd"])[0];if(u){var l=u.subarray(8);r.codec=Mt(l.subarray(4,8));var h,d=Bt(l,[r.codec])[0];d&&(/^[a-z]vc[1-9]$/i.test(r.codec)?(h=d.subarray(78),"avcC"===Mt(h.subarray(4,8))&&h.length>11?(r.codec+=".",r.codec+=Qt(h[9]),r.codec+=Qt(h[10]),r.codec+=Qt(h[11])):r.codec="avc1.4d400d"):/^mp4[a,v]$/i.test(r.codec)?(h=d.subarray(28),"esds"===Mt(h.subarray(4,8))&&h.length>20&&0!==h[19]?(r.codec+="."+Qt(h[19]),r.codec+="."+Qt(h[20]>>>2&63).replace(/^0/,"")):r.codec="mp4a.40.2"):r.codec=r.codec.toLowerCase())}var c=Bt(e,["mdia","mdhd"])[0];c&&(r.timescale=Dt(c)),i.push(r)})),i};var $t=xt,Jt=Rt,Zt=(Dt=function(e){var t=0===e[0]?12:20;return Xt(e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3])},function(e){var t=31&e[1];return t<<=8,t|=e[2]}),ei=function(e){return!!(64&e[1])},ti=function(e){var t=0;return(48&e[3])>>>4>1&&(t+=e[4]+1),t},ii=function(e){switch(e){case 5:return"slice_layer_without_partitioning_rbsp_idr";case 6:return"sei_rbsp";case 7:return"seq_parameter_set_rbsp";case 8:return"pic_parameter_set_rbsp";case 9:return"access_unit_delimiter_rbsp";default:return null}},ni={parseType:function(e,t){var i=Zt(e);return 0===i?"pat":i===t?"pmt":t?"pes":null},parsePat:function(e){var t=ei(e),i=4+ti(e);return t&&(i+=e[i]+1),(31&e[i+10])<<8|e[i+11]},parsePmt:function(e){var t={},i=ei(e),n=4+ti(e);if(i&&(n+=e[n]+1),1&e[n+5]){var r;r=3+((15&e[n+1])<<8|e[n+2])-4;for(var a=12+((15&e[n+10])<<8|e[n+11]);a=e.byteLength)return null;var i,n=null;return 192&(i=e[t+7])&&((n={}).pts=(14&e[t+9])<<27|(255&e[t+10])<<20|(254&e[t+11])<<12|(255&e[t+12])<<5|(254&e[t+13])>>>3,n.pts*=4,n.pts+=(6&e[t+13])>>>1,n.dts=n.pts,64&i&&(n.dts=(14&e[t+14])<<27|(255&e[t+15])<<20|(254&e[t+16])<<12|(255&e[t+17])<<5|(254&e[t+18])>>>3,n.dts*=4,n.dts+=(6&e[t+18])>>>1)),n},videoPacketContainsKeyFrame:function(e){for(var t=4+ti(e),i=e.subarray(t),n=0,r=0,a=!1;r3&&"slice_layer_without_partitioning_rbsp_idr"===ii(31&i[r+3])&&(a=!0),a}},ri=Ye,ai={};ai.ts=ni,ai.aac=vt;var si=he,oi=function(e,t,i){for(var n,r,a,s,o=0,u=188,l=!1;u<=e.byteLength;)if(71!==e[o]||71!==e[u]&&u!==e.byteLength)o++,u++;else{switch(n=e.subarray(o,u),ai.ts.parseType(n,t.pid)){case"pes":r=ai.ts.parsePesType(n,t.table),a=ai.ts.parsePayloadUnitStartIndicator(n),"audio"===r&&a&&(s=ai.ts.parsePesTime(n))&&(s.type="audio",i.audio.push(s),l=!0)}if(l)break;o+=188,u+=188}for(o=(u=e.byteLength)-188,l=!1;o>=0;)if(71!==e[o]||71!==e[u]&&u!==e.byteLength)o--,u--;else{switch(n=e.subarray(o,u),ai.ts.parseType(n,t.pid)){case"pes":r=ai.ts.parsePesType(n,t.table),a=ai.ts.parsePayloadUnitStartIndicator(n),"audio"===r&&a&&(s=ai.ts.parsePesTime(n))&&(s.type="audio",i.audio.push(s),l=!0)}if(l)break;o-=188,u-=188}},ui=function(e,t,i){for(var n,r,a,s,o,u,l,h=0,d=188,c=!1,f={data:[],size:0};d=0;)if(71!==e[h]||71!==e[d])h--,d--;else{switch(n=e.subarray(h,d),ai.ts.parseType(n,t.pid)){case"pes":r=ai.ts.parsePesType(n,t.table),a=ai.ts.parsePayloadUnitStartIndicator(n),"video"===r&&a&&(s=ai.ts.parsePesTime(n))&&(s.type="video",i.video.push(s),c=!0)}if(c)break;h-=188,d-=188}},li=function(e){var t={pid:null,table:null},i={};for(var n in function(e,t){for(var i,n=0,r=188;r=3;){switch(ai.aac.parseType(e,o)){case"timed-metadata":if(e.length-o<10){i=!0;break}if((s=ai.aac.parseId3TagSize(e,o))>e.length){i=!0;break}null===a&&(t=e.subarray(o,o+s),a=ai.aac.parseAacTimestamp(t)),o+=s;break;case"audio":if(e.length-o<7){i=!0;break}if((s=ai.aac.parseAdtsSize(e,o))>e.length){i=!0;break}null===r&&(t=e.subarray(o,o+s),r=ai.aac.parseSampleRate(t)),n++,o+=s;break;default:o++}if(i)return null}if(null===r||null===a)return null;var u=si/r;return{audio:[{type:"audio",dts:a,pts:a},{type:"audio",dts:a+1024*n*u,pts:a+1024*n*u}]}}(e):li(e))&&(i.audio||i.video)?(function(e,t){if(e.audio&&e.audio.length){var i=t;(void 0===i||isNaN(i))&&(i=e.audio[0].dts),e.audio.forEach((function(e){e.dts=ri(e.dts,i),e.pts=ri(e.pts,i),e.dtsTime=e.dts/si,e.ptsTime=e.pts/si}))}if(e.video&&e.video.length){var n=t;if((void 0===n||isNaN(n))&&(n=e.video[0].dts),e.video.forEach((function(e){e.dts=ri(e.dts,n),e.pts=ri(e.pts,n),e.dtsTime=e.dts/si,e.ptsTime=e.pts/si})),e.firstKeyFrame){var r=e.firstKeyFrame;r.dts=ri(r.dts,n),r.pts=ri(r.pts,n),r.dtsTime=r.dts/si,r.ptsTime=r.pts/si}}}(i,t),i):null},di=function(){function e(e,t){this.options=t||{},this.self=e,this.init()}var t=e.prototype;return t.init=function(){this.transmuxer&&this.transmuxer.dispose(),this.transmuxer=new Ot.Transmuxer(this.options),function(e,t){t.on("data",(function(t){var i=t.initSegment;t.initSegment={data:i.buffer,byteOffset:i.byteOffset,byteLength:i.byteLength};var n=t.data;t.data=n.buffer,e.postMessage({action:"data",segment:t,byteOffset:n.byteOffset,byteLength:n.byteLength},[t.data])})),t.on("done",(function(t){e.postMessage({action:"done"})})),t.on("gopInfo",(function(t){e.postMessage({action:"gopInfo",gopInfo:t})})),t.on("videoSegmentTimingInfo",(function(t){var i={start:{decode:ce(t.start.dts),presentation:ce(t.start.pts)},end:{decode:ce(t.end.dts),presentation:ce(t.end.pts)},baseMediaDecodeTime:ce(t.baseMediaDecodeTime)};t.prependedContentDuration&&(i.prependedContentDuration=ce(t.prependedContentDuration)),e.postMessage({action:"videoSegmentTimingInfo",videoSegmentTimingInfo:i})})),t.on("audioSegmentTimingInfo",(function(t){var i={start:{decode:ce(t.start.dts),presentation:ce(t.start.pts)},end:{decode:ce(t.end.dts),presentation:ce(t.end.pts)},baseMediaDecodeTime:ce(t.baseMediaDecodeTime)};t.prependedContentDuration&&(i.prependedContentDuration=ce(t.prependedContentDuration)),e.postMessage({action:"audioSegmentTimingInfo",audioSegmentTimingInfo:i})})),t.on("id3Frame",(function(t){e.postMessage({action:"id3Frame",id3Frame:t})})),t.on("caption",(function(t){e.postMessage({action:"caption",caption:t})})),t.on("trackinfo",(function(t){e.postMessage({action:"trackinfo",trackInfo:t})})),t.on("audioTimingInfo",(function(t){e.postMessage({action:"audioTimingInfo",audioTimingInfo:{start:ce(t.start),end:ce(t.end)}})})),t.on("videoTimingInfo",(function(t){e.postMessage({action:"videoTimingInfo",videoTimingInfo:{start:ce(t.start),end:ce(t.end)}})})),t.on("log",(function(t){e.postMessage({action:"log",log:t})}))}(this.self,this.transmuxer)},t.pushMp4Captions=function(e){this.captionParser||(this.captionParser=new Kt,this.captionParser.init());var t=new Uint8Array(e.data,e.byteOffset,e.byteLength),i=this.captionParser.parse(t,e.trackIds,e.timescales);this.self.postMessage({action:"mp4Captions",captions:i&&i.captions||[],logs:i&&i.logs||[],data:t.buffer},[t.buffer])},t.probeMp4StartTime=function(e){var t=e.timescales,i=e.data,n=$t(t,i);this.self.postMessage({action:"probeMp4StartTime",startTime:n,data:i},[i.buffer])},t.probeMp4Tracks=function(e){var t=e.data,i=Jt(t);this.self.postMessage({action:"probeMp4Tracks",tracks:i,data:t},[t.buffer])},t.probeTs=function(e){var t=e.data,i=e.baseStartTime,n="number"!=typeof i||isNaN(i)?void 0:i*he,r=hi(t,n),a=null;r&&((a={hasVideo:r.video&&2===r.video.length||!1,hasAudio:r.audio&&2===r.audio.length||!1}).hasVideo&&(a.videoStart=r.video[0].ptsTime),a.hasAudio&&(a.audioStart=r.audio[0].ptsTime)),this.self.postMessage({action:"probeTs",result:a,data:t},[t.buffer])},t.clearAllMp4Captions=function(){this.captionParser&&this.captionParser.clearAllCaptions()},t.clearParsedMp4Captions=function(){this.captionParser&&this.captionParser.clearParsedCaptions()},t.push=function(e){var t=new Uint8Array(e.data,e.byteOffset,e.byteLength);this.transmuxer.push(t)},t.reset=function(){this.transmuxer.reset()},t.setTimestampOffset=function(e){var t=e.timestampOffset||0;this.transmuxer.setBaseMediaDecodeTime(Math.round(de(t)))},t.setAudioAppendStart=function(e){this.transmuxer.setAudioAppendStart(Math.ceil(de(e.appendStart)))},t.setRemux=function(e){this.transmuxer.setRemux(e.remux)},t.flush=function(e){this.transmuxer.flush(),self.postMessage({action:"done",type:"transmuxed"})},t.endTimeline=function(){this.transmuxer.endTimeline(),self.postMessage({action:"endedtimeline",type:"transmuxed"})},t.alignGopsWith=function(e){this.transmuxer.alignGopsWith(e.gopsToAlignWith.slice())},e}();self.onmessage=function(e){"init"===e.data.action&&e.data.options?this.messageHandlers=new di(self,e.data.options):(this.messageHandlers||(this.messageHandlers=new di(self)),e.data&&e.data.action&&"init"!==e.data.action&&this.messageHandlers[e.data.action]&&this.messageHandlers[e.data.action](e.data))}})))),ls=function(e){var t=e.transmuxer,i=e.bytes,n=e.audioAppendStart,r=e.gopsToAlignWith,a=e.remux,s=e.onData,o=e.onTrackInfo,u=e.onAudioTimingInfo,l=e.onVideoTimingInfo,h=e.onVideoSegmentTimingInfo,d=e.onAudioSegmentTimingInfo,c=e.onId3,f=e.onCaptions,p=e.onDone,m=e.onEndedTimeline,_=e.onTransmuxerLog,g=e.isEndOfTimeline,v={buffer:[]},y=g;if(t.onmessage=function(i){t.currentTransmux===e&&("data"===i.data.action&&function(e,t,i){var n=e.data.segment,r=n.type,a=n.initSegment,s=n.captions,o=n.captionStreams,u=n.metadata,l=n.videoFrameDtsTime,h=n.videoFramePtsTime;t.buffer.push({captions:s,captionStreams:o,metadata:u});var d=e.data.segment.boxes||{data:e.data.segment.data},c={type:r,data:new Uint8Array(d.data,d.data.byteOffset,d.data.byteLength),initSegment:new Uint8Array(a.data,a.byteOffset,a.byteLength)};void 0!==l&&(c.videoFrameDtsTime=l),void 0!==h&&(c.videoFramePtsTime=h),i(c)}(i,v,s),"trackinfo"===i.data.action&&o(i.data.trackInfo),"gopInfo"===i.data.action&&function(e,t){t.gopInfo=e.data.gopInfo}(i,v),"audioTimingInfo"===i.data.action&&u(i.data.audioTimingInfo),"videoTimingInfo"===i.data.action&&l(i.data.videoTimingInfo),"videoSegmentTimingInfo"===i.data.action&&h(i.data.videoSegmentTimingInfo),"audioSegmentTimingInfo"===i.data.action&&d(i.data.audioSegmentTimingInfo),"id3Frame"===i.data.action&&c([i.data.id3Frame],i.data.id3Frame.dispatchType),"caption"===i.data.action&&f(i.data.caption),"endedtimeline"===i.data.action&&(y=!1,m()),"log"===i.data.action&&_(i.data.log),"transmuxed"===i.data.type&&(y||(t.onmessage=null,function(e){var t=e.transmuxedData,i=e.callback;t.buffer=[],i(t)}({transmuxedData:v,callback:p}),hs(t))))},n&&t.postMessage({action:"setAudioAppendStart",appendStart:n}),Array.isArray(r)&&t.postMessage({action:"alignGopsWith",gopsToAlignWith:r}),void 0!==a&&t.postMessage({action:"setRemux",remux:a}),i.byteLength){var b=i instanceof ArrayBuffer?i:i.buffer,S=i instanceof ArrayBuffer?0:i.byteOffset;t.postMessage({action:"push",data:b,byteOffset:S,byteLength:i.byteLength},[b])}g&&t.postMessage({action:"endTimeline"}),t.postMessage({action:"flush"})},hs=function(e){e.currentTransmux=null,e.transmuxQueue.length&&(e.currentTransmux=e.transmuxQueue.shift(),"function"==typeof e.currentTransmux?e.currentTransmux():ls(e.currentTransmux))},ds=function(e,t){e.postMessage({action:t}),hs(e)},cs=function(e,t){if(!t.currentTransmux)return t.currentTransmux=e,void ds(t,e);t.transmuxQueue.push(ds.bind(null,t,e))},fs=function(e){if(!e.transmuxer.currentTransmux)return e.transmuxer.currentTransmux=e,void ls(e);e.transmuxer.transmuxQueue.push(e)},ps=function(e){cs("reset",e)},ms=function(e){var t=new us;t.currentTransmux=null,t.transmuxQueue=[];var i=t.terminate;return t.terminate=function(){return t.currentTransmux=null,t.transmuxQueue.length=0,i.call(t)},t.postMessage({action:"init",options:e}),t},_s=function(e){var t=e.transmuxer,i=e.endAction||e.action,n=e.callback,r=P.default({},e,{endAction:null,transmuxer:null,callback:null});if(t.addEventListener("message",(function r(a){a.data.action===i&&(t.removeEventListener("message",r),a.data.data&&(a.data.data=new Uint8Array(a.data.data,e.byteOffset||0,e.byteLength||a.data.data.byteLength),e.data&&(e.data=a.data.data)),n(a.data))})),e.data){var a=e.data instanceof ArrayBuffer;r.byteOffset=a?0:e.data.byteOffset,r.byteLength=e.data.byteLength;var s=[a?e.data:e.data.buffer];t.postMessage(r,s)}else t.postMessage(r)},gs=2,vs=-101,ys=-102,bs=function(e){e.forEach((function(e){e.abort()}))},Ss=function(e,t){return t.timedout?{status:t.status,message:"HLS request timed-out at URL: "+t.uri,code:vs,xhr:t}:t.aborted?{status:t.status,message:"HLS request aborted at URL: "+t.uri,code:ys,xhr:t}:e?{status:t.status,message:"HLS request errored at URL: "+t.uri,code:gs,xhr:t}:"arraybuffer"===t.responseType&&0===t.response.byteLength?{status:t.status,message:"Empty HLS response at URL: "+t.uri,code:gs,xhr:t}:null},Ts=function(e,t,i){return function(n,r){var a=r.response,s=Ss(n,r);if(s)return i(s,e);if(16!==a.byteLength)return i({status:r.status,message:"Invalid HLS key at URL: "+r.uri,code:gs,xhr:r},e);for(var o=new DataView(a),u=new Uint32Array([o.getUint32(0),o.getUint32(4),o.getUint32(8),o.getUint32(12)]),l=0;l1)return xs("multiple "+e+" codecs found as attributes: "+t[e].join(", ")+". Setting playlist codecs to null so that we wait for mux.js to probe segments for real codecs."),void(t[e]=null);t[e]=t[e][0]})),t},Os=function(e){var t=0;return e.audio&&t++,e.video&&t++,t},Us=function(e,t){var i=t.attributes||{},n=Ds(function(e){var t=e.attributes||{};if(t.CODECS)return _.parseCodecs(t.CODECS)}(t)||[]);if(Rs(e,t)&&!n.audio&&!function(e,t){if(!Rs(e,t))return!0;var i=t.attributes||{},n=e.mediaGroups.AUDIO[i.AUDIO];for(var r in n)if(!n[r].uri&&!n[r].playlists)return!0;return!1}(e,t)){var r=Ds(_.codecsFromDefault(e,i.AUDIO)||[]);r.audio&&(n.audio=r.audio)}return n},Ms=$r("PlaylistSelector"),Fs=function(e){if(e&&e.playlist){var t=e.playlist;return JSON.stringify({id:t.id,bandwidth:e.bandwidth,width:e.width,height:e.height,codecs:t.attributes&&t.attributes.CODECS||""})}},Bs=function(e,t){if(!e)return"";var i=C.default.getComputedStyle(e);return i?i[t]:""},Ns=function(e,t){var i=e.slice();e.sort((function(e,n){var r=t(e,n);return 0===r?i.indexOf(e)-i.indexOf(n):r}))},js=function(e,t){var i,n;return e.attributes.BANDWIDTH&&(i=e.attributes.BANDWIDTH),i=i||C.default.Number.MAX_VALUE,t.attributes.BANDWIDTH&&(n=t.attributes.BANDWIDTH),i-(n=n||C.default.Number.MAX_VALUE)},Vs=function(e,t,i,n,r,a){if(e){var s={bandwidth:t,width:i,height:n,limitRenditionByPlayerDimensions:r},o=e.playlists;Sa.isAudioOnly(e)&&(o=a.getAudioTrackPlaylists_(),s.audioOnly=!0);var u=o.map((function(e){var t=e.attributes&&e.attributes.RESOLUTION&&e.attributes.RESOLUTION.width,i=e.attributes&&e.attributes.RESOLUTION&&e.attributes.RESOLUTION.height;return{bandwidth:e.attributes&&e.attributes.BANDWIDTH||C.default.Number.MAX_VALUE,width:t,height:i,playlist:e}}));Ns(u,(function(e,t){return e.bandwidth-t.bandwidth}));var l=(u=u.filter((function(e){return!Sa.isIncompatible(e.playlist)}))).filter((function(e){return Sa.isEnabled(e.playlist)}));l.length||(l=u.filter((function(e){return!Sa.isDisabled(e.playlist)})));var h=l.filter((function(e){return e.bandwidth*ns.BANDWIDTH_VARIANCEi||e.height>n}))).filter((function(e){return e.width===g[0].width&&e.height===g[0].height})),d=v[v.length-1],y=v.filter((function(e){return e.bandwidth===d.bandwidth}))[0]),a.experimentalLeastPixelDiffSelector){var T=m.map((function(e){return e.pixelDiff=Math.abs(e.width-i)+Math.abs(e.height-n),e}));Ns(T,(function(e,t){return e.pixelDiff===t.pixelDiff?t.bandwidth-e.bandwidth:e.pixelDiff-t.pixelDiff})),b=T[0]}var E=b||y||S||c||l[0]||u[0];if(E&&E.playlist){var w="sortedPlaylistReps";return b?w="leastPixelDiffRep":y?w="resolutionPlusOneRep":S?w="resolutionBestRep":c?w="bandwidthBestRep":l[0]&&(w="enabledPlaylistReps"),Ms("choosing "+Fs(E)+" using "+w+" with options",s),E.playlist}return Ms("could not choose a playlist with options",s),null}},Hs=function(){var e=this.useDevicePixelRatio&&C.default.devicePixelRatio||1;return Vs(this.playlists.master,this.systemBandwidth,parseInt(Bs(this.tech_.el(),"width"),10)*e,parseInt(Bs(this.tech_.el(),"height"),10)*e,this.limitRenditionByPlayerDimensions,this.masterPlaylistController_)},zs=function(e){var t=e.inbandTextTracks,i=e.metadataArray,n=e.timestampOffset,r=e.videoDuration;if(i){var a=C.default.WebKitDataCue||C.default.VTTCue,s=t.metadataTrack_;if(s&&(i.forEach((function(e){var t=e.cueTime+n;!("number"!=typeof t||C.default.isNaN(t)||t<0)&&t<1/0&&e.frames.forEach((function(e){var i=new a(t,t,e.value||e.url||e.data||"");i.frame=e,i.value=e,function(e){Object.defineProperties(e.frame,{id:{get:function(){return Yr.log.warn("cue.frame.id is deprecated. Use cue.value.key instead."),e.value.key}},value:{get:function(){return Yr.log.warn("cue.frame.value is deprecated. Use cue.value.data instead."),e.value.data}},privateData:{get:function(){return Yr.log.warn("cue.frame.privateData is deprecated. Use cue.value.data instead."),e.value.data}}})}(i),s.addCue(i)}))})),s.cues&&s.cues.length)){for(var o=s.cues,u=[],l=0;l=e&&r.endTime<=t&&i.removeCue(r)},Ws=function(e){return"number"==typeof e&&isFinite(e)},Ys=function(e){var t=e.startOfSegment,i=e.duration,n=e.segment,r=e.part,a=e.playlist,s=a.mediaSequence,o=a.id,u=a.segments,l=void 0===u?[]:u,h=e.mediaIndex,d=e.partIndex,c=e.timeline,f=l.length-1,p="mediaIndex/partIndex increment";e.getMediaInfoForTime?p="getMediaInfoForTime ("+e.getMediaInfoForTime+")":e.isSyncRequest&&(p="getSyncSegmentCandidate (isSyncRequest)");var m="number"==typeof d,_=e.segment.uri?"segment":"pre-segment",g=m?oa({preloadSegment:n})-1:0;return _+" ["+(s+h)+"/"+(s+f)+"]"+(m?" part ["+d+"/"+g+"]":"")+" segment start/end ["+n.start+" => "+n.end+"]"+(m?" part start/end ["+r.start+" => "+r.end+"]":"")+" startOfSegment ["+t+"] duration ["+i+"] timeline ["+c+"] selected by ["+p+"] playlist ["+o+"]"},qs=function(e){return e+"TimingInfo"},Ks=function(e){var t=e.timelineChangeController,i=e.currentTimeline,n=e.segmentTimeline,r=e.loaderType,a=e.audioDisabled;if(i===n)return!1;if("audio"===r){var s=t.lastTimelineChange({type:"main"});return!s||s.to!==n}if("main"===r&&a){var o=t.pendingTimelineChange({type:"audio"});return!o||o.to!==n}return!1},Xs=function(e){var t=e.segmentDuration,i=e.maxDuration;return!!t&&Math.round(t)>i+1/30},Qs=function(e,t){if("hls"!==t)return null;var i,n,r,a,s=(i=e.audioTimingInfo,n=e.videoTimingInfo,r=i&&"number"==typeof i.start&&"number"==typeof i.end?i.end-i.start:0,a=n&&"number"==typeof n.start&&"number"==typeof n.end?n.end-n.start:0,Math.max(r,a));if(!s)return null;var o=e.playlist.targetDuration,u=Xs({segmentDuration:s,maxDuration:2*o}),l=Xs({segmentDuration:s,maxDuration:o}),h="Segment with index "+e.mediaIndex+" from playlist "+e.playlist.id+" has a duration of "+s+" when the reported duration is "+e.duration+" and the target duration is "+o+". For HLS content, a duration in excess of the target duration may result in playback issues. See the HLS specification section on EXT-X-TARGETDURATION for more details: https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1";return u||l?{severity:u?"warn":"info",message:h}:null},$s=function(e){function t(t,i){var n;if(n=e.call(this)||this,!t)throw new TypeError("Initialization settings are required");if("function"!=typeof t.currentTime)throw new TypeError("No currentTime getter specified");if(!t.mediaSource)throw new TypeError("No MediaSource specified");return n.bandwidth=t.bandwidth,n.throughput={rate:0,count:0},n.roundTrip=NaN,n.resetStats_(),n.mediaIndex=null,n.partIndex=null,n.hasPlayed_=t.hasPlayed,n.currentTime_=t.currentTime,n.seekable_=t.seekable,n.seeking_=t.seeking,n.duration_=t.duration,n.mediaSource_=t.mediaSource,n.vhs_=t.vhs,n.loaderType_=t.loaderType,n.currentMediaInfo_=void 0,n.startingMediaInfo_=void 0,n.segmentMetadataTrack_=t.segmentMetadataTrack,n.goalBufferLength_=t.goalBufferLength,n.sourceType_=t.sourceType,n.sourceUpdater_=t.sourceUpdater,n.inbandTextTracks_=t.inbandTextTracks,n.state_="INIT",n.timelineChangeController_=t.timelineChangeController,n.shouldSaveSegmentTimingInfo_=!0,n.parse708captions_=t.parse708captions,n.experimentalExactManifestTimings=t.experimentalExactManifestTimings,n.checkBufferTimeout_=null,n.error_=void 0,n.currentTimeline_=-1,n.pendingSegment_=null,n.xhrOptions_=null,n.pendingSegments_=[],n.audioDisabled_=!1,n.isPendingTimestampOffset_=!1,n.gopBuffer_=[],n.timeMapping_=0,n.safeAppend_=Yr.browser.IE_VERSION>=11,n.appendInitSegment_={audio:!0,video:!0},n.playlistOfLastInitSegment_={audio:null,video:null},n.callQueue_=[],n.loadQueue_=[],n.metadataQueue_={id3:[],caption:[]},n.waitingOnRemove_=!1,n.quotaExceededErrorRetryTimeout_=null,n.activeInitSegmentId_=null,n.initSegments_={},n.cacheEncryptionKeys_=t.cacheEncryptionKeys,n.keyCache_={},n.decrypter_=t.decrypter,n.syncController_=t.syncController,n.syncPoint_={segmentIndex:0,time:0},n.transmuxer_=n.createTransmuxer_(),n.triggerSyncInfoUpdate_=function(){return n.trigger("syncinfoupdate")},n.syncController_.on("syncinfoupdate",n.triggerSyncInfoUpdate_),n.mediaSource_.addEventListener("sourceopen",(function(){n.isEndOfStream_()||(n.ended_=!1)})),n.fetchAtBuffer_=!1,n.logger_=$r("SegmentLoader["+n.loaderType_+"]"),Object.defineProperty(I.default(n),"state",{get:function(){return this.state_},set:function(e){e!==this.state_&&(this.logger_(this.state_+" -> "+e),this.state_=e,this.trigger("statechange"))}}),n.sourceUpdater_.on("ready",(function(){n.hasEnoughInfoToAppend_()&&n.processCallQueue_()})),"main"===n.loaderType_&&n.timelineChangeController_.on("pendingtimelinechange",(function(){n.hasEnoughInfoToAppend_()&&n.processCallQueue_()})),"audio"===n.loaderType_&&n.timelineChangeController_.on("timelinechange",(function(){n.hasEnoughInfoToLoad_()&&n.processLoadQueue_(),n.hasEnoughInfoToAppend_()&&n.processCallQueue_()})),n}L.default(t,e);var i=t.prototype;return i.createTransmuxer_=function(){return ms({remux:!1,alignGopsAtEnd:this.safeAppend_,keepOriginalTimestamps:!0,parse708captions:this.parse708captions_})},i.resetStats_=function(){this.mediaBytesTransferred=0,this.mediaRequests=0,this.mediaRequestsAborted=0,this.mediaRequestsTimedout=0,this.mediaRequestsErrored=0,this.mediaTransferDuration=0,this.mediaSecondsLoaded=0,this.mediaAppends=0},i.dispose=function(){this.trigger("dispose"),this.state="DISPOSED",this.pause(),this.abort_(),this.transmuxer_&&this.transmuxer_.terminate(),this.resetStats_(),this.checkBufferTimeout_&&C.default.clearTimeout(this.checkBufferTimeout_),this.syncController_&&this.triggerSyncInfoUpdate_&&this.syncController_.off("syncinfoupdate",this.triggerSyncInfoUpdate_),this.off()},i.setAudio=function(e){this.audioDisabled_=!e,e?this.appendInitSegment_.audio=!0:this.sourceUpdater_.removeAudio(0,this.duration_())},i.abort=function(){"WAITING"===this.state?(this.abort_(),this.state="READY",this.paused()||this.monitorBuffer_()):this.pendingSegment_&&(this.pendingSegment_=null)},i.abort_=function(){this.pendingSegment_&&this.pendingSegment_.abortRequests&&this.pendingSegment_.abortRequests(),this.pendingSegment_=null,this.callQueue_=[],this.loadQueue_=[],this.metadataQueue_.id3=[],this.metadataQueue_.caption=[],this.timelineChangeController_.clearPendingTimelineChange(this.loaderType_),this.waitingOnRemove_=!1,C.default.clearTimeout(this.quotaExceededErrorRetryTimeout_),this.quotaExceededErrorRetryTimeout_=null},i.checkForAbort_=function(e){return"APPENDING"!==this.state||this.pendingSegment_?!this.pendingSegment_||this.pendingSegment_.requestId!==e:(this.state="READY",!0)},i.error=function(e){return void 0!==e&&(this.logger_("error occurred:",e),this.error_=e),this.pendingSegment_=null,this.error_},i.endOfStream=function(){this.ended_=!0,this.transmuxer_&&ps(this.transmuxer_),this.gopBuffer_.length=0,this.pause(),this.trigger("ended")},i.buffered_=function(){var e=this.getMediaInfo_();if(!this.sourceUpdater_||!e)return Yr.createTimeRanges();if("main"===this.loaderType_){var t=e.hasAudio,i=e.hasVideo,n=e.isMuxed;if(i&&t&&!this.audioDisabled_&&!n)return this.sourceUpdater_.buffered();if(i)return this.sourceUpdater_.videoBuffered()}return this.sourceUpdater_.audioBuffered()},i.initSegmentForMap=function(e,t){if(void 0===t&&(t=!1),!e)return null;var i=Wa(e),n=this.initSegments_[i];return t&&!n&&e.bytes&&(this.initSegments_[i]=n={resolvedUri:e.resolvedUri,byterange:e.byterange,bytes:e.bytes,tracks:e.tracks,timescales:e.timescales}),n||e},i.segmentKey=function(e,t){if(void 0===t&&(t=!1),!e)return null;var i=Ya(e),n=this.keyCache_[i];this.cacheEncryptionKeys_&&t&&!n&&e.bytes&&(this.keyCache_[i]=n={resolvedUri:e.resolvedUri,bytes:e.bytes});var r={resolvedUri:(n||e).resolvedUri};return n&&(r.bytes=n.bytes),r},i.couldBeginLoading_=function(){return this.playlist_&&!this.paused()},i.load=function(){if(this.monitorBuffer_(),this.playlist_)return"INIT"===this.state&&this.couldBeginLoading_()?this.init_():void(!this.couldBeginLoading_()||"READY"!==this.state&&"INIT"!==this.state||(this.state="READY"))},i.init_=function(){return this.state="READY",this.resetEverything(),this.monitorBuffer_()},i.playlist=function(e,t){if(void 0===t&&(t={}),e){var i=this.playlist_,n=this.pendingSegment_;this.playlist_=e,this.xhrOptions_=t,"INIT"===this.state&&(e.syncInfo={mediaSequence:e.mediaSequence,time:0},"main"===this.loaderType_&&this.syncController_.setDateTimeMappingForStart(e));var r=null;if(i&&(i.id?r=i.id:i.uri&&(r=i.uri)),this.logger_("playlist update ["+r+" => "+(e.id||e.uri)+"]"),this.trigger("syncinfoupdate"),"INIT"===this.state&&this.couldBeginLoading_())return this.init_();if(!i||i.uri!==e.uri)return null!==this.mediaIndex&&this.resyncLoader(),this.currentMediaInfo_=void 0,void this.trigger("playlistupdate");var a=e.mediaSequence-i.mediaSequence;if(this.logger_("live window shift ["+a+"]"),null!==this.mediaIndex)if(this.mediaIndex-=a,this.mediaIndex<0)this.mediaIndex=null,this.partIndex=null;else{var s=this.playlist_.segments[this.mediaIndex];if(this.partIndex&&(!s.parts||!s.parts.length||!s.parts[this.partIndex])){var o=this.mediaIndex;this.logger_("currently processing part (index "+this.partIndex+") no longer exists."),this.resetLoader(),this.mediaIndex=o}}n&&(n.mediaIndex-=a,n.mediaIndex<0?(n.mediaIndex=null,n.partIndex=null):(n.mediaIndex>=0&&(n.segment=e.segments[n.mediaIndex]),n.partIndex>=0&&n.segment.parts&&(n.part=n.segment.parts[n.partIndex]))),this.syncController_.saveExpiredSegmentInfo(i,e)}},i.pause=function(){this.checkBufferTimeout_&&(C.default.clearTimeout(this.checkBufferTimeout_),this.checkBufferTimeout_=null)},i.paused=function(){return null===this.checkBufferTimeout_},i.resetEverything=function(e){this.ended_=!1,this.appendInitSegment_={audio:!0,video:!0},this.resetLoader(),this.remove(0,1/0,e),this.transmuxer_&&this.transmuxer_.postMessage({action:"clearAllMp4Captions"})},i.resetLoader=function(){this.fetchAtBuffer_=!1,this.resyncLoader()},i.resyncLoader=function(){this.transmuxer_&&ps(this.transmuxer_),this.mediaIndex=null,this.partIndex=null,this.syncPoint_=null,this.isPendingTimestampOffset_=!1,this.callQueue_=[],this.loadQueue_=[],this.metadataQueue_.id3=[],this.metadataQueue_.caption=[],this.abort(),this.transmuxer_&&this.transmuxer_.postMessage({action:"clearParsedMp4Captions"})},i.remove=function(e,t,i,n){if(void 0===i&&(i=function(){}),void 0===n&&(n=!1),t===1/0&&(t=this.duration_()),t<=e)this.logger_("skipping remove because end ${end} is <= start ${start}");else if(this.sourceUpdater_&&this.getMediaInfo_()){var r=1,a=function(){0===--r&&i()};for(var s in!n&&this.audioDisabled_||(r++,this.sourceUpdater_.removeAudio(e,t,a)),(n||"main"===this.loaderType_)&&(this.gopBuffer_=function(e,t,i,n){for(var r=Math.ceil((t-n)*E.ONE_SECOND_IN_TS),a=Math.ceil((i-n)*E.ONE_SECOND_IN_TS),s=e.slice(),o=e.length;o--&&!(e[o].pts<=a););if(-1===o)return s;for(var u=o+1;u--&&!(e[u].pts<=r););return u=Math.max(u,0),s.splice(u,o-u+1),s}(this.gopBuffer_,e,t,this.timeMapping_),r++,this.sourceUpdater_.removeVideo(e,t,a)),this.inbandTextTracks_)Gs(e,t,this.inbandTextTracks_[s]);Gs(e,t,this.segmentMetadataTrack_),a()}else this.logger_("skipping remove because no source updater or starting media info")},i.monitorBuffer_=function(){this.checkBufferTimeout_&&C.default.clearTimeout(this.checkBufferTimeout_),this.checkBufferTimeout_=C.default.setTimeout(this.monitorBufferTick_.bind(this),1)},i.monitorBufferTick_=function(){"READY"===this.state&&this.fillBuffer_(),this.checkBufferTimeout_&&C.default.clearTimeout(this.checkBufferTimeout_),this.checkBufferTimeout_=C.default.setTimeout(this.monitorBufferTick_.bind(this),500)},i.fillBuffer_=function(){if(!this.sourceUpdater_.updating()){var e=this.chooseNextRequest_();e&&("number"==typeof e.timestampOffset&&(this.isPendingTimestampOffset_=!1,this.timelineChangeController_.pendingTimelineChange({type:this.loaderType_,from:this.currentTimeline_,to:e.timeline})),this.loadSegment_(e))}},i.isEndOfStream_=function(e,t,i){if(void 0===e&&(e=this.mediaIndex),void 0===t&&(t=this.playlist_),void 0===i&&(i=this.partIndex),!t||!this.mediaSource_)return!1;var n="number"==typeof e&&t.segments[e],r=e+1===t.segments.length,a=!n||!n.parts||i+1===n.parts.length;return t.endList&&"open"===this.mediaSource_.readyState&&r&&a},i.chooseNextRequest_=function(){var e=na(this.buffered_())||0,t=Math.max(0,e-this.currentTime_()),i=!this.hasPlayed_()&&t>=1,n=t>=this.goalBufferLength_(),r=this.playlist_.segments;if(!r.length||i||n)return null;this.syncPoint_=this.syncPoint_||this.syncController_.getSyncPoint(this.playlist_,this.duration_(),this.currentTimeline_,this.currentTime_());var a={partIndex:null,mediaIndex:null,startOfSegment:null,playlist:this.playlist_,isSyncRequest:Boolean(!this.syncPoint_)};if(a.isSyncRequest)a.mediaIndex=function(e,t,i){t=t||[];for(var n=[],r=0,a=0;ai))return a}return 0===n.length?0:n[n.length-1]}(this.currentTimeline_,r,e);else if(null!==this.mediaIndex){var s=r[this.mediaIndex],o="number"==typeof this.partIndex?this.partIndex:-1;a.startOfSegment=s.end?s.end:e,s.parts&&s.parts[o+1]?(a.mediaIndex=this.mediaIndex,a.partIndex=o+1):a.mediaIndex=this.mediaIndex+1}else{var u=Sa.getMediaInfoForTime({experimentalExactManifestTimings:this.experimentalExactManifestTimings,playlist:this.playlist_,currentTime:this.fetchAtBuffer_?e:this.currentTime_(),startingPartIndex:this.syncPoint_.partIndex,startingSegmentIndex:this.syncPoint_.segmentIndex,startTime:this.syncPoint_.time}),l=u.segmentIndex,h=u.startTime,d=u.partIndex;a.getMediaInfoForTime=this.fetchAtBuffer_?"bufferedEnd":"currentTime",a.mediaIndex=l,a.startOfSegment=h,a.partIndex=d}var c=r[a.mediaIndex],f=c&&"number"==typeof a.partIndex&&c.parts&&c.parts[a.partIndex];if(!c||"number"==typeof a.partIndex&&!f)return null;"number"!=typeof a.partIndex&&c.parts&&(a.partIndex=0);var p=this.mediaSource_&&"ended"===this.mediaSource_.readyState;return a.mediaIndex>=r.length-1&&p&&!this.seeking_()?null:this.generateSegmentInfo_(a)},i.generateSegmentInfo_=function(e){var t=e.playlist,i=e.mediaIndex,n=e.startOfSegment,r=e.isSyncRequest,a=e.partIndex,s=e.forceTimestampOffset,o=e.getMediaInfoForTime,u=t.segments[i],l="number"==typeof a&&u.parts[a],h={requestId:"segment-loader-"+Math.random(),uri:l&&l.resolvedUri||u.resolvedUri,mediaIndex:i,partIndex:l?a:null,isSyncRequest:r,startOfSegment:n,playlist:t,bytes:null,encryptedBytes:null,timestampOffset:null,timeline:u.timeline,duration:l&&l.duration||u.duration,segment:u,part:l,byteLength:0,transmuxer:this.transmuxer_,getMediaInfoForTime:o},d=void 0!==s?s:this.isPendingTimestampOffset_;h.timestampOffset=this.timestampOffsetForSegment_({segmentTimeline:u.timeline,currentTimeline:this.currentTimeline_,startOfSegment:n,buffered:this.buffered_(),overrideCheck:d});var c=na(this.sourceUpdater_.audioBuffered());return"number"==typeof c&&(h.audioAppendStart=c-this.sourceUpdater_.audioTimestampOffset()),this.sourceUpdater_.videoBuffered().length&&(h.gopsToAlignWith=function(e,t,i){if(null==t||!e.length)return[];var n,r=Math.ceil((t-i+3)*E.ONE_SECOND_IN_TS);for(n=0;nr);n++);return e.slice(n)}(this.gopBuffer_,this.currentTime_()-this.sourceUpdater_.videoTimestampOffset(),this.timeMapping_)),h},i.timestampOffsetForSegment_=function(e){return i=(t=e).segmentTimeline,n=t.currentTimeline,r=t.startOfSegment,a=t.buffered,t.overrideCheck||i!==n?i "+s+" for "+e),function(e,t,i){if(!e[i]){t.trigger({type:"usage",name:"vhs-608"}),t.trigger({type:"usage",name:"hls-608"});var n=i;/^cc708_/.test(i)&&(n="SERVICE"+i.split("_")[1]);var r=t.textTracks().getTrackById(n);if(r)e[i]=r;else{var a=i,s=i,o=!1,u=(t.options_.vhs&&t.options_.vhs.captionServices||{})[n];u&&(a=u.label,s=u.language,o=u.default),e[i]=t.addRemoteTextTrack({kind:"captions",id:n,default:o,label:a,language:s},!1).track}}}(u,i.vhs_.tech_,e),Gs(a,s,u[e]),function(e){var t=e.inbandTextTracks,i=e.captionArray,n=e.timestampOffset;if(i){var r=C.default.WebKitDataCue||C.default.VTTCue;i.forEach((function(e){var i=e.stream;t[i].addCue(new r(e.startTime+n,e.endTime+n,e.text))}))}}({captionArray:o,inbandTextTracks:u,timestampOffset:n})})),this.transmuxer_&&this.transmuxer_.postMessage({action:"clearParsedMp4Captions"})}else this.metadataQueue_.caption.push(this.handleCaptions_.bind(this,e,t));else this.logger_("SegmentLoader received no captions from a caption event")},i.handleId3_=function(e,t,i){if(this.earlyAbortWhenNeeded_(e.stats),!this.checkForAbort_(e.requestId))if(this.pendingSegment_.hasAppendedData_){var n=null===this.sourceUpdater_.videoTimestampOffset()?this.sourceUpdater_.audioTimestampOffset():this.sourceUpdater_.videoTimestampOffset();!function(e,t,i){e.metadataTrack_||(e.metadataTrack_=i.addRemoteTextTrack({kind:"metadata",label:"Timed Metadata"},!1).track,e.metadataTrack_.inBandMetadataTrackDispatchType=t)}(this.inbandTextTracks_,i,this.vhs_.tech_),zs({inbandTextTracks:this.inbandTextTracks_,metadataArray:t,timestampOffset:n,videoDuration:this.duration_()})}else this.metadataQueue_.id3.push(this.handleId3_.bind(this,e,t,i))},i.processMetadataQueue_=function(){this.metadataQueue_.id3.forEach((function(e){return e()})),this.metadataQueue_.caption.forEach((function(e){return e()})),this.metadataQueue_.id3=[],this.metadataQueue_.caption=[]},i.processCallQueue_=function(){var e=this.callQueue_;this.callQueue_=[],e.forEach((function(e){return e()}))},i.processLoadQueue_=function(){var e=this.loadQueue_;this.loadQueue_=[],e.forEach((function(e){return e()}))},i.hasEnoughInfoToLoad_=function(){if("audio"!==this.loaderType_)return!0;var e=this.pendingSegment_;return!!e&&(!this.getCurrentMediaInfo_()||!Ks({timelineChangeController:this.timelineChangeController_,currentTimeline:this.currentTimeline_,segmentTimeline:e.timeline,loaderType:this.loaderType_,audioDisabled:this.audioDisabled_}))},i.getCurrentMediaInfo_=function(e){return void 0===e&&(e=this.pendingSegment_),e&&e.trackInfo||this.currentMediaInfo_},i.getMediaInfo_=function(e){return void 0===e&&(e=this.pendingSegment_),this.getCurrentMediaInfo_(e)||this.startingMediaInfo_},i.hasEnoughInfoToAppend_=function(){if(!this.sourceUpdater_.ready())return!1;if(this.waitingOnRemove_||this.quotaExceededErrorRetryTimeout_)return!1;var e=this.pendingSegment_,t=this.getCurrentMediaInfo_();if(!e||!t)return!1;var i=t.hasAudio,n=t.hasVideo,r=t.isMuxed;return!(n&&!e.videoTimingInfo)&&(!(i&&!this.audioDisabled_&&!r&&!e.audioTimingInfo)&&!Ks({timelineChangeController:this.timelineChangeController_,currentTimeline:this.currentTimeline_,segmentTimeline:e.timeline,loaderType:this.loaderType_,audioDisabled:this.audioDisabled_}))},i.handleData_=function(e,t){if(this.earlyAbortWhenNeeded_(e.stats),!this.checkForAbort_(e.requestId))if(!this.callQueue_.length&&this.hasEnoughInfoToAppend_()){var i=this.pendingSegment_;if(this.setTimeMapping_(i.timeline),this.updateMediaSecondsLoaded_(i.segment),"closed"!==this.mediaSource_.readyState){if(e.map&&(e.map=this.initSegmentForMap(e.map,!0),i.segment.map=e.map),e.key&&this.segmentKey(e.key,!0),i.isFmp4=e.isFmp4,i.timingInfo=i.timingInfo||{},i.isFmp4)this.trigger("fmp4"),i.timingInfo.start=i[qs(t.type)].start;else{var n,r=this.getCurrentMediaInfo_(),a="main"===this.loaderType_&&r&&r.hasVideo;a&&(n=i.videoTimingInfo.start),i.timingInfo.start=this.trueSegmentStart_({currentStart:i.timingInfo.start,playlist:i.playlist,mediaIndex:i.mediaIndex,currentVideoTimestampOffset:this.sourceUpdater_.videoTimestampOffset(),useVideoTimingInfo:a,firstVideoFrameTimeForData:n,videoTimingInfo:i.videoTimingInfo,audioTimingInfo:i.audioTimingInfo})}if(this.updateAppendInitSegmentStatus(i,t.type),this.updateSourceBufferTimestampOffset_(i),i.isSyncRequest){this.updateTimingInfoEnd_(i),this.syncController_.saveSegmentTimingInfo({segmentInfo:i,shouldSaveTimelineMapping:"main"===this.loaderType_});var s=this.chooseNextRequest_();if(s.mediaIndex!==i.mediaIndex||s.partIndex!==i.partIndex)return void this.logger_("sync segment was incorrect, not appending");this.logger_("sync segment was correct, appending")}i.hasAppendedData_=!0,this.processMetadataQueue_(),this.appendData_(i,t)}}else this.callQueue_.push(this.handleData_.bind(this,e,t))},i.updateAppendInitSegmentStatus=function(e,t){"main"!==this.loaderType_||"number"!=typeof e.timestampOffset||e.changedTimestampOffset||(this.appendInitSegment_={audio:!0,video:!0}),this.playlistOfLastInitSegment_[t]!==e.playlist&&(this.appendInitSegment_[t]=!0)},i.getInitSegmentAndUpdateState_=function(e){var t=e.type,i=e.initSegment,n=e.map,r=e.playlist;if(n){var a=Wa(n);if(this.activeInitSegmentId_===a)return null;i=this.initSegmentForMap(n,!0).bytes,this.activeInitSegmentId_=a}return i&&this.appendInitSegment_[t]?(this.playlistOfLastInitSegment_[t]=r,this.appendInitSegment_[t]=!1,this.activeInitSegmentId_=null,i):null},i.handleQuotaExceededError_=function(e,t){var i=this,n=e.segmentInfo,r=e.type,a=e.bytes,s=this.sourceUpdater_.audioBuffered(),o=this.sourceUpdater_.videoBuffered();s.length>1&&this.logger_("On QUOTA_EXCEEDED_ERR, found gaps in the audio buffer: "+ia(s).join(", ")),o.length>1&&this.logger_("On QUOTA_EXCEEDED_ERR, found gaps in the video buffer: "+ia(o).join(", "));var u=s.length?s.start(0):0,l=s.length?s.end(s.length-1):0,h=o.length?o.start(0):0,d=o.length?o.end(o.length-1):0;if(l-u<=1&&d-h<=1)return this.logger_("On QUOTA_EXCEEDED_ERR, single segment too large to append to buffer, triggering an error. Appended byte length: "+a.byteLength+", audio buffer: "+ia(s).join(", ")+", video buffer: "+ia(o).join(", ")+", "),this.error({message:"Quota exceeded error with append of a single segment of content",excludeUntil:1/0}),void this.trigger("error");this.waitingOnRemove_=!0,this.callQueue_.push(this.appendToSourceBuffer_.bind(this,{segmentInfo:n,type:r,bytes:a}));var c=this.currentTime_()-1;this.logger_("On QUOTA_EXCEEDED_ERR, removing audio/video from 0 to "+c),this.remove(0,c,(function(){i.logger_("On QUOTA_EXCEEDED_ERR, retrying append in 1s"),i.waitingOnRemove_=!1,i.quotaExceededErrorRetryTimeout_=C.default.setTimeout((function(){i.logger_("On QUOTA_EXCEEDED_ERR, re-processing call queue"),i.quotaExceededErrorRetryTimeout_=null,i.processCallQueue_()}),1e3)}),!0)},i.handleAppendError_=function(e,t){var i=e.segmentInfo,n=e.type,r=e.bytes;t&&(22!==t.code?(this.logger_("Received non QUOTA_EXCEEDED_ERR on append",t),this.error(n+" append of "+r.length+"b failed for segment #"+i.mediaIndex+" in playlist "+i.playlist.id),this.trigger("appenderror")):this.handleQuotaExceededError_({segmentInfo:i,type:n,bytes:r}))},i.appendToSourceBuffer_=function(e){var t,i,n,r=e.segmentInfo,a=e.type,s=e.initSegment,o=e.data,u=e.bytes;if(!u){var l=[o],h=o.byteLength;s&&(l.unshift(s),h+=s.byteLength),n=0,(t={bytes:h,segments:l}).bytes&&(i=new Uint8Array(t.bytes),t.segments.forEach((function(e){i.set(e,n),n+=e.byteLength}))),u=i}this.sourceUpdater_.appendBuffer({segmentInfo:r,type:a,bytes:u},this.handleAppendError_.bind(this,{segmentInfo:r,type:a,bytes:u}))},i.handleSegmentTimingInfo_=function(e,t,i){if(this.pendingSegment_&&t===this.pendingSegment_.requestId){var n=this.pendingSegment_.segment,r=e+"TimingInfo";n[r]||(n[r]={}),n[r].transmuxerPrependedSeconds=i.prependedContentDuration||0,n[r].transmuxedPresentationStart=i.start.presentation,n[r].transmuxedDecodeStart=i.start.decode,n[r].transmuxedPresentationEnd=i.end.presentation,n[r].transmuxedDecodeEnd=i.end.decode,n[r].baseMediaDecodeTime=i.baseMediaDecodeTime}},i.appendData_=function(e,t){var i=t.type,n=t.data;if(n&&n.byteLength&&("audio"!==i||!this.audioDisabled_)){var r=this.getInitSegmentAndUpdateState_({type:i,initSegment:t.initSegment,playlist:e.playlist,map:e.isFmp4?e.segment.map:null});this.appendToSourceBuffer_({segmentInfo:e,type:i,initSegment:r,data:n})}},i.loadSegment_=function(e){var t=this;this.state="WAITING",this.pendingSegment_=e,this.trimBackBuffer_(e),"number"==typeof e.timestampOffset&&this.transmuxer_&&this.transmuxer_.postMessage({action:"clearAllMp4Captions"}),this.hasEnoughInfoToLoad_()?this.updateTransmuxerAndRequestSegment_(e):this.loadQueue_.push((function(){var i=P.default({},e,{forceTimestampOffset:!0});P.default(e,t.generateSegmentInfo_(i)),t.isPendingTimestampOffset_=!1,t.updateTransmuxerAndRequestSegment_(e)}))},i.updateTransmuxerAndRequestSegment_=function(e){var t=this;this.shouldUpdateTransmuxerTimestampOffset_(e.timestampOffset)&&(this.gopBuffer_.length=0,e.gopsToAlignWith=[],this.timeMapping_=0,this.transmuxer_.postMessage({action:"reset"}),this.transmuxer_.postMessage({action:"setTimestampOffset",timestampOffset:e.timestampOffset}));var i=this.createSimplifiedSegmentObj_(e),n=this.isEndOfStream_(e.mediaIndex,e.playlist,e.partIndex),r=null!==this.mediaIndex,a=e.timeline!==this.currentTimeline_&&e.timeline>0,s=n||r&&a;this.logger_("Requesting "+Ys(e)),i.map&&!i.map.bytes&&(this.logger_("going to request init segment."),this.appendInitSegment_={video:!0,audio:!0}),e.abortRequests=Ls({xhr:this.vhs_.xhr,xhrOptions:this.xhrOptions_,decryptionWorker:this.decrypter_,segment:i,abortFn:this.handleAbort_.bind(this,e),progressFn:this.handleProgress_.bind(this),trackInfoFn:this.handleTrackInfo_.bind(this),timingInfoFn:this.handleTimingInfo_.bind(this),videoSegmentTimingInfoFn:this.handleSegmentTimingInfo_.bind(this,"video",e.requestId),audioSegmentTimingInfoFn:this.handleSegmentTimingInfo_.bind(this,"audio",e.requestId),captionsFn:this.handleCaptions_.bind(this),isEndOfTimeline:s,endedTimelineFn:function(){t.logger_("received endedtimeline callback")},id3Fn:this.handleId3_.bind(this),dataFn:this.handleData_.bind(this),doneFn:this.segmentRequestFinished_.bind(this),onTransmuxerLog:function(i){var n=i.message,r=i.level,a=i.stream;t.logger_(Ys(e)+" logged from transmuxer stream "+a+" as a "+r+": "+n)}})},i.trimBackBuffer_=function(e){var t=function(e,t,i){var n=t-ns.BACK_BUFFER_LENGTH;e.length&&(n=Math.max(n,e.start(0)));var r=t-i;return Math.min(r,n)}(this.seekable_(),this.currentTime_(),this.playlist_.targetDuration||10);t>0&&this.remove(0,t)},i.createSimplifiedSegmentObj_=function(e){var t=e.segment,i=e.part,n={resolvedUri:i?i.resolvedUri:t.resolvedUri,byterange:i?i.byterange:t.byterange,requestId:e.requestId,transmuxer:e.transmuxer,audioAppendStart:e.audioAppendStart,gopsToAlignWith:e.gopsToAlignWith,part:e.part},r=e.playlist.segments[e.mediaIndex-1];if(r&&r.timeline===t.timeline&&(r.videoTimingInfo?n.baseStartTime=r.videoTimingInfo.transmuxedDecodeEnd:r.audioTimingInfo&&(n.baseStartTime=r.audioTimingInfo.transmuxedDecodeEnd)),t.key){var a=t.key.iv||new Uint32Array([0,0,0,e.mediaIndex+e.playlist.mediaSequence]);n.key=this.segmentKey(t.key),n.key.iv=a}return t.map&&(n.map=this.initSegmentForMap(t.map)),n},i.saveTransferStats_=function(e){this.mediaRequests+=1,e&&(this.mediaBytesTransferred+=e.bytesReceived,this.mediaTransferDuration+=e.roundTripTime)},i.saveBandwidthRelatedStats_=function(e,t){this.pendingSegment_.byteLength=t.bytesReceived,e<1/60?this.logger_("Ignoring segment's bandwidth because its duration of "+e+" is less than the min to record "+1/60):(this.bandwidth=t.bandwidth,this.roundTrip=t.roundTripTime)},i.handleTimeout_=function(){this.mediaRequestsTimedout+=1,this.bandwidth=1,this.roundTrip=NaN,this.trigger("bandwidthupdate")},i.segmentRequestFinished_=function(e,t,i){if(this.callQueue_.length)this.callQueue_.push(this.segmentRequestFinished_.bind(this,e,t,i));else if(this.saveTransferStats_(t.stats),this.pendingSegment_&&t.requestId===this.pendingSegment_.requestId){if(e){if(this.pendingSegment_=null,this.state="READY",e.code===ys)return;return this.pause(),e.code===vs?void this.handleTimeout_():(this.mediaRequestsErrored+=1,this.error(e),void this.trigger("error"))}var n=this.pendingSegment_;this.saveBandwidthRelatedStats_(n.duration,t.stats),n.endOfAllRequests=t.endOfAllRequests,i.gopInfo&&(this.gopBuffer_=function(e,t,i){if(!t.length)return e;if(i)return t.slice();for(var n=t[0].pts,r=0;r=n);r++);return e.slice(0,r).concat(t)}(this.gopBuffer_,i.gopInfo,this.safeAppend_)),this.state="APPENDING",this.trigger("appending"),this.waitForAppendsToComplete_(n)}},i.setTimeMapping_=function(e){var t=this.syncController_.mappingForTimeline(e);null!==t&&(this.timeMapping_=t)},i.updateMediaSecondsLoaded_=function(e){"number"==typeof e.start&&"number"==typeof e.end?this.mediaSecondsLoaded+=e.end-e.start:this.mediaSecondsLoaded+=e.duration},i.shouldUpdateTransmuxerTimestampOffset_=function(e){return null!==e&&("main"===this.loaderType_&&e!==this.sourceUpdater_.videoTimestampOffset()||!this.audioDisabled_&&e!==this.sourceUpdater_.audioTimestampOffset())},i.trueSegmentStart_=function(e){var t=e.currentStart,i=e.playlist,n=e.mediaIndex,r=e.firstVideoFrameTimeForData,a=e.currentVideoTimestampOffset,s=e.useVideoTimingInfo,o=e.videoTimingInfo,u=e.audioTimingInfo;if(void 0!==t)return t;if(!s)return u.start;var l=i.segments[n-1];return 0!==n&&l&&void 0!==l.start&&l.end===r+a?o.start:r},i.waitForAppendsToComplete_=function(e){var t=this.getCurrentMediaInfo_(e);if(!t)return this.error({message:"No starting media returned, likely due to an unsupported media format.",blacklistDuration:1/0}),void this.trigger("error");var i=t.hasAudio,n=t.hasVideo,r=t.isMuxed,a="main"===this.loaderType_&&n,s=!this.audioDisabled_&&i&&!r;if(e.waitingOnAppends=0,!e.hasAppendedData_)return e.timingInfo||"number"!=typeof e.timestampOffset||(this.isPendingTimestampOffset_=!0),e.timingInfo={start:0},e.waitingOnAppends++,this.isPendingTimestampOffset_||(this.updateSourceBufferTimestampOffset_(e),this.processMetadataQueue_()),void this.checkAppendsDone_(e);a&&e.waitingOnAppends++,s&&e.waitingOnAppends++,a&&this.sourceUpdater_.videoQueueCallback(this.checkAppendsDone_.bind(this,e)),s&&this.sourceUpdater_.audioQueueCallback(this.checkAppendsDone_.bind(this,e))},i.checkAppendsDone_=function(e){this.checkForAbort_(e.requestId)||(e.waitingOnAppends--,0===e.waitingOnAppends&&this.handleAppendsDone_())},i.checkForIllegalMediaSwitch=function(e){var t=function(e,t,i){return"main"===e&&t&&i?i.hasAudio||i.hasVideo?t.hasVideo&&!i.hasVideo?"Only audio found in segment when we expected video. We can't switch to audio only from a stream that had video. To get rid of this message, please add codec information to the manifest.":!t.hasVideo&&i.hasVideo?"Video found in segment when we expected only audio. We can't switch to a stream with video from an audio only stream. To get rid of this message, please add codec information to the manifest.":null:"Neither audio nor video found in segment.":null}(this.loaderType_,this.getCurrentMediaInfo_(),e);return!!t&&(this.error({message:t,blacklistDuration:1/0}),this.trigger("error"),!0)},i.updateSourceBufferTimestampOffset_=function(e){if(null!==e.timestampOffset&&"number"==typeof e.timingInfo.start&&!e.changedTimestampOffset&&"main"===this.loaderType_){var t=!1;e.timestampOffset-=e.timingInfo.start,e.changedTimestampOffset=!0,e.timestampOffset!==this.sourceUpdater_.videoTimestampOffset()&&(this.sourceUpdater_.videoTimestampOffset(e.timestampOffset),t=!0),e.timestampOffset!==this.sourceUpdater_.audioTimestampOffset()&&(this.sourceUpdater_.audioTimestampOffset(e.timestampOffset),t=!0),t&&this.trigger("timestampoffset")}},i.updateTimingInfoEnd_=function(e){e.timingInfo=e.timingInfo||{};var t=this.getMediaInfo_(),i="main"===this.loaderType_&&t&&t.hasVideo&&e.videoTimingInfo?e.videoTimingInfo:e.audioTimingInfo;i&&(e.timingInfo.end="number"==typeof i.end?i.end:i.start+e.duration)},i.handleAppendsDone_=function(){if(this.pendingSegment_&&this.trigger("appendsdone"),!this.pendingSegment_)return this.state="READY",void(this.paused()||this.monitorBuffer_());var e=this.pendingSegment_;this.updateTimingInfoEnd_(e),this.shouldSaveSegmentTimingInfo_&&this.syncController_.saveSegmentTimingInfo({segmentInfo:e,shouldSaveTimelineMapping:"main"===this.loaderType_});var t=Qs(e,this.sourceType_);if(t&&("warn"===t.severity?Yr.log.warn(t.message):this.logger_(t.message)),this.recordThroughput_(e),this.pendingSegment_=null,this.state="READY",!e.isSyncRequest||(this.trigger("syncinfoupdate"),e.hasAppendedData_)){this.logger_("Appended "+Ys(e)),this.addSegmentMetadataCue_(e),this.fetchAtBuffer_=!0,this.currentTimeline_!==e.timeline&&(this.timelineChangeController_.lastTimelineChange({type:this.loaderType_,from:this.currentTimeline_,to:e.timeline}),"main"!==this.loaderType_||this.audioDisabled_||this.timelineChangeController_.lastTimelineChange({type:"audio",from:this.currentTimeline_,to:e.timeline})),this.currentTimeline_=e.timeline,this.trigger("syncinfoupdate");var i=e.segment;if(i.end&&this.currentTime_()-i.end>3*e.playlist.targetDuration)this.resetEverything();else null!==this.mediaIndex&&this.trigger("bandwidthupdate"),this.trigger("progress"),this.mediaIndex=e.mediaIndex,this.partIndex=e.partIndex,this.isEndOfStream_(e.mediaIndex,e.playlist,e.partIndex)&&this.endOfStream(),this.trigger("appended"),e.hasAppendedData_&&this.mediaAppends++,this.paused()||this.monitorBuffer_()}else this.logger_("Throwing away un-appended sync request "+Ys(e))},i.recordThroughput_=function(e){if(e.duration<1/60)this.logger_("Ignoring segment's throughput because its duration of "+e.duration+" is less than the min to record "+1/60);else{var t=this.throughput.rate,i=Date.now()-e.endOfAllRequests+1,n=Math.floor(e.byteLength/i*8*1e3);this.throughput.rate+=(n-t)/++this.throughput.count}},i.addSegmentMetadataCue_=function(e){if(this.segmentMetadataTrack_){var t=e.segment,i=t.start,n=t.end;if(Ws(i)&&Ws(n)){Gs(i,n,this.segmentMetadataTrack_);var r=C.default.WebKitDataCue||C.default.VTTCue,a={custom:t.custom,dateTimeObject:t.dateTimeObject,dateTimeString:t.dateTimeString,bandwidth:e.playlist.attributes.BANDWIDTH,resolution:e.playlist.attributes.RESOLUTION,codecs:e.playlist.attributes.CODECS,byteLength:e.byteLength,uri:e.uri,timeline:e.timeline,playlist:e.playlist.id,start:i,end:n},s=new r(i,n,JSON.stringify(a));s.value=a,this.segmentMetadataTrack_.addCue(s)}}},t}(Yr.EventTarget);function Js(){}var Zs,eo=function(e){return"string"!=typeof e?e:e.replace(/./,(function(e){return e.toUpperCase()}))},to=["video","audio"],io=function(e,t){var i=t[e+"Buffer"];return i&&i.updating||t.queuePending[e]},no=function e(t,i){if(0!==i.queue.length){var n=0,r=i.queue[n];if("mediaSource"!==r.type){if("mediaSource"!==t&&i.ready()&&"closed"!==i.mediaSource.readyState&&!io(t,i)){if(r.type!==t){if(null===(n=function(e,t){for(var i=0;i=e.playlist.segments.length){e=null;break}e=this.generateSegmentInfo_({playlist:e.playlist,mediaIndex:e.mediaIndex+1,startOfSegment:e.startOfSegment+e.duration,isSyncRequest:e.isSyncRequest})}return e},i.stopForError=function(e){this.error(e),this.state="READY",this.pause(),this.trigger("error")},i.segmentRequestFinished_=function(e,t,i){var n=this;if(this.subtitlesTrack_){if(this.saveTransferStats_(t.stats),!this.pendingSegment_)return this.state="READY",void(this.mediaRequestsAborted+=1);if(e)return e.code===vs&&this.handleTimeout_(),e.code===ys?this.mediaRequestsAborted+=1:this.mediaRequestsErrored+=1,void this.stopForError(e);var r=this.pendingSegment_;this.saveBandwidthRelatedStats_(r.duration,t.stats),this.state="APPENDING",this.trigger("appending");var a=r.segment;if(a.map&&(a.map.bytes=t.map.bytes),r.bytes=t.bytes,"function"!=typeof C.default.WebVTT&&this.subtitlesTrack_&&this.subtitlesTrack_.tech_){var s,o=function(){n.subtitlesTrack_.tech_.off("vttjsloaded",s),n.stopForError({message:"Error loading vtt.js"})};return s=function(){n.subtitlesTrack_.tech_.off("vttjserror",o),n.segmentRequestFinished_(e,t,i)},this.state="WAITING_ON_VTTJS",this.subtitlesTrack_.tech_.one("vttjsloaded",s),void this.subtitlesTrack_.tech_.one("vttjserror",o)}a.requested=!0;try{this.parseVTTCues_(r)}catch(e){return void this.stopForError({message:e.message})}if(this.updateTimeMapping_(r,this.syncController_.timelines[r.timeline],this.playlist_),r.cues.length?r.timingInfo={start:r.cues[0].startTime,end:r.cues[r.cues.length-1].endTime}:r.timingInfo={start:r.startOfSegment,end:r.startOfSegment+r.duration},r.isSyncRequest)return this.trigger("syncinfoupdate"),this.pendingSegment_=null,void(this.state="READY");r.byteLength=r.bytes.byteLength,this.mediaSecondsLoaded+=a.duration,r.cues.forEach((function(e){n.subtitlesTrack_.addCue(n.featuresNativeTextTracks_?new C.default.VTTCue(e.startTime,e.endTime,e.text):e)})),function(e){var t=e.cues;if(t)for(var i=0;i1&&n.push(t[a]);n.length&&n.forEach((function(t){return e.removeCue(t)}))}}(this.subtitlesTrack_),this.handleAppendsDone_()}else this.state="READY"},i.handleData_=function(){},i.updateTimingInfoEnd_=function(){},i.parseVTTCues_=function(e){var t,i=!1;"function"==typeof C.default.TextDecoder?t=new C.default.TextDecoder("utf8"):(t=C.default.WebVTT.StringDecoder(),i=!0);var n=new C.default.WebVTT.Parser(C.default,C.default.vttjs,t);if(e.cues=[],e.timestampmap={MPEGTS:0,LOCAL:0},n.oncue=e.cues.push.bind(e.cues),n.ontimestampmap=function(t){e.timestampmap=t},n.onparsingerror=function(e){Yr.log.warn("Error encountered when parsing cues: "+e.message)},e.segment.map){var r=e.segment.map.bytes;i&&(r=bo(r)),n.parse(r)}var a=e.bytes;i&&(a=bo(a)),n.parse(a),n.flush()},i.updateTimeMapping_=function(e,t,i){var n=e.segment;if(t)if(e.cues.length){var r=e.timestampmap,a=r.MPEGTS/E.ONE_SECOND_IN_TS-r.LOCAL+t.mapping;if(e.cues.forEach((function(e){e.startTime+=a,e.endTime+=a})),!i.syncInfo){var s=e.cues[0].startTime,o=e.cues[e.cues.length-1].startTime;i.syncInfo={mediaSequence:i.mediaSequence+e.mediaIndex,time:Math.min(s,o-n.duration)}}}else n.empty=!0},t}($s),Eo=function(e,t){for(var i=e.cues,n=0;n=r.adStartTime&&t<=r.adEndTime)return r}return null},wo=[{name:"VOD",run:function(e,t,i,n,r){if(i!==1/0){return{time:0,segmentIndex:0,partIndex:null}}return null}},{name:"ProgramDateTime",run:function(e,t,i,n,r){if(!Object.keys(e.timelineToDatetimeMappings).length)return null;var a=null,s=null,o=aa(t);r=r||0;for(var u=0;u=c)&&(s=c,a={time:d,segmentIndex:l.segmentIndex,partIndex:l.partIndex})}}return a}},{name:"Discontinuity",run:function(e,t,i,n,r){var a=null;if(r=r||0,t.discontinuityStarts&&t.discontinuityStarts.length)for(var s=null,o=0;o=d)&&(s=d,a={time:h.time,segmentIndex:u,partIndex:null})}}return a}},{name:"Playlist",run:function(e,t,i,n,r){return t.syncInfo?{time:t.syncInfo.time,segmentIndex:t.syncInfo.mediaSequence-t.mediaSequence,partIndex:null}:null}}],Ao=function(e){function t(t){var i;return(i=e.call(this)||this).timelines=[],i.discontinuities=[],i.timelineToDatetimeMappings={},i.logger_=$r("SyncController"),i}L.default(t,e);var i=t.prototype;return i.getSyncPoint=function(e,t,i,n){var r=this.runStrategies_(e,t,i,n);return r.length?this.selectSyncPoint_(r,{key:"time",value:n}):null},i.getExpiredTime=function(e,t){if(!e||!e.segments)return null;var i=this.runStrategies_(e,t,e.discontinuitySequence,0);if(!i.length)return null;var n=this.selectSyncPoint_(i,{key:"segmentIndex",value:0});return n.segmentIndex>0&&(n.time*=-1),Math.abs(n.time+da({defaultDuration:e.targetDuration,durationList:e.segments,startIndex:n.segmentIndex,endIndex:0}))},i.runStrategies_=function(e,t,i,n){for(var r=[],a=0;a=0;i--){var n=e.segments[i];if(n&&void 0!==n.start){t.syncInfo={mediaSequence:e.mediaSequence+i,time:n.start},this.logger_("playlist refresh sync: [time:"+t.syncInfo.time+", mediaSequence: "+t.syncInfo.mediaSequence+"]"),this.trigger("syncinfoupdate");break}}},i.setDateTimeMappingForStart=function(e){if(this.timelineToDatetimeMappings={},e.segments&&e.segments.length&&e.segments[0].dateTimeObject){var t=e.segments[0],i=t.dateTimeObject.getTime()/1e3;this.timelineToDatetimeMappings[t.timeline]=-i}},i.saveSegmentTimingInfo=function(e){var t=e.segmentInfo,i=e.shouldSaveTimelineMapping,n=this.calculateSegmentTimeMapping_(t,t.timingInfo,i),r=t.segment;n&&(this.saveDiscontinuitySyncInfo_(t),t.playlist.syncInfo||(t.playlist.syncInfo={mediaSequence:t.playlist.mediaSequence+t.mediaIndex,time:r.start}));var a=r.dateTimeObject;r.discontinuity&&i&&a&&(this.timelineToDatetimeMappings[r.timeline]=-a.getTime()/1e3)},i.timestampOffsetForTimeline=function(e){return void 0===this.timelines[e]?null:this.timelines[e].time},i.mappingForTimeline=function(e){return void 0===this.timelines[e]?null:this.timelines[e].mapping},i.calculateSegmentTimeMapping_=function(e,t,i){var n,r,a=e.segment,s=e.part,o=this.timelines[e.timeline];if("number"==typeof e.timestampOffset)o={time:e.startOfSegment,mapping:e.startOfSegment-t.start},i&&(this.timelines[e.timeline]=o,this.trigger("timestampoffset"),this.logger_("time mapping for timeline "+e.timeline+": [time: "+o.time+"] [mapping: "+o.mapping+"]")),n=e.startOfSegment,r=t.end+o.mapping;else{if(!o)return!1;n=t.start+o.mapping,r=t.end+o.mapping}return s&&(s.start=n,s.end=r),(!a.start||no){var u=void 0;u=s<0?i.start-da({defaultDuration:t.targetDuration,durationList:t.segments,startIndex:e.mediaIndex,endIndex:r}):i.end+da({defaultDuration:t.targetDuration,durationList:t.segments,startIndex:e.mediaIndex+1,endIndex:r}),this.discontinuities[a]={time:u,accuracy:o}}}},i.dispose=function(){this.trigger("dispose"),this.off()},t}(Yr.EventTarget),Co=function(e){function t(){var t;return(t=e.call(this)||this).pendingTimelineChanges_={},t.lastTimelineChanges_={},t}L.default(t,e);var i=t.prototype;return i.clearPendingTimelineChange=function(e){this.pendingTimelineChanges_[e]=null,this.trigger("pendingtimelinechange")},i.pendingTimelineChange=function(e){var t=e.type,i=e.from,n=e.to;return"number"==typeof i&&"number"==typeof n&&(this.pendingTimelineChanges_[t]={type:t,from:i,to:n},this.trigger("pendingtimelinechange")),this.pendingTimelineChanges_[t]},i.lastTimelineChange=function(e){var t=e.type,i=e.from,n=e.to;return"number"==typeof i&&"number"==typeof n&&(this.lastTimelineChanges_[t]={type:t,from:i,to:n},delete this.pendingTimelineChanges_[t],this.trigger("timelinechange")),this.lastTimelineChanges_[t]},i.dispose=function(){this.trigger("dispose"),this.pendingTimelineChanges_={},this.lastTimelineChanges_={},this.off()},t}(Yr.EventTarget),ko=as(ss(os((function(){function e(e,t,i){return e(i={path:t,exports:{},require:function(e,t){return function(){throw new Error("Dynamic requires are not currently supported by @rollup/plugin-commonjs")}(null==t&&i.path)}},i.exports),i.exports}var t=e((function(e){function t(e,t){for(var i=0;i-1},t.trigger=function(e){var t=this.listeners[e];if(t)if(2===arguments.length)for(var i=t.length,n=0;n>7))^e]=e;for(t=i=0;!d[t];t^=n||1,i=p[i]||1)for(a=(a=i^i<<1^i<<2^i<<3^i<<4)>>8^255&a^99,d[t]=a,c[a]=t,o=16843009*f[r=f[n=f[t]]]^65537*r^257*n^16843008*t,s=257*f[a]^16843008*a,e=0;e<4;e++)l[e][t]=s=s<<24^s>>>8,h[e][a]=o=o<<24^o>>>8;for(e=0;e<5;e++)l[e]=l[e].slice(0),h[e]=h[e].slice(0);return u}()),this._tables=[[a[0][0].slice(),a[0][1].slice(),a[0][2].slice(),a[0][3].slice(),a[0][4].slice()],[a[1][0].slice(),a[1][1].slice(),a[1][2].slice(),a[1][3].slice(),a[1][4].slice()]];var r=this._tables[0][4],s=this._tables[1],o=e.length,u=1;if(4!==o&&6!==o&&8!==o)throw new Error("Invalid aes key size");var l=e.slice(0),h=[];for(this._key=[l,h],t=o;t<4*o+28;t++)n=l[t-1],(t%o==0||8===o&&t%o==4)&&(n=r[n>>>24]<<24^r[n>>16&255]<<16^r[n>>8&255]<<8^r[255&n],t%o==0&&(n=n<<8^n>>>24^u<<24,u=u<<1^283*(u>>7))),l[t]=l[t-o]^n;for(i=0;t;i++,t--)n=l[3&i?t:t-4],h[i]=t<=4||i<4?n:s[0][r[n>>>24]]^s[1][r[n>>16&255]]^s[2][r[n>>8&255]]^s[3][r[255&n]]}return e.prototype.decrypt=function(e,t,i,n,r,a){var s,o,u,l,h=this._key[1],d=e^h[0],c=n^h[1],f=i^h[2],p=t^h[3],m=h.length/4-2,_=4,g=this._tables[1],v=g[0],y=g[1],b=g[2],S=g[3],T=g[4];for(l=0;l>>24]^y[c>>16&255]^b[f>>8&255]^S[255&p]^h[_],o=v[c>>>24]^y[f>>16&255]^b[p>>8&255]^S[255&d]^h[_+1],u=v[f>>>24]^y[p>>16&255]^b[d>>8&255]^S[255&c]^h[_+2],p=v[p>>>24]^y[d>>16&255]^b[c>>8&255]^S[255&f]^h[_+3],_+=4,d=s,c=o,f=u;for(l=0;l<4;l++)r[(3&-l)+a]=T[d>>>24]<<24^T[c>>16&255]<<16^T[f>>8&255]<<8^T[255&p]^h[_++],s=d,d=c,c=f,f=p,p=s},e}(),o=function(e){function t(){var t;return(t=e.call(this,r)||this).jobs=[],t.delay=1,t.timeout_=null,t}n(t,e);var i=t.prototype;return i.processJob_=function(){this.jobs.shift()(),this.jobs.length?this.timeout_=setTimeout(this.processJob_.bind(this),this.delay):this.timeout_=null},i.push=function(e){this.jobs.push(e),this.timeout_||(this.timeout_=setTimeout(this.processJob_.bind(this),this.delay))},t}(r),u=function(e){return e<<24|(65280&e)<<8|(16711680&e)>>8|e>>>24},l=function(){function e(t,i,n,r){var a=e.STEP,s=new Int32Array(t.buffer),l=new Uint8Array(t.byteLength),h=0;for(this.asyncStream_=new o,this.asyncStream_.push(this.decryptChunk_(s.subarray(h,h+a),i,n,l)),h=a;h>2),m=new s(Array.prototype.slice.call(t)),_=new Uint8Array(e.byteLength),g=new Int32Array(_.buffer);for(n=i[0],r=i[1],a=i[2],o=i[3],f=0;f=0&&(t="main-desc"),t},Io=function(e,t){e.abort(),e.pause(),t&&t.activePlaylistLoader&&(t.activePlaylistLoader.pause(),t.activePlaylistLoader=null)},Lo=function(e,t){t.activePlaylistLoader=e,e.load()},xo={AUDIO:function(e,t){return function(){var i=t.segmentLoaders[e],n=t.mediaTypes[e],r=t.blacklistCurrentPlaylist;Io(i,n);var a=n.activeTrack(),s=n.activeGroup(),o=(s.filter((function(e){return e.default}))[0]||s[0]).id,u=n.tracks[o];if(a!==u){for(var l in Yr.log.warn("Problem encountered loading the alternate audio track.Switching back to default."),n.tracks)n.tracks[l].enabled=n.tracks[l]===u;n.onTrackChanged()}else r({message:"Problem encountered loading the default audio track."})}},SUBTITLES:function(e,t){return function(){var i=t.segmentLoaders[e],n=t.mediaTypes[e];Yr.log.warn("Problem encountered loading the subtitle track.Disabling subtitle track."),Io(i,n);var r=n.activeTrack();r&&(r.mode="disabled"),n.onTrackChanged()}}},Ro={AUDIO:function(e,t,i){if(t){var n=i.tech,r=i.requestOptions,a=i.segmentLoaders[e];t.on("loadedmetadata",(function(){var e=t.media();a.playlist(e,r),(!n.paused()||e.endList&&"none"!==n.preload())&&a.load()})),t.on("loadedplaylist",(function(){a.playlist(t.media(),r),n.paused()||a.load()})),t.on("error",xo[e](e,i))}},SUBTITLES:function(e,t,i){var n=i.tech,r=i.requestOptions,a=i.segmentLoaders[e],s=i.mediaTypes[e];t.on("loadedmetadata",(function(){var e=t.media();a.playlist(e,r),a.track(s.activeTrack()),(!n.paused()||e.endList&&"none"!==n.preload())&&a.load()})),t.on("loadedplaylist",(function(){a.playlist(t.media(),r),n.paused()||a.load()})),t.on("error",xo[e](e,i))}},Do={AUDIO:function(e,t){var i=t.vhs,n=t.sourceType,r=t.segmentLoaders[e],a=t.requestOptions,s=t.master.mediaGroups,o=t.mediaTypes[e],u=o.groups,l=o.tracks,h=o.logger_,d=t.masterPlaylistLoader,c=ba(d.master);for(var f in s[e]&&0!==Object.keys(s[e]).length||(s[e]={main:{default:{default:!0}}},c&&(s[e].main.default.playlists=d.master.playlists)),s[e])for(var p in u[f]||(u[f]=[]),s[e][f]){var m=s[e][f][p],_=void 0;if(c?(h("AUDIO group '"+f+"' label '"+p+"' is a master playlist"),m.isMasterPlaylist=!0,_=null):_="vhs-json"===n&&m.playlists?new Ua(m.playlists[0],i,a):m.resolvedUri?new Ua(m.resolvedUri,i,a):m.playlists&&"dash"===n?new is(m.playlists[0],i,a,d):null,m=Yr.mergeOptions({id:p,playlistLoader:_},m),Ro[e](e,m.playlistLoader,t),u[f].push(m),void 0===l[p]){var g=new Yr.AudioTrack({id:p,kind:Po(m),enabled:!1,language:m.language,default:m.default,label:p});l[p]=g}}r.on("error",xo[e](e,t))},SUBTITLES:function(e,t){var i=t.tech,n=t.vhs,r=t.sourceType,a=t.segmentLoaders[e],s=t.requestOptions,o=t.master.mediaGroups,u=t.mediaTypes[e],l=u.groups,h=u.tracks,d=t.masterPlaylistLoader;for(var c in o[e])for(var f in l[c]||(l[c]=[]),o[e][c])if(!o[e][c][f].forced){var p=o[e][c][f],m=void 0;if("hls"===r)m=new Ua(p.resolvedUri,n,s);else if("dash"===r){if(!p.playlists.filter((function(e){return e.excludeUntil!==1/0})).length)return;m=new is(p.playlists[0],n,s,d)}else"vhs-json"===r&&(m=new Ua(p.playlists?p.playlists[0]:p.resolvedUri,n,s));if(p=Yr.mergeOptions({id:f,playlistLoader:m},p),Ro[e](e,p.playlistLoader,t),l[c].push(p),void 0===h[f]){var _=i.addRemoteTextTrack({id:f,kind:"subtitles",default:p.default&&p.autoselect,language:p.language,label:f},!1).track;h[f]=_}}a.on("error",xo[e](e,t))},"CLOSED-CAPTIONS":function(e,t){var i=t.tech,n=t.master.mediaGroups,r=t.mediaTypes[e],a=r.groups,s=r.tracks;for(var o in n[e])for(var u in a[o]||(a[o]=[]),n[e][o]){var l=n[e][o][u];if(/^(?:CC|SERVICE)/.test(l.instreamId)){var h=i.options_.vhs&&i.options_.vhs.captionServices||{},d={label:u,language:l.language,instreamId:l.instreamId,default:l.default&&l.autoselect};if(h[d.instreamId]&&(d=Yr.mergeOptions(d,h[d.instreamId])),void 0===d.default&&delete d.default,a[o].push(Yr.mergeOptions({id:u},l)),void 0===s[u]){var c=i.addRemoteTextTrack({id:d.instreamId,kind:"captions",default:d.default,language:d.language,label:d.label},!1).track;s[u]=c}}}}},Oo=function e(t,i){for(var n=0;n1&&ba(t.master))for(var u=0;u "+a+" from "+t),this.tech_.trigger({type:"usage",name:"vhs-rendition-change-"+t})),this.masterPlaylistLoader_.media(e,i)},i.startABRTimer_=function(){var e=this;this.stopABRTimer_(),this.abrTimer_=C.default.setInterval((function(){return e.checkABR_()}),250)},i.stopABRTimer_=function(){this.tech_.scrubbing&&this.tech_.scrubbing()||(C.default.clearInterval(this.abrTimer_),this.abrTimer_=null)},i.getAudioTrackPlaylists_=function(){var e=this.master(),t=e&&e.playlists||[];if(!e||!e.mediaGroups||!e.mediaGroups.AUDIO)return t;var i,n=e.mediaGroups.AUDIO,r=Object.keys(n);if(Object.keys(this.mediaTypes_.AUDIO.groups).length)i=this.mediaTypes_.AUDIO.activeTrack();else{var a=n.main||r.length&&n[r[0]];for(var s in a)if(a[s].default){i={label:s};break}}if(!i)return t;var o=[];for(var u in n)if(n[u][i.label]){var l=n[u][i.label];if(l.playlists&&l.playlists.length)o.push.apply(o,l.playlists);else if(l.uri)o.push(l);else if(e.playlists.length)for(var h=0;h1&&(this.tech_.trigger({type:"usage",name:"vhs-alternate-audio"}),this.tech_.trigger({type:"usage",name:"hls-alternate-audio"})),this.useCueTags_&&(this.tech_.trigger({type:"usage",name:"vhs-playlist-cue-tags"}),this.tech_.trigger({type:"usage",name:"hls-playlist-cue-tags"}))},i.shouldSwitchToMedia_=function(e){var t=this.masterPlaylistLoader_.media(),i=this.tech_.buffered();return function(e){var t=e.currentPlaylist,i=e.nextPlaylist,n=e.forwardBuffer,r=e.bufferLowWaterLine,a=e.bufferHighWaterLine,s=e.duration,o=e.experimentalBufferBasedABR,u=e.log;if(!i)return Yr.log.warn("We received no playlist to switch to. Please check your stream."),!1;var l="allowing switch "+(t&&t.id||"null")+" -> "+i.id;if(!t)return u(l+" as current playlist is not set"),!0;if(i.id===t.id)return!1;if(!t.endList)return u(l+" as current playlist is live"),!0;var h=o?ns.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE:ns.MAX_BUFFER_LOW_WATER_LINE;if(sc)&&n>=r){var p=l+" as forwardBuffer >= bufferLowWaterLine ("+n+" >= "+r+")";return o&&(p+=" and next bandwidth > current bandwidth ("+d+" > "+c+")"),u(p),!0}return u("not "+l+" as no switching criteria met"),!1}({currentPlaylist:t,nextPlaylist:e,forwardBuffer:i.length?i.end(i.length-1)-this.tech_.currentTime():0,bufferLowWaterLine:this.bufferLowWaterLine(),bufferHighWaterLine:this.bufferHighWaterLine(),duration:this.duration(),experimentalBufferBasedABR:this.experimentalBufferBasedABR,log:this.logger_})},i.setupSegmentLoaderListeners_=function(){var e=this;this.experimentalBufferBasedABR||(this.mainSegmentLoader_.on("bandwidthupdate",(function(){var t=e.selectPlaylist();e.shouldSwitchToMedia_(t)&&e.switchMedia_(t,"bandwidthupdate"),e.tech_.trigger("bandwidthupdate")})),this.mainSegmentLoader_.on("progress",(function(){e.trigger("progress")}))),this.mainSegmentLoader_.on("error",(function(){e.blacklistCurrentPlaylist(e.mainSegmentLoader_.error())})),this.mainSegmentLoader_.on("appenderror",(function(){e.error=e.mainSegmentLoader_.error_,e.trigger("error")})),this.mainSegmentLoader_.on("syncinfoupdate",(function(){e.onSyncInfoUpdate_()})),this.mainSegmentLoader_.on("timestampoffset",(function(){e.tech_.trigger({type:"usage",name:"vhs-timestamp-offset"}),e.tech_.trigger({type:"usage",name:"hls-timestamp-offset"})})),this.audioSegmentLoader_.on("syncinfoupdate",(function(){e.onSyncInfoUpdate_()})),this.audioSegmentLoader_.on("appenderror",(function(){e.error=e.audioSegmentLoader_.error_,e.trigger("error")})),this.mainSegmentLoader_.on("ended",(function(){e.logger_("main segment loader ended"),e.onEndOfStream()})),this.mainSegmentLoader_.on("earlyabort",(function(t){e.experimentalBufferBasedABR||(e.delegateLoaders_("all",["abort"]),e.blacklistCurrentPlaylist({message:"Aborted early because there isn't enough bandwidth to complete the request without rebuffering."},120))}));var t=function(){if(!e.sourceUpdater_.hasCreatedSourceBuffers())return e.tryToCreateSourceBuffers_();var t=e.getCodecsOrExclude_();t&&e.sourceUpdater_.addOrChangeSourceBuffers(t)};this.mainSegmentLoader_.on("trackinfo",t),this.audioSegmentLoader_.on("trackinfo",t),this.mainSegmentLoader_.on("fmp4",(function(){e.triggeredFmp4Usage||(e.tech_.trigger({type:"usage",name:"vhs-fmp4"}),e.tech_.trigger({type:"usage",name:"hls-fmp4"}),e.triggeredFmp4Usage=!0)})),this.audioSegmentLoader_.on("fmp4",(function(){e.triggeredFmp4Usage||(e.tech_.trigger({type:"usage",name:"vhs-fmp4"}),e.tech_.trigger({type:"usage",name:"hls-fmp4"}),e.triggeredFmp4Usage=!0)})),this.audioSegmentLoader_.on("ended",(function(){e.logger_("audioSegmentLoader ended"),e.onEndOfStream()}))},i.mediaSecondsLoaded_=function(){return Math.max(this.audioSegmentLoader_.mediaSecondsLoaded+this.mainSegmentLoader_.mediaSecondsLoaded)},i.load=function(){this.mainSegmentLoader_.load(),this.mediaTypes_.AUDIO.activePlaylistLoader&&this.audioSegmentLoader_.load(),this.mediaTypes_.SUBTITLES.activePlaylistLoader&&this.subtitleSegmentLoader_.load()},i.smoothQualityChange_=function(e){void 0===e&&(e=this.selectPlaylist()),this.fastQualityChange_(e)},i.fastQualityChange_=function(e){var t=this;void 0===e&&(e=this.selectPlaylist()),e!==this.masterPlaylistLoader_.media()?(this.switchMedia_(e,"fast-quality"),this.mainSegmentLoader_.resetEverything((function(){Yr.browser.IE_VERSION||Yr.browser.IS_EDGE?t.tech_.setCurrentTime(t.tech_.currentTime()+.04):t.tech_.setCurrentTime(t.tech_.currentTime())}))):this.logger_("skipping fastQualityChange because new media is same as old")},i.play=function(){if(!this.setupFirstPlay()){this.tech_.ended()&&this.tech_.setCurrentTime(0),this.hasPlayed_&&this.load();var e=this.tech_.seekable();return this.tech_.duration()===1/0&&this.tech_.currentTime()this.maxPlaylistRetries?1/0:Date.now()+1e3*t,i.excludeUntil=n,e.reason&&(i.lastExcludeReason_=e.reason),this.tech_.trigger("blacklistplaylist"),this.tech_.trigger({type:"usage",name:"vhs-rendition-blacklisted"}),this.tech_.trigger({type:"usage",name:"hls-rendition-blacklisted"});var u=this.selectPlaylist();if(!u)return this.error="Playback cannot continue. No available working or supported playlists.",void this.trigger("error");var l=e.internal?this.logger_:Yr.log.warn,h=e.message?" "+e.message:"";l((e.internal?"Internal problem":"Problem")+" encountered with playlist "+i.id+"."+h+" Switching to playlist "+u.id+"."),u.attributes.AUDIO!==i.attributes.AUDIO&&this.delegateLoaders_("audio",["abort","pause"]),u.attributes.SUBTITLES!==i.attributes.SUBTITLES&&this.delegateLoaders_("subtitle",["abort","pause"]),this.delegateLoaders_("main",["abort","pause"]);var d=u.targetDuration/2*1e3||5e3,c="number"==typeof u.lastRequest&&Date.now()-u.lastRequest<=d;return this.switchMedia_(u,"exclude",s||c)},i.pauseLoading=function(){this.delegateLoaders_("all",["abort","pause"]),this.stopABRTimer_()},i.delegateLoaders_=function(e,t){var i=this,n=[],r="all"===e;(r||"main"===e)&&n.push(this.masterPlaylistLoader_);var a=[];(r||"audio"===e)&&a.push("AUDIO"),(r||"subtitle"===e)&&(a.push("CLOSED-CAPTIONS"),a.push("SUBTITLES")),a.forEach((function(e){var t=i.mediaTypes_[e]&&i.mediaTypes_[e].activePlaylistLoader;t&&n.push(t)})),["main","audio","subtitle"].forEach((function(t){var r=i[t+"SegmentLoader_"];!r||e!==t&&"all"!==e||n.push(r)})),n.forEach((function(e){return t.forEach((function(t){"function"==typeof e[t]&&e[t]()}))}))},i.setCurrentTime=function(e){var t=Zr(this.tech_.buffered(),e);return this.masterPlaylistLoader_&&this.masterPlaylistLoader_.media()&&this.masterPlaylistLoader_.media().segments?t&&t.length?e:(this.mainSegmentLoader_.resetEverything(),this.mainSegmentLoader_.abort(),this.mediaTypes_.AUDIO.activePlaylistLoader&&(this.audioSegmentLoader_.resetEverything(),this.audioSegmentLoader_.abort()),this.mediaTypes_.SUBTITLES.activePlaylistLoader&&(this.subtitleSegmentLoader_.resetEverything(),this.subtitleSegmentLoader_.abort()),void this.load()):0},i.duration=function(){if(!this.masterPlaylistLoader_)return 0;var e=this.masterPlaylistLoader_.media();return e?e.endList?this.mediaSource?this.mediaSource.duration:Zs.Playlist.duration(e):1/0:0},i.seekable=function(){return this.seekable_},i.onSyncInfoUpdate_=function(){var e;if(this.masterPlaylistLoader_){var t=this.masterPlaylistLoader_.media();if(t){var i=this.syncController_.getExpiredTime(t,this.duration());if(null!==i){var n=this.masterPlaylistLoader_.master,r=Zs.Playlist.seekable(t,i,Zs.Playlist.liveEdgeDelay(n,t));if(0!==r.length){if(this.mediaTypes_.AUDIO.activePlaylistLoader){if(t=this.mediaTypes_.AUDIO.activePlaylistLoader.media(),null===(i=this.syncController_.getExpiredTime(t,this.duration())))return;if(0===(e=Zs.Playlist.seekable(t,i,Zs.Playlist.liveEdgeDelay(n,t))).length)return}var a,s;this.seekable_&&this.seekable_.length&&(a=this.seekable_.end(0),s=this.seekable_.start(0)),e?e.start(0)>r.end(0)||r.start(0)>e.end(0)?this.seekable_=r:this.seekable_=Yr.createTimeRanges([[e.start(0)>r.start(0)?e.start(0):r.start(0),e.end(0)0&&(n=Math.max(n,i.end(i.length-1))),this.mediaSource.duration!==n&&this.sourceUpdater_.setDuration(n)}},i.dispose=function(){var e=this;this.trigger("dispose"),this.decrypter_.terminate(),this.masterPlaylistLoader_.dispose(),this.mainSegmentLoader_.dispose(),this.loadOnPlay_&&this.tech_.off("play",this.loadOnPlay_),["AUDIO","SUBTITLES"].forEach((function(t){var i=e.mediaTypes_[t].groups;for(var n in i)i[n].forEach((function(e){e.playlistLoader&&e.playlistLoader.dispose()}))})),this.audioSegmentLoader_.dispose(),this.subtitleSegmentLoader_.dispose(),this.sourceUpdater_.dispose(),this.timelineChangeController_.dispose(),this.stopABRTimer_(),this.updateDuration_&&this.mediaSource.removeEventListener("sourceopen",this.updateDuration_),this.mediaSource.removeEventListener("durationchange",this.handleDurationChange_),this.mediaSource.removeEventListener("sourceopen",this.handleSourceOpen_),this.mediaSource.removeEventListener("sourceended",this.handleSourceEnded_),this.off()},i.master=function(){return this.masterPlaylistLoader_.master},i.media=function(){return this.masterPlaylistLoader_.media()||this.initialMedia_},i.areMediaTypesKnown_=function(){var e=!!this.mediaTypes_.AUDIO.activePlaylistLoader,t=!!this.mainSegmentLoader_.getCurrentMediaInfo_(),i=!e||!!this.audioSegmentLoader_.getCurrentMediaInfo_();return!(!t||!i)},i.getCodecsOrExclude_=function(){var e=this,t={main:this.mainSegmentLoader_.getCurrentMediaInfo_()||{},audio:this.audioSegmentLoader_.getCurrentMediaInfo_()||{}};t.video=t.main;var i=Us(this.master(),this.media()),n={},r=!!this.mediaTypes_.AUDIO.activePlaylistLoader;if(t.main.hasVideo&&(n.video=i.video||t.main.videoCodec||_.DEFAULT_VIDEO_CODEC),t.main.isMuxed&&(n.video+=","+(i.audio||t.main.audioCodec||_.DEFAULT_AUDIO_CODEC)),(t.main.hasAudio&&!t.main.isMuxed||t.audio.hasAudio||r)&&(n.audio=i.audio||t.main.audioCodec||t.audio.audioCodec||_.DEFAULT_AUDIO_CODEC,t.audio.isFmp4=t.main.hasAudio&&!t.main.isMuxed?t.main.isFmp4:t.audio.isFmp4),n.audio||n.video){var a,s={};if(["video","audio"].forEach((function(e){if(n.hasOwnProperty(e)&&(r=t[e].isFmp4,o=n[e],!(r?_.browserSupportsCodec(o):_.muxerSupportsCodec(o)))){var i=t[e].isFmp4?"browser":"muxer";s[i]=s[i]||[],s[i].push(n[e]),"audio"===e&&(a=i)}var r,o})),r&&a&&this.media().attributes.AUDIO){var o=this.media().attributes.AUDIO;this.master().playlists.forEach((function(t){(t.attributes&&t.attributes.AUDIO)===o&&t!==e.media()&&(t.excludeUntil=1/0)})),this.logger_("excluding audio group "+o+" as "+a+' does not support codec(s): "'+n.audio+'"')}if(!Object.keys(s).length){if(this.sourceUpdater_.hasCreatedSourceBuffers()&&!this.sourceUpdater_.canChangeType()){var u=[];if(["video","audio"].forEach((function(t){var i=(_.parseCodecs(e.sourceUpdater_.codecs[t]||"")[0]||{}).type,r=(_.parseCodecs(n[t]||"")[0]||{}).type;i&&r&&i.toLowerCase()!==r.toLowerCase()&&u.push('"'+e.sourceUpdater_.codecs[t]+'" -> "'+n[t]+'"')})),u.length)return void this.blacklistCurrentPlaylist({playlist:this.media(),message:"Codec switching not supported: "+u.join(", ")+".",blacklistDuration:1/0,internal:!0})}return n}var l=Object.keys(s).reduce((function(e,t){return e&&(e+=", "),e+=t+' does not support codec(s): "'+s[t].join(",")+'"'}),"")+".";this.blacklistCurrentPlaylist({playlist:this.media(),internal:!0,message:l,blacklistDuration:1/0})}else this.blacklistCurrentPlaylist({playlist:this.media(),message:"Could not determine codecs for playlist.",blacklistDuration:1/0})},i.tryToCreateSourceBuffers_=function(){if("open"===this.mediaSource.readyState&&!this.sourceUpdater_.hasCreatedSourceBuffers()&&this.areMediaTypesKnown_()){var e=this.getCodecsOrExclude_();if(e){this.sourceUpdater_.createSourceBuffers(e);var t=[e.video,e.audio].filter(Boolean).join(",");this.excludeIncompatibleVariants_(t)}}},i.excludeUnsupportedVariants_=function(){var e=this,t=this.master().playlists,i=[];Object.keys(t).forEach((function(n){var r=t[n];if(-1===i.indexOf(r.id)){i.push(r.id);var a=Us(e.master,r),s=[];!a.audio||_.muxerSupportsCodec(a.audio)||_.browserSupportsCodec(a.audio)||s.push("audio codec "+a.audio),!a.video||_.muxerSupportsCodec(a.video)||_.browserSupportsCodec(a.video)||s.push("video codec "+a.video),a.text&&"stpp.ttml.im1t"===a.text&&s.push("text codec "+a.text),s.length&&(r.excludeUntil=1/0,e.logger_("excluding "+r.id+" for unsupported: "+s.join(", ")))}}))},i.excludeIncompatibleVariants_=function(e){var t=this,i=[],n=this.master().playlists,r=Ds(_.parseCodecs(e)),a=Os(r),s=r.video&&_.parseCodecs(r.video)[0]||null,o=r.audio&&_.parseCodecs(r.audio)[0]||null;Object.keys(n).forEach((function(e){var r=n[e];if(-1===i.indexOf(r.id)&&r.excludeUntil!==1/0){i.push(r.id);var u=[],l=Us(t.masterPlaylistLoader_.master,r),h=Os(l);if(l.audio||l.video){if(h!==a&&u.push('codec count "'+h+'" !== "'+a+'"'),!t.sourceUpdater_.canChangeType()){var d=l.video&&_.parseCodecs(l.video)[0]||null,c=l.audio&&_.parseCodecs(l.audio)[0]||null;d&&s&&d.type.toLowerCase()!==s.type.toLowerCase()&&u.push('video codec "'+d.type+'" !== "'+s.type+'"'),c&&o&&c.type.toLowerCase()!==o.type.toLowerCase()&&u.push('audio codec "'+c.type+'" !== "'+o.type+'"')}u.length&&(r.excludeUntil=1/0,t.logger_("blacklisting "+r.id+": "+u.join(" && ")))}}}))},i.updateAdCues_=function(e){var t=0,i=this.seekable();i.length&&(t=i.start(0)),function(e,t,i){if(void 0===i&&(i=0),e.segments)for(var n,r=i,a=0;a0&&this.logger_("resetting possible stalled download count for "+e+" loader"),this[e+"StalledDownloads_"]=0,this[e+"Buffered_"]=t.buffered_()},t.checkSegmentDownloads_=function(e){var t=this.masterPlaylistController_,i=t[e+"SegmentLoader_"],n=i.buffered_(),r=function(e,t){if(e===t)return!1;if(!e&&t||!t&&e)return!0;if(e.length!==t.length)return!0;for(var i=0;i=t.end(t.length-1)))return this.techWaiting_();this.consecutiveUpdates>=5&&e===this.lastRecordedTime?(this.consecutiveUpdates++,this.waiting_()):e===this.lastRecordedTime?this.consecutiveUpdates++:(this.consecutiveUpdates=0,this.lastRecordedTime=e)}},t.cancelTimer_=function(){this.consecutiveUpdates=0,this.timer_&&(this.logger_("cancelTimer_"),clearTimeout(this.timer_)),this.timer_=null},t.fixesBadSeeks_=function(){if(!this.tech_.seeking())return!1;var e,t=this.seekable(),i=this.tech_.currentTime();this.afterSeekableWindow_(t,i,this.media(),this.allowSeeksWithinUnsafeLiveWindow)&&(e=t.end(t.length-1));if(this.beforeSeekableWindow_(t,i)){var n=t.start(0);e=n+(n===t.end(0)?0:.1)}if(void 0!==e)return this.logger_("Trying to seek outside of seekable at time "+i+" with seekable range "+ta(t)+". Seeking to "+e+"."),this.tech_.setCurrentTime(e),!0;var r=this.tech_.buffered();return!!function(e){var t=e.buffered,i=e.targetDuration,n=e.currentTime;return!!t.length&&(!(t.end(0)-t.start(0)<2*i)&&(!(n>t.start(0))&&t.start(0)-n "+i.end(0)+"]. Attempting to resume playback by seeking to the current time."),this.tech_.trigger({type:"usage",name:"vhs-unknown-waiting"}),void this.tech_.trigger({type:"usage",name:"hls-unknown-waiting"})):void 0}},t.techWaiting_=function(){var e=this.seekable(),t=this.tech_.currentTime();if(this.tech_.seeking()&&this.fixesBadSeeks_())return!0;if(this.tech_.seeking()||null!==this.timer_)return!0;if(this.beforeSeekableWindow_(e,t)){var i=e.end(e.length-1);return this.logger_("Fell out of live window at time "+t+". Seeking to live point (seekable end) "+i),this.cancelTimer_(),this.tech_.setCurrentTime(i),this.tech_.trigger({type:"usage",name:"vhs-live-resync"}),this.tech_.trigger({type:"usage",name:"hls-live-resync"}),!0}var n=this.tech_.vhs.masterPlaylistController_.sourceUpdater_,r=this.tech_.buffered();if(this.videoUnderflow_({audioBuffered:n.audioBuffered(),videoBuffered:n.videoBuffered(),currentTime:t}))return this.cancelTimer_(),this.tech_.setCurrentTime(t),this.tech_.trigger({type:"usage",name:"vhs-video-underflow"}),this.tech_.trigger({type:"usage",name:"hls-video-underflow"}),!0;var a=ea(r,t);if(a.length>0){var s=a.start(0)-t;return this.logger_("Stopped at "+t+", setting timer for "+s+", seeking to "+a.start(0)),this.cancelTimer_(),this.timer_=setTimeout(this.skipTheGap_.bind(this),1e3*s,t),!0}return!1},t.afterSeekableWindow_=function(e,t,i,n){if(void 0===n&&(n=!1),!e.length)return!1;var r=e.end(e.length-1)+.1;return!i.endList&&n&&(r=e.end(e.length-1)+3*i.targetDuration),t>r},t.beforeSeekableWindow_=function(e,t){return!!(e.length&&e.start(0)>0&&t2)return{start:r,end:a}}return null},e}(),zo={errorInterval:30,getSource:function(e){return e(this.tech({IWillNotUseThisInPlugins:!0}).currentSource_||this.currentSource())}},Go=function(e){!function e(t,i){var n=0,r=0,a=Yr.mergeOptions(zo,i);t.ready((function(){t.trigger({type:"usage",name:"vhs-error-reload-initialized"}),t.trigger({type:"usage",name:"hls-error-reload-initialized"})}));var s=function(){r&&t.currentTime(r)},o=function(e){null!=e&&(r=t.duration()!==1/0&&t.currentTime()||0,t.one("loadedmetadata",s),t.src(e),t.trigger({type:"usage",name:"vhs-error-reload"}),t.trigger({type:"usage",name:"hls-error-reload"}),t.play())},u=function(){return Date.now()-n<1e3*a.errorInterval?(t.trigger({type:"usage",name:"vhs-error-reload-canceled"}),void t.trigger({type:"usage",name:"hls-error-reload-canceled"})):a.getSource&&"function"==typeof a.getSource?(n=Date.now(),a.getSource.call(t,o)):void Yr.log.error("ERROR: reloadSourceOnError - The option getSource must be a function!")},l=function e(){t.off("loadedmetadata",s),t.off("error",u),t.off("dispose",e)};t.on("error",u),t.on("dispose",l),t.reloadSourceOnError=function(i){l(),e(t,i)}}(this,e)},Wo={PlaylistLoader:Ua,Playlist:Sa,utils:Ka,STANDARD_PLAYLIST_SELECTOR:Hs,INITIAL_PLAYLIST_SELECTOR:function(){var e=this,t=this.playlists.master.playlists.filter(Sa.isEnabled);return Ns(t,(function(e,t){return js(e,t)})),t.filter((function(t){return!!Us(e.playlists.master,t).video}))[0]||null},lastBandwidthSelector:Hs,movingAverageBandwidthSelector:function(e){var t=-1,i=-1;if(e<0||e>1)throw new Error("Moving average bandwidth decay must be between 0 and 1.");return function(){var n=this.useDevicePixelRatio&&C.default.devicePixelRatio||1;return t<0&&(t=this.systemBandwidth,i=this.systemBandwidth),this.systemBandwidth>0&&this.systemBandwidth!==i&&(t=e*this.systemBandwidth+(1-e)*t,i=this.systemBandwidth),Vs(this.playlists.master,t,parseInt(Bs(this.tech_.el(),"width"),10)*n,parseInt(Bs(this.tech_.el(),"height"),10)*n,this.limitRenditionByPlayerDimensions,this.masterPlaylistController_)}},comparePlaylistBandwidth:js,comparePlaylistResolution:function(e,t){var i,n;return e.attributes.RESOLUTION&&e.attributes.RESOLUTION.width&&(i=e.attributes.RESOLUTION.width),i=i||C.default.Number.MAX_VALUE,t.attributes.RESOLUTION&&t.attributes.RESOLUTION.width&&(n=t.attributes.RESOLUTION.width),i===(n=n||C.default.Number.MAX_VALUE)&&e.attributes.BANDWIDTH&&t.attributes.BANDWIDTH?e.attributes.BANDWIDTH-t.attributes.BANDWIDTH:i-n},xhr:Na()};Object.keys(ns).forEach((function(e){Object.defineProperty(Wo,e,{get:function(){return Yr.log.warn("using Vhs."+e+" is UNSAFE be sure you know what you are doing"),ns[e]},set:function(t){Yr.log.warn("using Vhs."+e+" is UNSAFE be sure you know what you are doing"),"number"!=typeof t||t<0?Yr.log.warn("value of Vhs."+e+" must be greater than or equal to 0"):ns[e]=t}})}));var Yo=function(e,t){for(var i=t.media(),n=-1,r=0;r0?1/this.throughput:0,Math.floor(1/(t+e))},set:function(){Yr.log.error('The "systemBandwidth" property is read-only')}}}),this.options_.bandwidth&&(this.bandwidth=this.options_.bandwidth),this.options_.throughput&&(this.throughput=this.options_.throughput),Object.defineProperties(this.stats,{bandwidth:{get:function(){return i.bandwidth||0},enumerable:!0},mediaRequests:{get:function(){return i.masterPlaylistController_.mediaRequests_()||0},enumerable:!0},mediaRequestsAborted:{get:function(){return i.masterPlaylistController_.mediaRequestsAborted_()||0},enumerable:!0},mediaRequestsTimedout:{get:function(){return i.masterPlaylistController_.mediaRequestsTimedout_()||0},enumerable:!0},mediaRequestsErrored:{get:function(){return i.masterPlaylistController_.mediaRequestsErrored_()||0},enumerable:!0},mediaTransferDuration:{get:function(){return i.masterPlaylistController_.mediaTransferDuration_()||0},enumerable:!0},mediaBytesTransferred:{get:function(){return i.masterPlaylistController_.mediaBytesTransferred_()||0},enumerable:!0},mediaSecondsLoaded:{get:function(){return i.masterPlaylistController_.mediaSecondsLoaded_()||0},enumerable:!0},mediaAppends:{get:function(){return i.masterPlaylistController_.mediaAppends_()||0},enumerable:!0},mainAppendsToLoadedData:{get:function(){return i.masterPlaylistController_.mainAppendsToLoadedData_()||0},enumerable:!0},audioAppendsToLoadedData:{get:function(){return i.masterPlaylistController_.audioAppendsToLoadedData_()||0},enumerable:!0},appendsToLoadedData:{get:function(){return i.masterPlaylistController_.appendsToLoadedData_()||0},enumerable:!0},timeToLoadedData:{get:function(){return i.masterPlaylistController_.timeToLoadedData_()||0},enumerable:!0},buffered:{get:function(){return ia(i.tech_.buffered())},enumerable:!0},currentTime:{get:function(){return i.tech_.currentTime()},enumerable:!0},currentSource:{get:function(){return i.tech_.currentSource_},enumerable:!0},currentTech:{get:function(){return i.tech_.name_},enumerable:!0},duration:{get:function(){return i.tech_.duration()},enumerable:!0},master:{get:function(){return i.playlists.master},enumerable:!0},playerDimensions:{get:function(){return i.tech_.currentDimensions()},enumerable:!0},seekable:{get:function(){return ia(i.tech_.seekable())},enumerable:!0},timestamp:{get:function(){return Date.now()},enumerable:!0},videoPlaybackQuality:{get:function(){return i.tech_.getVideoPlaybackQuality()},enumerable:!0}}),this.tech_.one("canplay",this.masterPlaylistController_.setupFirstPlay.bind(this.masterPlaylistController_)),this.tech_.on("bandwidthupdate",(function(){i.options_.useBandwidthFromLocalStorage&&function(e){if(!C.default.localStorage)return!1;var t=Xo();t=t?Yr.mergeOptions(t,e):e;try{C.default.localStorage.setItem("videojs-vhs",JSON.stringify(t))}catch(e){return!1}}({bandwidth:i.bandwidth,throughput:Math.round(i.throughput)})})),this.masterPlaylistController_.on("selectedinitialmedia",(function(){var e;(e=i).representations=function(){var t=e.masterPlaylistController_.master(),i=ba(t)?e.masterPlaylistController_.getAudioTrackPlaylists_():t.playlists;return i?i.filter((function(e){return!pa(e)})).map((function(t,i){return new jo(e,t,t.id)})):[]}})),this.masterPlaylistController_.sourceUpdater_.on("createdsourcebuffers",(function(){i.setupEme_()})),this.on(this.masterPlaylistController_,"progress",(function(){this.tech_.trigger("progress")})),this.on(this.masterPlaylistController_,"firstplay",(function(){this.ignoreNextSeekingEvent_=!0})),this.setupQualityLevels_(),this.tech_.el()&&(this.mediaSourceUrl_=C.default.URL.createObjectURL(this.masterPlaylistController_.mediaSource),this.tech_.src(this.mediaSourceUrl_))}},i.setupEme_=function(){var e=this,t=this.masterPlaylistController_.mediaTypes_.AUDIO.activePlaylistLoader,i=Ko({player:this.player_,sourceKeySystems:this.source_.keySystems,media:this.playlists.media(),audioMedia:t&&t.media()});this.player_.tech_.on("keystatuschange",(function(t){"output-restricted"===t.status&&e.masterPlaylistController_.blacklistCurrentPlaylist({playlist:e.masterPlaylistController_.media(),message:"DRM keystatus changed to "+t.status+". Playlist will fail to play. Check for HDCP content.",blacklistDuration:1/0})})),11!==Yr.browser.IE_VERSION&&i?(this.logger_("waiting for EME key session creation"),qo({player:this.player_,sourceKeySystems:this.source_.keySystems,audioMedia:t&&t.media(),mainPlaylists:this.playlists.master.playlists}).then((function(){e.logger_("created EME key session"),e.masterPlaylistController_.sourceUpdater_.initializedEme()})).catch((function(t){e.logger_("error while creating EME key session",t),e.player_.error({message:"Failed to initialize media keys for EME",code:3})}))):this.masterPlaylistController_.sourceUpdater_.initializedEme()},i.setupQualityLevels_=function(){var e=this,t=Yr.players[this.tech_.options_.playerId];t&&t.qualityLevels&&!this.qualityLevels_&&(this.qualityLevels_=t.qualityLevels(),this.masterPlaylistController_.on("selectedinitialmedia",(function(){var t,i;t=e.qualityLevels_,(i=e).representations().forEach((function(e){t.addQualityLevel(e)})),Yo(t,i.playlists)})),this.playlists.on("mediachange",(function(){Yo(e.qualityLevels_,e.playlists)})))},t.version=function(){return{"@videojs/http-streaming":"2.10.2","mux.js":"5.13.0","mpd-parser":"0.19.0","m3u8-parser":"4.7.0","aes-decrypter":"3.1.2"}},i.version=function(){return this.constructor.version()},i.canChangeType=function(){return yo.canChangeType()},i.play=function(){this.masterPlaylistController_.play()},i.setCurrentTime=function(e){this.masterPlaylistController_.setCurrentTime(e)},i.duration=function(){return this.masterPlaylistController_.duration()},i.seekable=function(){return this.masterPlaylistController_.seekable()},i.dispose=function(){this.playbackWatcher_&&this.playbackWatcher_.dispose(),this.masterPlaylistController_&&this.masterPlaylistController_.dispose(),this.qualityLevels_&&this.qualityLevels_.dispose(),this.player_&&(delete this.player_.vhs,delete this.player_.dash,delete this.player_.hls),this.tech_&&this.tech_.vhs&&delete this.tech_.vhs,this.tech_&&delete this.tech_.hls,this.mediaSourceUrl_&&C.default.URL.revokeObjectURL&&(C.default.URL.revokeObjectURL(this.mediaSourceUrl_),this.mediaSourceUrl_=null),e.prototype.dispose.call(this)},i.convertToProgramTime=function(e,t){return Xa({playlist:this.masterPlaylistController_.media(),time:e,callback:t})},i.seekToProgramTime=function(e,t,i,n){return void 0===i&&(i=!0),void 0===n&&(n=2),Qa({programTime:e,playlist:this.masterPlaylistController_.media(),retryCount:n,pauseAfterSeek:i,seekTo:this.options_.seekTo,tech:this.options_.tech,callback:t})},t}(Yr.getComponent("Component")),$o={name:"videojs-http-streaming",VERSION:"2.10.2",canHandleSource:function(e,t){void 0===t&&(t={});var i=Yr.mergeOptions(Yr.options,t);return $o.canPlayType(e.type,i)},handleSource:function(e,t,i){void 0===i&&(i={});var n=Yr.mergeOptions(Yr.options,i);return t.vhs=new Qo(e,t,n),Yr.hasOwnProperty("hls")||Object.defineProperty(t,"hls",{get:function(){return Yr.log.warn("player.tech().hls is deprecated. Use player.tech().vhs instead."),t.vhs},configurable:!0}),t.vhs.xhr=Na(),t.vhs.src(e.src,e.type),t.vhs},canPlayType:function(e,t){void 0===t&&(t={});var i=Yr.mergeOptions(Yr.options,t).vhs.overrideNative,n=void 0===i?!Yr.browser.IS_ANY_SAFARI:i,r=g.simpleTypeFromSourceType(e);return r&&(!Wo.supportsTypeNatively(r)||n)?"maybe":""}};_.browserSupportsCodec("avc1.4d400d,mp4a.40.2")&&Yr.getTech("Html5").registerSourceHandler($o,0),Yr.VhsHandler=Qo,Object.defineProperty(Yr,"HlsHandler",{get:function(){return Yr.log.warn("videojs.HlsHandler is deprecated. Use videojs.VhsHandler instead."),Qo},configurable:!0}),Yr.VhsSourceHandler=$o,Object.defineProperty(Yr,"HlsSourceHandler",{get:function(){return Yr.log.warn("videojs.HlsSourceHandler is deprecated. Use videojs.VhsSourceHandler instead."),$o},configurable:!0}),Yr.Vhs=Wo,Object.defineProperty(Yr,"Hls",{get:function(){return Yr.log.warn("videojs.Hls is deprecated. Use videojs.Vhs instead."),Wo},configurable:!0}),Yr.use||(Yr.registerComponent("Hls",Wo),Yr.registerComponent("Vhs",Wo)),Yr.options.vhs=Yr.options.vhs||{},Yr.options.hls=Yr.options.hls||{},Yr.registerPlugin?Yr.registerPlugin("reloadSourceOnError",Go):Yr.plugin("reloadSourceOnError",Go),t.exports=Yr},{"@babel/runtime/helpers/assertThisInitialized":1,"@babel/runtime/helpers/construct":2,"@babel/runtime/helpers/extends":3,"@babel/runtime/helpers/inherits":4,"@babel/runtime/helpers/inheritsLoose":5,"@videojs/vhs-utils/cjs/byte-helpers":9,"@videojs/vhs-utils/cjs/codecs.js":11,"@videojs/vhs-utils/cjs/containers":12,"@videojs/vhs-utils/cjs/id3-helpers":15,"@videojs/vhs-utils/cjs/media-types.js":16,"@videojs/vhs-utils/cjs/resolve-url.js":20,"@videojs/xhr":23,"global/document":33,"global/window":34,keycode:37,"m3u8-parser":38,"mpd-parser":40,"mux.js/lib/tools/parse-sidx":42,"mux.js/lib/utils/clock":43,"safe-json-parse/tuple":45,"videojs-vtt.js":48}],48:[function(e,t,i){var n=e("global/window"),r=t.exports={WebVTT:e("./vtt.js"),VTTCue:e("./vttcue.js"),VTTRegion:e("./vttregion.js")};n.vttjs=r,n.WebVTT=r.WebVTT;var a=r.VTTCue,s=r.VTTRegion,o=n.VTTCue,u=n.VTTRegion;r.shim=function(){n.VTTCue=a,n.VTTRegion=s},r.restore=function(){n.VTTCue=o,n.VTTRegion=u},n.VTTCue||r.shim()},{"./vtt.js":49,"./vttcue.js":50,"./vttregion.js":51,"global/window":34}],49:[function(e,t,i){var n=e("global/document"),r=Object.create||function(){function e(){}return function(t){if(1!==arguments.length)throw new Error("Object.create shim only accepts one parameter.");return e.prototype=t,new e}}();function a(e,t){this.name="ParsingError",this.code=e.code,this.message=t||e.message}function s(e){function t(e,t,i,n){return 3600*(0|e)+60*(0|t)+(0|i)+(0|n)/1e3}var i=e.match(/^(\d+):(\d{1,2})(:\d{1,2})?\.(\d{3})/);return i?i[3]?t(i[1],i[2],i[3].replace(":",""),i[4]):i[1]>59?t(i[1],i[2],0,i[4]):t(0,i[1],i[2],i[4]):null}function o(){this.values=r(null)}function u(e,t,i,n){var r=n?e.split(n):[e];for(var a in r)if("string"==typeof r[a]){var s=r[a].split(i);if(2===s.length)t(s[0],s[1])}}function l(e,t,i){var n=e;function r(){var t=s(e);if(null===t)throw new a(a.Errors.BadTimeStamp,"Malformed timestamp: "+n);return e=e.replace(/^[^\sa-zA-Z-]+/,""),t}function l(){e=e.replace(/^\s+/,"")}if(l(),t.startTime=r(),l(),"--\x3e"!==e.substr(0,3))throw new a(a.Errors.BadTimeStamp,"Malformed time stamp (time stamps must be separated by '--\x3e'): "+n);e=e.substr(3),l(),t.endTime=r(),l(),function(e,t){var n=new o;u(e,(function(e,t){switch(e){case"region":for(var r=i.length-1;r>=0;r--)if(i[r].id===t){n.set(e,i[r].region);break}break;case"vertical":n.alt(e,t,["rl","lr"]);break;case"line":var a=t.split(","),s=a[0];n.integer(e,s),n.percent(e,s)&&n.set("snapToLines",!1),n.alt(e,s,["auto"]),2===a.length&&n.alt("lineAlign",a[1],["start","center","end"]);break;case"position":a=t.split(","),n.percent(e,a[0]),2===a.length&&n.alt("positionAlign",a[1],["start","center","end"]);break;case"size":n.percent(e,t);break;case"align":n.alt(e,t,["start","center","end","left","right"])}}),/:/,/\s/),t.region=n.get("region",null),t.vertical=n.get("vertical","");try{t.line=n.get("line","auto")}catch(e){}t.lineAlign=n.get("lineAlign","start"),t.snapToLines=n.get("snapToLines",!0),t.size=n.get("size",100);try{t.align=n.get("align","center")}catch(e){t.align=n.get("align","middle")}try{t.position=n.get("position","auto")}catch(e){t.position=n.get("position",{start:0,left:0,center:50,middle:50,end:100,right:100},t.align)}t.positionAlign=n.get("positionAlign",{start:"start",left:"start",center:"center",middle:"center",end:"end",right:"end"},t.align)}(e,t)}a.prototype=r(Error.prototype),a.prototype.constructor=a,a.Errors={BadSignature:{code:0,message:"Malformed WebVTT signature."},BadTimeStamp:{code:1,message:"Malformed time stamp."}},o.prototype={set:function(e,t){this.get(e)||""===t||(this.values[e]=t)},get:function(e,t,i){return i?this.has(e)?this.values[e]:t[i]:this.has(e)?this.values[e]:t},has:function(e){return e in this.values},alt:function(e,t,i){for(var n=0;n=0&&t<=100)&&(this.set(e,t),!0)}};var h=n.createElement&&n.createElement("textarea"),d={c:"span",i:"i",b:"b",u:"u",ruby:"ruby",rt:"rt",v:"span",lang:"span"},c={white:"rgba(255,255,255,1)",lime:"rgba(0,255,0,1)",cyan:"rgba(0,255,255,1)",red:"rgba(255,0,0,1)",yellow:"rgba(255,255,0,1)",magenta:"rgba(255,0,255,1)",blue:"rgba(0,0,255,1)",black:"rgba(0,0,0,1)"},f={v:"title",lang:"lang"},p={rt:"ruby"};function m(e,t){function i(){if(!t)return null;var e,i=t.match(/^([^<]*)(<[^>]*>?)?/);return e=i[1]?i[1]:i[2],t=t.substr(e.length),e}function n(e,t){return!p[t.localName]||p[t.localName]===e.localName}function r(t,i){var n=d[t];if(!n)return null;var r=e.document.createElement(n),a=f[t];return a&&i&&(r[a]=i.trim()),r}for(var a,o,u=e.document.createElement("div"),l=u,m=[];null!==(a=i());)if("<"!==a[0])l.appendChild(e.document.createTextNode((o=a,h.innerHTML=o,o=h.textContent,h.textContent="",o)));else{if("/"===a[1]){m.length&&m[m.length-1]===a.substr(2).replace(">","")&&(m.pop(),l=l.parentNode);continue}var _,g=s(a.substr(1,a.length-2));if(g){_=e.document.createProcessingInstruction("timestamp",g),l.appendChild(_);continue}var v=a.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/);if(!v)continue;if(!(_=r(v[1],v[3])))continue;if(!n(l,_))continue;if(v[2]){var y=v[2].split(".");y.forEach((function(e){var t=/^bg_/.test(e),i=t?e.slice(3):e;if(c.hasOwnProperty(i)){var n=t?"background-color":"color",r=c[i];_.style[n]=r}})),_.className=y.join(" ")}m.push(v[1]),l.appendChild(_),l=_}return u}var _=[[1470,1470],[1472,1472],[1475,1475],[1478,1478],[1488,1514],[1520,1524],[1544,1544],[1547,1547],[1549,1549],[1563,1563],[1566,1610],[1645,1647],[1649,1749],[1765,1766],[1774,1775],[1786,1805],[1807,1808],[1810,1839],[1869,1957],[1969,1969],[1984,2026],[2036,2037],[2042,2042],[2048,2069],[2074,2074],[2084,2084],[2088,2088],[2096,2110],[2112,2136],[2142,2142],[2208,2208],[2210,2220],[8207,8207],[64285,64285],[64287,64296],[64298,64310],[64312,64316],[64318,64318],[64320,64321],[64323,64324],[64326,64449],[64467,64829],[64848,64911],[64914,64967],[65008,65020],[65136,65140],[65142,65276],[67584,67589],[67592,67592],[67594,67637],[67639,67640],[67644,67644],[67647,67669],[67671,67679],[67840,67867],[67872,67897],[67903,67903],[67968,68023],[68030,68031],[68096,68096],[68112,68115],[68117,68119],[68121,68147],[68160,68167],[68176,68184],[68192,68223],[68352,68405],[68416,68437],[68440,68466],[68472,68479],[68608,68680],[126464,126467],[126469,126495],[126497,126498],[126500,126500],[126503,126503],[126505,126514],[126516,126519],[126521,126521],[126523,126523],[126530,126530],[126535,126535],[126537,126537],[126539,126539],[126541,126543],[126545,126546],[126548,126548],[126551,126551],[126553,126553],[126555,126555],[126557,126557],[126559,126559],[126561,126562],[126564,126564],[126567,126570],[126572,126578],[126580,126583],[126585,126588],[126590,126590],[126592,126601],[126603,126619],[126625,126627],[126629,126633],[126635,126651],[1114109,1114109]];function g(e){for(var t=0;t<_.length;t++){var i=_[t];if(e>=i[0]&&e<=i[1])return!0}return!1}function v(e){var t=[],i="";if(!e||!e.childNodes)return"ltr";function n(e,t){for(var i=t.childNodes.length-1;i>=0;i--)e.push(t.childNodes[i])}function r(e){if(!e||!e.length)return null;var t=e.pop(),i=t.textContent||t.innerText;if(i){var a=i.match(/^.*(\n|\r)/);return a?(e.length=0,a[0]):i}return"ruby"===t.tagName?r(e):t.childNodes?(n(e,t),r(e)):void 0}for(n(t,e);i=r(t);)for(var a=0;a=0&&e.line<=100))return e.line;if(!e.track||!e.track.textTrackList||!e.track.textTrackList.mediaElement)return-1;for(var t=e.track,i=t.textTrackList,n=0,r=0;rd&&(h=h<0?-1:1,h*=Math.ceil(d/l)*l),s<0&&(h+=""===a.vertical?i.height:i.width,o=o.reverse()),r.move(c,h)}else{var f=r.lineHeight/i.height*100;switch(a.lineAlign){case"center":s-=f/2;break;case"end":s-=f}switch(a.vertical){case"":t.applyStyles({top:t.formatStyle(s,"%")});break;case"rl":t.applyStyles({left:t.formatStyle(s,"%")});break;case"lr":t.applyStyles({right:t.formatStyle(s,"%")})}o=["+y","-x","+x","-y"],r=new S(t)}var p=function(e,t){for(var r,a=new S(e),s=1,o=0;ou&&(r=new S(e),s=u),e=new S(a)}return r||a}(r,o);t.move(p.toCSSCompatValues(i))}function E(){}y.prototype.applyStyles=function(e,t){for(var i in t=t||this.div,e)e.hasOwnProperty(i)&&(t.style[i]=e[i])},y.prototype.formatStyle=function(e,t){return 0===e?0:e+t},b.prototype=r(y.prototype),b.prototype.constructor=b,S.prototype.move=function(e,t){switch(t=void 0!==t?t:this.lineHeight,e){case"+x":this.left+=t,this.right+=t;break;case"-x":this.left-=t,this.right-=t;break;case"+y":this.top+=t,this.bottom+=t;break;case"-y":this.top-=t,this.bottom-=t}},S.prototype.overlaps=function(e){return this.lefte.left&&this.tope.top},S.prototype.overlapsAny=function(e){for(var t=0;t=e.top&&this.bottom<=e.bottom&&this.left>=e.left&&this.right<=e.right},S.prototype.overlapsOppositeAxis=function(e,t){switch(t){case"+x":return this.lefte.right;case"+y":return this.tope.bottom}},S.prototype.intersectPercentage=function(e){return Math.max(0,Math.min(this.right,e.right)-Math.max(this.left,e.left))*Math.max(0,Math.min(this.bottom,e.bottom)-Math.max(this.top,e.top))/(this.height*this.width)},S.prototype.toCSSCompatValues=function(e){return{top:this.top-e.top,bottom:e.bottom-this.bottom,left:this.left-e.left,right:e.right-this.right,height:this.height,width:this.width}},S.getSimpleBoxPosition=function(e){var t=e.div?e.div.offsetHeight:e.tagName?e.offsetHeight:0,i=e.div?e.div.offsetWidth:e.tagName?e.offsetWidth:0,n=e.div?e.div.offsetTop:e.tagName?e.offsetTop:0;return{left:(e=e.div?e.div.getBoundingClientRect():e.tagName?e.getBoundingClientRect():e).left,right:e.right,top:e.top||n,height:e.height||t,bottom:e.bottom||n+(e.height||t),width:e.width||i}},E.StringDecoder=function(){return{decode:function(e){if(!e)return"";if("string"!=typeof e)throw new Error("Error - expected string data.");return decodeURIComponent(encodeURIComponent(e))}}},E.convertCueToDOMTree=function(e,t){return e&&t?m(e,t):null};E.processCues=function(e,t,i){if(!e||!t||!i)return null;for(;i.firstChild;)i.removeChild(i.firstChild);var n=e.document.createElement("div");if(n.style.position="absolute",n.style.left="0",n.style.right="0",n.style.top="0",n.style.bottom="0",n.style.margin="1.5%",i.appendChild(n),function(e){for(var t=0;t100)throw new Error("Position must be between 0 and 100.");m=e,this.hasBeenReset=!0}},positionAlign:{enumerable:!0,get:function(){return _},set:function(e){var t=a(e);t&&(_=t,this.hasBeenReset=!0)}},size:{enumerable:!0,get:function(){return g},set:function(e){if(e<0||e>100)throw new Error("Size must be between 0 and 100.");g=e,this.hasBeenReset=!0}},align:{enumerable:!0,get:function(){return v},set:function(e){var t=a(e);if(!t)throw new SyntaxError("align: an invalid or illegal alignment string was specified.");v=t,this.hasBeenReset=!0}}}),this.displayState=void 0}s.prototype.getCueAsHTML=function(){return WebVTT.convertCueToDOMTree(window,this.text)},t.exports=s},{}],51:[function(e,t,i){var n={"":!0,up:!0};function r(e){return"number"==typeof e&&e>=0&&e<=100}t.exports=function(){var e=100,t=3,i=0,a=100,s=0,o=100,u="";Object.defineProperties(this,{width:{enumerable:!0,get:function(){return e},set:function(t){if(!r(t))throw new Error("Width must be between 0 and 100.");e=t}},lines:{enumerable:!0,get:function(){return t},set:function(e){if("number"!=typeof e)throw new TypeError("Lines must be set to a number.");t=e}},regionAnchorY:{enumerable:!0,get:function(){return a},set:function(e){if(!r(e))throw new Error("RegionAnchorX must be between 0 and 100.");a=e}},regionAnchorX:{enumerable:!0,get:function(){return i},set:function(e){if(!r(e))throw new Error("RegionAnchorY must be between 0 and 100.");i=e}},viewportAnchorY:{enumerable:!0,get:function(){return o},set:function(e){if(!r(e))throw new Error("ViewportAnchorY must be between 0 and 100.");o=e}},viewportAnchorX:{enumerable:!0,get:function(){return s},set:function(e){if(!r(e))throw new Error("ViewportAnchorX must be between 0 and 100.");s=e}},scroll:{enumerable:!0,get:function(){return u},set:function(e){var t=function(e){return"string"==typeof e&&(!!n[e.toLowerCase()]&&e.toLowerCase())}(e);!1===t||(u=t)}}})}},{}],52:[function(e,t,i){"use strict";t.exports={H265WEBJS_COMPILE_MULTI_THREAD_SHAREDBUFFER:0,DEFAULT_PLAYERE_LOAD_TIMEOUT:20,DEFAILT_WEBGL_PLAY_ID:"glplayer",PLAYER_IN_TYPE_MP4:"mp4",PLAYER_IN_TYPE_FLV:"flv",PLAYER_IN_TYPE_HTTPFLV:"httpflv",PLAYER_IN_TYPE_RAW_265:"raw265",PLAYER_IN_TYPE_TS:"ts",PLAYER_IN_TYPE_MPEGTS:"mpegts",PLAYER_IN_TYPE_M3U8:"hls",PLAYER_IN_TYPE_M3U8_VOD:"m3u8",PLAYER_IN_TYPE_M3U8_LIVE:"hls",APPEND_TYPE_STREAM:0,APPEND_TYPE_FRAME:1,APPEND_TYPE_SEQUENCE:2,DEFAULT_WIDTH:600,DEFAULT_HEIGHT:600,DEFAULT_FPS:30,DEFAULT_FRAME_DUR:40,DEFAULT_FIXED:!1,DEFAULT_SAMPLERATE:44100,DEFAULT_CHANNELS:2,DEFAULT_CONSU_SAMPLE_LEN:20,PLAYER_MODE_VOD:"vod",PLAYER_MODE_NOTIME_LIVE:"live",AUDIO_MODE_ONCE:"ONCE",AUDIO_MODE_SWAP:"SWAP",DEFAULT_STRING_LIVE:"LIVE",CODEC_H265:0,CODEC_H264:1,PLAYER_CORE_TYPE_DEFAULT:0,PLAYER_CORE_TYPE_CNATIVE:1,PLAYER_CNATIVE_VOD_RETRY_MAX:7,URI_PROTOCOL_WEBSOCKET:"ws",URI_PROTOCOL_WEBSOCKET_DESC:"websocket",URI_PROTOCOL_HTTP:"http",URI_PROTOCOL_HTTP_DESC:"http",FETCH_FIRST_MAX_TIMES:5,FETCH_HTTP_FLV_TIMEOUT_MS:7e3,V_CODEC_NAME_HEVC:265,V_CODEC_NAME_AVC:264,V_CODEC_NAME_UNKN:500,A_CODEC_NAME_AAC:112,A_CODEC_NAME_MP3:113,A_CODEC_NAME_UNKN:500,CACHE_NO_LOADCACHE:1001,CACHE_WITH_PLAY_SIGN:1002,CACHE_WITH_NOPLAY_SIGN:1003,V_CODEC_AVC_DEFAULT_FPS:25}},{}],53:[function(e,t,i){"use strict";var n=window.AudioContext||window.webkitAudioContext,r=e("../consts"),a=e("./av-common");t.exports=function(){var e={options:{sampleRate:r.DEFAULT_SAMPLERATE,appendType:r.APPEND_TYPE_FRAME,playMode:r.AUDIO_MODE_SWAP},sourceChannel:-1,audioCtx:new n({latencyHint:"interactive",sampleRate:r.DEFAULT_SAMPLERATE}),gainNode:null,sourceList:[],startStatus:!1,sampleQueue:[],nextBuffer:null,playTimestamp:0,playStartTime:0,durationMs:-1,isLIVE:!1,voice:1,onLoadCache:null,resetStartParam:function(){e.playTimestamp=0,e.playStartTime=0},setOnLoadCache:function(t){e.onLoadCache=t},setDurationMs:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:-1;e.durationMs=t},setVoice:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;e.voice=t,e.gainNode.gain.value=t},getAlignVPTS:function(){return e.playTimestamp+(a.GetMsTime()-e.playStartTime)/1e3},swapSource:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:-1,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:-1;if(0==e.startStatus)return null;if(t<0||t>=e.sourceList.length)return null;if(i<0||i>=e.sourceList.length)return null;try{e.sourceChannel===t&&null!==e.sourceList[t]&&(e.sourceList[t].disconnect(e.gainNode),e.sourceList[t]=null)}catch(e){console.error("[DEFINE ERROR] audioPcmModule disconnect source Index:"+t+" error happened!",e)}e.sourceChannel=i;var n=e.decodeSample(i,t);-2==n&&e.isLIVE&&(e.getAlignVPTS()>=e.durationMs/1e3-.04?e.pause():null!==e.onLoadCache&&e.onLoadCache())},addSample:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;return!(null==t||!t||null==t)&&(0==e.sampleQueue.length&&(e.seekPos=t.pts),e.sampleQueue.push(t),e.sampleQueue.length,!0)},runNextBuffer:function(){window.setInterval((function(){if(!(null!=e.nextBuffer||e.sampleQueue.length0&&void 0!==arguments[0]?arguments[0]:-1,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:-1;if(t<0||t>=e.sourceList.length)return-1;if(null!=e.sourceList[t]&&null!=e.sourceList[t]&&e.sourceList[t]||(e.sourceList[t]=e.audioCtx.createBufferSource(),e.sourceList[t].onended=function(){e.swapSource(t,i)}),0==e.sampleQueue.length)return e.isLIVE?(e.sourceList[t].connect(e.gainNode),e.sourceList[t].start(),e.sourceList[t].onended=function(){e.swapSource(t,i)},e.sourceList[t].stop(),0):-2;if(e.sourceList[t].buffer)return e.swapSource(t,i),0;if(null==e.nextBuffer||e.nextBuffer.data.length<1)return e.sourceList[t].connect(e.gainNode),e.sourceList[t].start(),e.sourceList[t].startState=!0,e.sourceList[t].stop(),1;var n=e.nextBuffer.data;e.playTimestamp=e.nextBuffer.pts,e.playStartTime=a.GetMsTime(),e.nextBuffer.data,e.playTimestamp;try{var r=e.audioCtx.createBuffer(1,n.length,e.options.sampleRate);r.copyToChannel(n,0),null!==e.sourceList[t]&&(e.sourceList[t].buffer=r,e.sourceList[t].connect(e.gainNode),e.sourceList[t].start(),e.sourceList[t].startState=!0)}catch(t){return e.nextBuffer=null,-3}return e.nextBuffer=null,0},decodeWholeSamples:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:-1;if(e.sourceChannel=t,t<0||t>=e.sourceList.length)return-1;if(null!=e.sourceList[t]&&null!=e.sourceList[t]&&e.sourceList[t]||(e.sourceList[t]=e.audioCtx.createBufferSource(),e.sourceList[t].onended=function(){}),0==e.sampleQueue.length)return-2;for(var i=null,n=null,a=0;a0&&void 0!==arguments[0]?arguments[0]:-1;t.durationMs=e},setVoice:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;t.voice=e,t.gainNode.gain.value=e},getAlignVPTS:function(){return t.playTimestamp+(a.GetMsTime()-t.playStartTime)/1e3},swapSource:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:-1,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:-1;if(0==t.startStatus)return null;if(e<0||e>=t.sourceList.length)return null;if(i<0||i>=t.sourceList.length)return null;try{t.sourceChannel===e&&null!==t.sourceList[e]&&(t.sourceList[e].disconnect(t.gainNode),t.sourceList[e]=null)}catch(t){console.error("[DEFINE ERROR] audioModule disconnect source Index:"+e+" error happened!",t)}t.sourceChannel=i;var n=t.decodeSample(i,e);-2==n&&t.isLIVE&&(t.getAlignVPTS()>=t.durationMs/1e3-.04?t.pause():null!==t.onLoadCache&&t.onLoadCache())},addSample:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;return!(null==e||!e||null==e)&&(0==t.sampleQueue.length&&(t.seekPos=e.pts),t.sampleQueue.push(e),!0)},runNextBuffer:function(){window.setInterval((function(){if(!(null!=t.nextBuffer||t.sampleQueue.length0&&void 0!==arguments[0]?arguments[0]:-1,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:-1;if(e<0||e>=t.sourceList.length)return-1;if(null!=t.sourceList[e]&&null!=t.sourceList[e]&&t.sourceList[e]||(t.sourceList[e]=t.audioCtx.createBufferSource(),t.sourceList[e].onended=function(){t.swapSource(e,i)}),0==t.sampleQueue.length)return t.isLIVE?(t.sourceList[e].connect(t.gainNode),t.sourceList[e].start(),t.sourceList[e].onended=function(){t.swapSource(e,i)},t.sourceList[e].stop(),0):-2;if(t.sourceList[e].buffer)return t.swapSource(e,i),0;if(null==t.nextBuffer||t.nextBuffer.data.length<1)return t.sourceList[e].connect(t.gainNode),t.sourceList[e].start(),t.sourceList[e].startState=!0,t.sourceList[e].stop(),1;var n=t.nextBuffer.data.buffer;t.playTimestamp=t.nextBuffer.pts,t.playStartTime=a.GetMsTime();try{t.audioCtx.decodeAudioData(n,(function(i){null!==t.sourceList[e]&&(t.sourceList[e].buffer=i,t.sourceList[e].connect(t.gainNode),t.sourceList[e].start(),t.sourceList[e].startState=!0)}),(function(e){}))}catch(e){return t.nextBuffer=null,-3}return t.nextBuffer=null,0},decodeWholeSamples:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:-1;if(t.sourceChannel=e,e<0||e>=t.sourceList.length)return-1;if(null!=t.sourceList[e]&&null!=t.sourceList[e]&&t.sourceList[e]||(t.sourceList[e]=t.audioCtx.createBufferSource(),t.sourceList[e].onended=function(){}),0==t.sampleQueue.length)return-2;for(var i=null,n=null,a=0;a=2){var s=i.length/2;a=new Float32Array(s);for(var o=0,u=0;uthis._push_start_idx))return-1;this.playStartTime<0&&(this.playStartTime=a.GetMsTime(),this.playTimestamp=a.GetMsTime()),this._swapStartPlay=!1;var e=this._push_start_idx+this._once_pop_len;e>this._pcm_array_buf.length&&(e=this._pcm_array_buf.length);var t=this._pcm_array_buf.slice(this._push_start_idx,e);this._push_start_idx+=t.length,this._now_seg_dur=1*t.length/this._sample_rate*1e3,t.length,this._sample_rate,this._now_seg_dur;var i=this._ctx.createBuffer(1,t.length,this._sample_rate);return t.length,new Date,i.copyToChannel(t,0),this._active_node=this._ctx.createBufferSource(),this._active_node.buffer=i,this._active_node.connect(this._gain),this.playStartTime=a.GetMsTime(),this._active_node.start(0),this.playTimestamp+=this._now_seg_dur,0}},{key:"getAlignVPTS",value:function(){return this.playTimestamp}},{key:"pause",value:function(){null!==this._playInterval&&(window.clearInterval(this._playInterval),this._playInterval=null)}},{key:"play",value:function(){var e=this;this._playInterval=window.setInterval((function(){e.readingLoopWithF32()}),10)}}])&&n(t.prototype,i),s&&n(t,s),e}();i.AudioPcmPlayer=s},{"../consts":52,"./av-common":56}],56:[function(e,t,i){"use strict";var n=e("../consts"),r=[{format:"mp4",value:"mp4",core:n.PLAYER_CORE_TYPE_CNATIVE},{format:"mov",value:"mp4",core:n.PLAYER_CORE_TYPE_CNATIVE},{format:"mkv",value:"mp4",core:n.PLAYER_CORE_TYPE_CNATIVE},{format:"flv",value:"flv",core:n.PLAYER_CORE_TYPE_CNATIVE},{format:"m3u8",value:"hls",core:n.PLAYER_CORE_TYPE_DEFAULT},{format:"m3u",value:"hls",core:n.PLAYER_CORE_TYPE_DEFAULT},{format:"ts",value:"ts",core:n.PLAYER_CORE_TYPE_DEFAULT},{format:"ps",value:"ts",core:n.PLAYER_CORE_TYPE_DEFAULT},{format:"mpegts",value:"ts",core:n.PLAYER_CORE_TYPE_DEFAULT},{format:"hevc",value:"raw265",core:n.PLAYER_CORE_TYPE_DEFAULT},{format:"h265",value:"raw265",core:n.PLAYER_CORE_TYPE_DEFAULT},{format:"265",value:"raw265",core:n.PLAYER_CORE_TYPE_DEFAULT}],a=[{format:n.URI_PROTOCOL_HTTP,value:n.URI_PROTOCOL_HTTP_DESC},{format:n.URI_PROTOCOL_WEBSOCKET,value:n.URI_PROTOCOL_WEBSOCKET_DESC}];t.exports={frameDataAlignCrop:function(e,t,i,n,r,a,s,o){if(0==e-n)return[a,s,o];for(var u=n*r,l=u/4,h=new Uint8Array(u),d=new Uint8Array(l),c=new Uint8Array(l),f=n,p=n/2,m=0;m=0)return i.value}return r[0].value},GetFormatPlayCore:function(e){if(null!=e)for(var t=0;t=0)return i.value}return a[0].value},GetMsTime:function(){return(new Date).getTime()},GetScriptPath:function(e){var t=e.toString(),i=t.match(/^\s*function\s*\(\s*\)\s*\{(([\s\S](?!\}$))*[\s\S])/),n=[i[1]];return window.URL.createObjectURL(new Blob(n,{type:"text/javascript"}))},BrowserJudge:function(){var e=window.document,t=window.navigator.userAgent.toLowerCase(),i=e.documentMode,n=window.chrome||!1,r={agent:t,isIE:/msie/.test(t),isGecko:t.indexOf("gecko")>0&&t.indexOf("like gecko")<0,isWebkit:t.indexOf("webkit")>0,isStrict:"CSS1Compat"===e.compatMode,supportSubTitle:function(){return"track"in e.createElement("track")},supportScope:function(){return"scoped"in e.createElement("style")},ieVersion:function(){try{return t.match(/msie ([\d.]+)/)[1]||0}catch(e){return i}},operaVersion:function(){try{if(window.opera)return t.match(/opera.([\d.]+)/)[1];if(t.indexOf("opr")>0)return t.match(/opr\/([\d.]+)/)[1]}catch(e){return 0}},versionFilter:function(){if(1===arguments.length&&"string"==typeof arguments[0]){var e=arguments[0],t=e.indexOf(".");if(t>0){var i=e.indexOf(".",t+1);if(-1!==i)return e.substr(0,i)}return e}return 1===arguments.length?arguments[0]:0}};try{r.type=r.isIE?"IE":window.opera||t.indexOf("opr")>0?"Opera":t.indexOf("chrome")>0?"Chrome":t.indexOf("safari")>0||window.openDatabase?"Safari":t.indexOf("firefox")>0?"Firefox":"unknow",r.version="IE"===r.type?r.ieVersion():"Firefox"===r.type?t.match(/firefox\/([\d.]+)/)[1]:"Chrome"===r.type?t.match(/chrome\/([\d.]+)/)[1]:"Opera"===r.type?r.operaVersion():"Safari"===r.type?t.match(/version\/([\d.]+)/)[1]:"0",r.shell=function(){if(t.indexOf("maxthon")>0)return r.version=t.match(/maxthon\/([\d.]+)/)[1]||r.version,"傲游浏览器";if(t.indexOf("qqbrowser")>0)return r.version=t.match(/qqbrowser\/([\d.]+)/)[1]||r.version,"QQ浏览器";if(t.indexOf("se 2.x")>0)return"搜狗浏览器";if(n&&"Opera"!==r.type){var e=window.external,i=window.clientInformation.languages;if(e&&"LiebaoGetVersion"in e)return"猎豹浏览器";if(t.indexOf("bidubrowser")>0)return r.version=t.match(/bidubrowser\/([\d.]+)/)[1]||t.match(/chrome\/([\d.]+)/)[1],"百度浏览器";if(r.supportSubTitle()&&void 0===i){var a=Object.keys(n.webstore).length;window;return a>1?"360极速浏览器":"360安全浏览器"}return"Chrome"}return r.type},r.name=r.shell(),r.version=r.versionFilter(r.version)}catch(e){}return[r.type,r.version]},ParseGetMediaURL:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"http";if("http"!==t&&"ws"!==t&&"wss"!==t&&(e.indexOf("ws")>=0||e.indexOf("wss")>=0)&&(t="ws"),"ws"===t||"wss"===t)return e;var i=e;if(e.indexOf(t)>=0)i=e;else if("/"===e[0])i="/"===e[1]?t+":"+e:window.location.origin+e;else if(":"===e[0])i=t+e;else{var n=window.location.href.split("/");i=window.location.href.replace(n[n.length-1],e)}return i},IsSupport265Mse:function(){return MediaSource.isTypeSupported('video/mp4;codecs=hvc1.1.1.L63.B0"')}}},{"../consts":52}],57:[function(e,t,i){"use strict";function n(e,t){for(var i=0;i0&&a.GetMsTime()-t.getPackageTimeMS>=o.FETCH_HTTP_FLV_TIMEOUT_MS&&(t.getPackageTimeMS=a.GetMsTime(),t.workerFetch.postMessage({cmd:"retry",data:null,msg:"retry"}))}),5));break;case"fetch-chunk":var n=i.data;t.download_length+=n.length,setTimeout((function(){var e=Module._malloc(n.length);Module.HEAP8.set(n,e),Module.cwrap("pushSniffG711FlvData","number",["number","number","number","number"])(t.corePtr,e,n.length,t.config.probeSize),Module._free(e),e=null}),0),t.totalLen+=n.length,n.length>0&&(t.getPackageTimeMS=a.GetMsTime()),t.pushPkg++;break;case"close":t.AVGetInterval&&clearInterval(t.AVGetInterval),t.AVGetInterval=null;case"fetch-fin":break;case"fetch-error":t.onError&&t.onError(i.data)}}},{key:"_checkDisplaySize",value:function(e,t,i){var n=t-e,r=this.config.width+Math.ceil(n/2),a=t/this.config.width>i/this.config.height,s=(r/t).toFixed(2),o=(this.config.height/i).toFixed(2),u=a?s:o,l=this.config.fixed,h=l?r:parseInt(t*u),d=l?this.config.height:parseInt(i*u);if(this.CanvasObj.offsetWidth!=h||this.CanvasObj.offsetHeight!=d){var c=parseInt((this.canvasBox.offsetHeight-d)/2),f=parseInt((this.canvasBox.offsetWidth-h)/2);c=c<0?0:c,f=f<0?0:f,this.CanvasObj.style.marginTop=c+"px",this.CanvasObj.style.marginLeft=f+"px",this.CanvasObj.style.width=h+"px",this.CanvasObj.style.height=d+"px"}return this.isCheckDisplay=!0,[h,d]}},{key:"_ptsFixed2",value:function(e){return Math.ceil(100*e)/100}},{key:"_reinitAudioModule",value:function(){void 0!==this.audioWAudio&&null!==this.audioWAudio&&(this.audioWAudio.stop(),this.audioWAudio=null),this.audioWAudio=s()}},{key:"_callbackProbe",value:function(e,t,i,n,r,a,s,u,l){for(var h=Module.HEAPU8.subarray(l,l+10),d=0;d100&&(c=o.DEFAULT_FPS,this.mediaInfo.noFPS=!0),this.vCodecID=u,this.config.fps=c,this.mediaInfo.fps=c,this.mediaInfo.size.width=t,this.mediaInfo.size.height=i,this.frameTime=Math.floor(1e3/(this.mediaInfo.fps+2)),this.CanvasObj.width==t&&this.CanvasObj.height==i||(this.CanvasObj.width=t,this.CanvasObj.height=i,this.isCheckDisplay)||this._checkDisplaySize(t,t,i),r>=0&&!1===this.mediaInfo.noFPS?(this.config.sampleRate=a,this.mediaInfo.sampleRate=a,!1===this.muted&&this._reinitAudioModule(this.mediaInfo.sampleRate)):this.mediaInfo.audioNone=!0,this.onProbeFinish&&this.onProbeFinish()}},{key:"_callbackYUV",value:function(e,t,i,n,r,a,s,o,u,l){var h=this,d=Module.HEAPU8.subarray(e,e+n*o),c=new Uint8Array(d),f=Module.HEAPU8.subarray(t,t+r*o/2),p=new Uint8Array(f),m=Module.HEAPU8.subarray(i,i+a*o/2),_={bufY:c,bufU:p,bufV:new Uint8Array(m),line_y:n,h:o,pts:u};this.YuvBuf.push(_),this.checkCacheState(),Module._free(d),d=null,Module._free(f),f=null,Module._free(m),m=null,!1===this.readyShowDone&&!0===this.playYUV()&&(this.readyShowDone=!0,this.onReadyShowDone&&this.onReadyShowDone(),this.audioWAudio||!0!==this.config.autoPlay||(this.play(),setTimeout((function(){h.isPlayingState()}),3e3)))}},{key:"_callbackNALU",value:function(e,t,i,n,r,a,s){if(!1===this.readyKeyFrame){if(i<=0)return;this.readyKeyFrame=!0}var o=Module.HEAPU8.subarray(e,e+t),u=new Uint8Array(o);this.NaluBuf.push({bufData:u,len:t,isKey:i,w:n,h:r,pts:1e3*a,dts:1e3*s}),Module._free(o),o=null}},{key:"_callbackPCM",value:function(e,t,i,n){var r=Module.HEAPU8.subarray(e,e+t),a=new Uint8Array(r).buffer,s=this._ptsFixed2(i),o=null,u=a.byteLength%4;if(0!==u){var l=new Uint8Array(a.byteLength+u);l.set(new Uint8Array(a),0),o=new Float32Array(l.buffer)}else o=new Float32Array(a);var h={pts:s,data:o};this.audioWAudio.addSample(h),this.checkCacheState()}},{key:"_decode",value:function(){var e=this;setTimeout((function(){null!==e.workerFetch&&(Module.cwrap("decodeG711Frame","number",["number"])(e.corePtr),e._decode())}),1)}},{key:"setScreen",value:function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0];this.showScreen=e}},{key:"checkCacheState",value:function(){var e=this.YuvBuf.length>=25&&(!0===this.mediaInfo.audioNone||this.audioWAudio&&this.audioWAudio.sampleQueue.length>=50);return!1===this.cache_status&&e&&(this.playInterval&&this.audioWAudio&&this.audioWAudio.play(),this.onLoadCacheFinshed&&this.onLoadCacheFinshed(),this.cache_status=!0),e}},{key:"setVoice",value:function(e){this.audioVoice=e,this.audioWAudio&&this.audioWAudio.setVoice(e)}},{key:"_removeBindFuncPtr",value:function(){null!==this._ptr_probeCallback&&Module.removeFunction(this._ptr_probeCallback),null!==this._ptr_frameCallback&&Module.removeFunction(this._ptr_frameCallback),null!==this._ptr_naluCallback&&Module.removeFunction(this._ptr_naluCallback),null!==this._ptr_sampleCallback&&Module.removeFunction(this._ptr_sampleCallback),null!==this._ptr_aacCallback&&Module.removeFunction(this._ptr_aacCallback),this._ptr_probeCallback=null,this._ptr_frameCallback=null,this._ptr_naluCallback=null,this._ptr_sampleCallback=null,this._ptr_aacCallback=null}},{key:"release",value:function(){return this.pause(),this.NaluBuf.length=0,this.YuvBuf.length=0,void 0!==this.workerFetch&&null!==this.workerFetch&&this.workerFetch.postMessage({cmd:"stop",data:"stop",msg:"stop"}),this.workerFetch=null,this.AVGetInterval&&clearInterval(this.AVGetInterval),this.AVGetInterval=null,this._removeBindFuncPtr(),void 0!==this.corePtr&&null!==this.corePtr&&Module.cwrap("releaseG711","number",["number"])(this.corePtr),this.playInterval&&clearInterval(this.playInterval),this.playInterval=null,this.audioWAudio&&this.audioWAudio.stop(),this.audioWAudio=null,void 0!==this.AVGLObj&&null!==this.AVGLObj&&(r.releaseContext(this.AVGLObj),this.AVGLObj=null),this.CanvasObj&&this.CanvasObj.remove(),this.CanvasObj=null,window.onclick=document.body.onclick=null,delete window.g_players[this.corePtr],0}},{key:"isPlayingState",value:function(){return null!==this.playInterval&&void 0!==this.playInterval}},{key:"pause",value:function(){this.audioWAudio&&this.audioWAudio.pause(),this.playInterval&&clearInterval(this.playInterval),this.playInterval=null,this.onPlayState&&this.onPlayState(this.isPlayingState())}},{key:"playYUV",value:function(){if(this.YuvBuf.length>0){var e=this.YuvBuf.shift();return e.pts,this.onRender&&this.onRender(e.line_y,e.h,e.bufY,e.bufU,e.bufV),r.renderFrame(this.AVGLObj,e.bufY,e.bufU,e.bufV,e.line_y,e.h),!0}return!1}},{key:"play",value:function(){var e=this;if(!1===this.checkCacheState())return this.onLoadCache&&this.onLoadCache(),setTimeout((function(){e.play()}),100),!1;var t=1*e.frameTime;if(void 0===this.playInterval||null===this.playInterval){var i=0,n=0,s=0;!1===this.mediaInfo.audioNone&&this.audioWAudio&&!1===this.mediaInfo.noFPS?(this.playInterval=setInterval((function(){if(n=a.GetMsTime(),e.cache_status){if(n-i>=e.frameTime-s){var o=e.YuvBuf.shift();if(null!=o&&null!==o){o.pts;var u=0;null!==e.audioWAudio&&void 0!==e.audioWAudio?(u=1e3*(o.pts-e.audioWAudio.getAlignVPTS()),s=u<0&&-1*u<=t||u>0&&u<=t||0===u||u>0&&u>t?a.GetMsTime()-n+1:e.frameTime):s=a.GetMsTime()-n+1,e.showScreen&&e.onRender&&e.onRender(o.line_y,o.h,o.bufY,o.bufU,o.bufV),o.pts,r.renderFrame(e.AVGLObj,o.bufY,o.bufU,o.bufV,o.line_y,o.h)}e.YuvBuf.length<=0&&(e.cache_status=!1,e.onLoadCache&&e.onLoadCache(),e.audioWAudio&&e.audioWAudio.pause()),i=n}}else s=e.frameTime}),1),this.audioWAudio&&this.audioWAudio.play()):this.playInterval=setInterval((function(){var t=e.YuvBuf.shift();null!=t&&null!==t&&(t.pts,e.showScreen&&e.onRender&&e.onRender(t.line_y,t.h,t.bufY,t.bufU,t.bufV),r.renderFrame(e.AVGLObj,t.bufY,t.bufU,t.bufV,t.line_y,t.h)),e.YuvBuf.length<=0&&(e.cache_status=!1)}),e.frameTime)}this.onPlayState&&this.onPlayState(this.isPlayingState())}},{key:"start",value:function(e){var t=this;this.workerFetch=new Worker(a.GetScriptPath((function(){var e=null,t=new AbortController,i=t.signal,n=(self,function(e){var t=!1;t||(t=!0,fetch(e,{signal:i}).then((function(e){return function e(t){return t.read().then((function(i){if(!i.done){var n=i.value;return self.postMessage({cmd:"fetch-chunk",data:n,msg:"fetch-chunk"}),e(t)}self.postMessage({cmd:"fetch-fin",data:null,msg:"fetch-fin"})}))}(e.body.getReader())})).catch((function(e){if(!e.toString().includes("user aborted")){var t=" httplive request error:"+e+" start to retry";console.error(t),self.postMessage({cmd:"fetch-error",data:t,msg:"fetch-error"})}})))});self.onmessage=function(r){var a=r.data;switch(void 0===a.cmd||null===a.cmd?"":a.cmd){case"start":e=a.data,n(e),self.postMessage({cmd:"startok",data:"WORKER STARTED",msg:"startok"});break;case"stop":t.abort(),self.close(),self.postMessage({cmd:"close",data:"close",msg:"close"});break;case"retry":t.abort(),t=null,i=null,t=new AbortController,i=t.signal,setTimeout((function(){n(e)}),3e3)}}}))),this.workerFetch.onmessage=function(e){t._workerFetch_onmessage(e,t)},this.workerFetch,this._ptr_probeCallback=Module.addFunction(this._callbackProbe.bind(this)),this._ptr_yuvCallback=Module.addFunction(this._callbackYUV.bind(this)),this._ptr_sampleCallback=Module.addFunction(this._callbackPCM.bind(this)),Module.cwrap("initializeSniffG711Module","number",["number","number","number","number","number","number"])(this.corePtr,this._ptr_probeCallback,this._ptr_yuvCallback,this._ptr_sampleCallback,0,1),this.AVGLObj=r.setupCanvas(this.CanvasObj,{preserveDrawingBuffer:!1}),this.workerFetch.postMessage({cmd:"start",data:e,msg:"start"}),0===o.H265WEBJS_COMPILE_MULTI_THREAD_SHAREDBUFFER&&this._decode()}}])&&n(t.prototype,i),u&&n(t,u),e}());i.CHttpG711Core=u},{"../consts":52,"../demuxer/buffer":66,"../demuxer/bufferFrame":67,"../render-engine/webgl-420p":81,"../version":84,"./audio-core":54,"./audio-core-pcm":53,"./audio-native-core":55,"./av-common":56,"./cache":61,"./cacheYuv":62}],58:[function(e,t,i){"use strict";function n(e,t){for(var i=0;it.config.probeSize?(Module.cwrap("getSniffHttpFlvPkg","number",["number"])(t.corePtr),t.pushPkg-=1):t.getPackageTimeMS>0&&a.GetMsTime()-t.getPackageTimeMS>=o.FETCH_HTTP_FLV_TIMEOUT_MS&&(t.getPackageTimeMS=a.GetMsTime(),t.workerFetch.postMessage({cmd:"retry",data:null,msg:"retry"}))}),5));break;case"fetch-chunk":var n=i.data;t.download_length+=n.length,setTimeout((function(){var e=Module._malloc(n.length);Module.HEAP8.set(n,e),Module.cwrap("pushSniffHttpFlvData","number",["number","number","number","number"])(t.corePtr,e,n.length,t.config.probeSize),Module._free(e),e=null}),0),t.totalLen+=n.length,n.length>0&&(t.getPackageTimeMS=a.GetMsTime()),t.pushPkg++;break;case"close":t.AVGetInterval&&clearInterval(t.AVGetInterval),t.AVGetInterval=null;break;case"fetch-fin":break;case"fetch-error":t.onError&&t.onError(i.data)}}},{key:"_checkDisplaySize",value:function(e,t,i){var n=t-e,r=this.config.width+Math.ceil(n/2),a=t/this.config.width>i/this.config.height,s=(r/t).toFixed(2),o=(this.config.height/i).toFixed(2),u=a?s:o,l=this.config.fixed,h=l?r:parseInt(t*u),d=l?this.config.height:parseInt(i*u);if(this.CanvasObj.offsetWidth!=h||this.CanvasObj.offsetHeight!=d){var c=parseInt((this.canvasBox.offsetHeight-d)/2),f=parseInt((this.canvasBox.offsetWidth-h)/2);c=c<0?0:c,f=f<0?0:f,this.CanvasObj.style.marginTop=c+"px",this.CanvasObj.style.marginLeft=f+"px",this.CanvasObj.style.width=h+"px",this.CanvasObj.style.height=d+"px"}return this.isCheckDisplay=!0,[h,d]}},{key:"_ptsFixed2",value:function(e){return Math.ceil(100*e)/100}},{key:"_reinitAudioModule",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:44100;this.config.ignoreAudio>0||(void 0!==this.audioWAudio&&null!==this.audioWAudio&&(this.audioWAudio.stop(),this.audioWAudio=null),this.audioWAudio=s({sampleRate:e,appendType:o.APPEND_TYPE_FRAME}),this.audioWAudio.isLIVE=!0)}},{key:"_callbackProbe",value:function(e,t,i,n,r,a,s,u,l){var h=arguments.length>9&&void 0!==arguments[9]?arguments[9]:0;if(1!==h){for(var d=Module.HEAPU8.subarray(l,l+10),c=0;c100&&(f=o.DEFAULT_FPS,this.mediaInfo.noFPS=!0),this.vCodecID=u,this.config.fps=f,this.mediaInfo.fps=f,this.mediaInfo.size.width=t,this.mediaInfo.size.height=i,this.frameTime=Math.floor(1e3/(this.mediaInfo.fps+5)),this.chaseFrame=0,this.CanvasObj.width==t&&this.CanvasObj.height==i||(this.CanvasObj.width=t,this.CanvasObj.height=i,this.isCheckDisplay)||this._checkDisplaySize(t,t,i),r>=0&&!1===this.mediaInfo.noFPS?(this.config.sampleRate=a,this.mediaInfo.sampleRate=a,this.config.ignoreAudio<1&&!1===this.muted&&this._reinitAudioModule(this.mediaInfo.sampleRate)):this.mediaInfo.audioNone=!0,this.onProbeFinish&&this.onProbeFinish()}else this.onProbeFinish&&this.onProbeFinish(h)}},{key:"_callbackYUV",value:function(e,t,i,n,r,a,s,o,u,l){var h=this,d=Module.HEAPU8.subarray(e,e+n*o),c=new Uint8Array(d),f=Module.HEAPU8.subarray(t,t+r*o/2),p=new Uint8Array(f),m=Module.HEAPU8.subarray(i,i+a*o/2),_={bufY:c,bufU:p,bufV:new Uint8Array(m),line_y:n,h:o,pts:u};this.YuvBuf.push(_),this.YuvBuf.length,this.checkCacheState(),Module._free(d),d=null,Module._free(f),f=null,Module._free(m),m=null,!1===this.readyShowDone&&!0===this.playYUV()&&(this.readyShowDone=!0,this.onReadyShowDone&&this.onReadyShowDone(),this.audioWAudio||!0!==this.config.autoPlay||(this.play(),setTimeout((function(){h.isPlayingState()}),3e3)))}},{key:"_callbackNALU",value:function(e,t,i,n,r,a,s){if(!1===this.readyKeyFrame){if(i<=0)return;this.readyKeyFrame=!0}var o=Module.HEAPU8.subarray(e,e+t),u=new Uint8Array(o);this.NaluBuf.push({bufData:u,len:t,isKey:i,w:n,h:r,pts:1e3*a,dts:1e3*s}),Module._free(o),o=null}},{key:"_callbackPCM",value:function(e){this.config.ignoreAudio}},{key:"_callbackAAC",value:function(e,t,i,n){if(!(this.config.ignoreAudio>0)){var r=this._ptsFixed2(n);if(this.audioWAudio&&!1===this.muted){var a=Module.HEAPU8.subarray(e,e+t),s={pts:r,data:new Uint8Array(a)};this.audioWAudio.addSample(s),this.checkCacheState()}}}},{key:"_decode",value:function(){var e=this;setTimeout((function(){if(null!==e.workerFetch){var t=e.NaluBuf.shift();if(null!=t){var i=Module._malloc(t.bufData.length);Module.HEAP8.set(t.bufData,i),Module.cwrap("decodeHttpFlvVideoFrame","number",["number","number","number","number","number"])(e.corePtr,i,t.bufData.length,t.pts,t.dts,0),Module._free(i),i=null}e._decode()}}),1)}},{key:"setScreen",value:function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0];this.showScreen=e}},{key:"checkCacheState",value:function(){this.YuvBuf.length,this.config.ignoreAudio>0||!0===this.mediaInfo.audioNone||this.audioWAudio&&this.audioWAudio.sampleQueue.length;var e=this.YuvBuf.length>=25&&(!0===this.muted||this.config.ignoreAudio>0||!0===this.mediaInfo.audioNone||this.audioWAudio&&this.audioWAudio.sampleQueue.length>=50);return!1===this.cache_status&&e&&(this.playInterval&&this.audioWAudio&&this.audioWAudio.play(),this.onLoadCacheFinshed&&this.onLoadCacheFinshed(),this.cache_status=!0),e}},{key:"setVoice",value:function(e){this.config.ignoreAudio<1&&(this.audioVoice=e,this.audioWAudio&&this.audioWAudio.setVoice(e))}},{key:"_removeBindFuncPtr",value:function(){null!==this._ptr_probeCallback&&Module.removeFunction(this._ptr_probeCallback),null!==this._ptr_frameCallback&&Module.removeFunction(this._ptr_frameCallback),null!==this._ptr_naluCallback&&Module.removeFunction(this._ptr_naluCallback),null!==this._ptr_sampleCallback&&Module.removeFunction(this._ptr_sampleCallback),null!==this._ptr_aacCallback&&Module.removeFunction(this._ptr_aacCallback),this._ptr_probeCallback=null,this._ptr_frameCallback=null,this._ptr_naluCallback=null,this._ptr_sampleCallback=null,this._ptr_aacCallback=null}},{key:"release",value:function(){return this.pause(),this.NaluBuf.length=0,this.YuvBuf.length=0,void 0!==this.workerFetch&&null!==this.workerFetch&&this.workerFetch.postMessage({cmd:"stop",data:"stop",msg:"stop"}),this.workerFetch=null,this.AVGetInterval&&clearInterval(this.AVGetInterval),this.AVGetInterval=null,this._removeBindFuncPtr(),void 0!==this.corePtr&&null!==this.corePtr&&Module.cwrap("releaseHttpFLV","number",["number"])(this.corePtr),this.playInterval&&clearInterval(this.playInterval),this.playInterval=null,this.audioWAudio&&this.audioWAudio.stop(),this.audioWAudio=null,void 0!==this.AVGLObj&&null!==this.AVGLObj&&(r.releaseContext(this.AVGLObj),this.AVGLObj=null),this.CanvasObj&&this.CanvasObj.remove(),this.CanvasObj=null,window.onclick=document.body.onclick=null,delete window.g_players[this.corePtr],0}},{key:"isPlayingState",value:function(){return null!==this.playInterval&&void 0!==this.playInterval}},{key:"pause",value:function(){this.config.ignoreAudio,this.audioWAudio,this.config.ignoreAudio<1&&this.audioWAudio&&this.audioWAudio.pause(),this.playInterval&&clearInterval(this.playInterval),this.playInterval=null,this.chaseFrame=0,this.onPlayState&&this.onPlayState(this.isPlayingState())}},{key:"playYUV",value:function(){if(this.YuvBuf.length>0){var e=this.YuvBuf.shift();return this.onRender&&this.onRender(e.line_y,e.h,e.bufY,e.bufU,e.bufV),r.renderFrame(this.AVGLObj,e.bufY,e.bufU,e.bufV,e.line_y,e.h),!0}return!1}},{key:"play",value:function(){var e=this,t=this;if(this.chaseFrame=0,!1===this.checkCacheState())return this.onLoadCache&&this.onLoadCache(),setTimeout((function(){e.play()}),100),!1;var i=1*t.frameTime;if(void 0===this.playInterval||null===this.playInterval){var n=0,s=0,o=0;if(this.config.ignoreAudio<1&&!1===this.mediaInfo.audioNone&&null!=this.audioWAudio&&!1===this.mediaInfo.noFPS)this.config.ignoreAudio,this.mediaInfo.audioNone,this.audioWAudio,this.mediaInfo.noFPS,this.playInterval=setInterval((function(){if(s=a.GetMsTime(),t.cache_status){if(s-n>=t.frameTime-o){var e=t.YuvBuf.shift();if(e.pts,t.YuvBuf.length,null!=e&&null!==e){var u=0;null!==t.audioWAudio&&void 0!==t.audioWAudio?(u=1e3*(e.pts-t.audioWAudio.getAlignVPTS()),o=u<0&&-1*u<=i||u>0&&u<=i||0===u||u>0&&u>i?a.GetMsTime()-s+1:t.frameTime):o=a.GetMsTime()-s+1,t.showScreen&&t.onRender&&t.onRender(e.line_y,e.h,e.bufY,e.bufU,e.bufV),e.pts,r.renderFrame(t.AVGLObj,e.bufY,e.bufU,e.bufV,e.line_y,e.h)}(t.YuvBuf.length<=0||t.audioWAudio&&t.audioWAudio.sampleQueue.length<=0)&&(t.cache_status=!1,t.onLoadCache&&t.onLoadCache(),t.audioWAudio&&t.audioWAudio.pause()),n=s}}else o=t.frameTime}),1),this.audioWAudio&&this.audioWAudio.play();else{var u=-1;this.playInterval=setInterval((function(){if(s=a.GetMsTime(),t.cache_status){t.YuvBuf.length,t.frameTime,t.frameTime,t.chaseFrame;var e=-1;if(u>0&&(e=s-n,t.frameTime,t.chaseFrame<=0&&o>0&&(t.chaseFrame=Math.floor(o/t.frameTime),t.chaseFrame)),u<=0||e>=t.frameTime||t.chaseFrame>0){u=1;var i=t.YuvBuf.shift();i.pts,t.YuvBuf.length,null!=i&&null!==i&&(t.showScreen&&t.onRender&&t.onRender(i.line_y,i.h,i.bufY,i.bufU,i.bufV),i.pts,r.renderFrame(t.AVGLObj,i.bufY,i.bufU,i.bufV,i.line_y,i.h),o=a.GetMsTime()-s+1),t.YuvBuf.length<=0&&(t.cache_status=!1,t.onLoadCache&&t.onLoadCache()),n=s,t.chaseFrame>0&&(t.chaseFrame--,0===t.chaseFrame&&(o=t.frameTime))}}else o=t.frameTime,u=-1,t.chaseFrame=0,n=0,s=0,o=0}),1)}}this.onPlayState&&this.onPlayState(this.isPlayingState())}},{key:"start",value:function(e){var t=this;this.workerFetch=new Worker(a.GetScriptPath((function(){var e=null,t=new AbortController,i=t.signal,n=(self,function(e){var t=!1;t||(t=!0,fetch(e,{signal:i}).then((function(e){return function e(t){return t.read().then((function(i){if(!i.done){var n=i.value;return self.postMessage({cmd:"fetch-chunk",data:n,msg:"fetch-chunk"}),e(t)}self.postMessage({cmd:"fetch-fin",data:null,msg:"fetch-fin"})}))}(e.body.getReader())})).catch((function(e){if(!e.toString().includes("user aborted")){var t=" httplive request error:"+e+" start to retry";console.error(t),self.postMessage({cmd:"fetch-error",data:t,msg:"fetch-error"})}})))});self.onmessage=function(r){var a=r.data;switch(void 0===a.cmd||null===a.cmd?"":a.cmd){case"start":e=a.data,n(e),self.postMessage({cmd:"startok",data:"WORKER STARTED",msg:"startok"});break;case"stop":t.abort(),self.close(),self.postMessage({cmd:"close",data:"close",msg:"close"});break;case"retry":t.abort(),t=null,i=null,t=new AbortController,i=t.signal,setTimeout((function(){n(e)}),3e3)}}}))),this.workerFetch.onmessage=function(e){t._workerFetch_onmessage(e,t)},this.workerFetch,this._ptr_probeCallback=Module.addFunction(this._callbackProbe.bind(this)),this._ptr_yuvCallback=Module.addFunction(this._callbackYUV.bind(this)),this._ptr_naluCallback=Module.addFunction(this._callbackNALU.bind(this)),this._ptr_sampleCallback=Module.addFunction(this._callbackPCM.bind(this)),this._ptr_aacCallback=Module.addFunction(this._callbackAAC.bind(this)),Module.cwrap("initializeSniffHttpFlvModule","number",["number","number","number","number","number","number","number"])(this.corePtr,this._ptr_probeCallback,this._ptr_yuvCallback,this._ptr_naluCallback,this._ptr_sampleCallback,this._ptr_aacCallback,this.config.ignoreAudio),this.AVGLObj=r.setupCanvas(this.CanvasObj,{preserveDrawingBuffer:!1}),this.workerFetch.postMessage({cmd:"start",data:e,msg:"start"}),this._decode()}}])&&n(t.prototype,i),u&&n(t,u),e}());i.CHttpLiveCore=u},{"../consts":52,"../demuxer/buffer":66,"../demuxer/bufferFrame":67,"../render-engine/webgl-420p":81,"../version":84,"./audio-core":54,"./audio-native-core":55,"./av-common":56,"./cache":61,"./cacheYuv":62}],59:[function(e,t,i){"use strict";function n(e,t){for(var i=0;i0&&void 0!==arguments[0]&&arguments[0];this.showScreen=e}},{key:"getCachePTS",value:function(){return 1!==this.config.ignoreAudio&&this.audioWAudio?Math.max(this.vCachePTS,this.aCachePTS):this.vCachePTS}},{key:"getMaxPTS",value:function(){return Math.max(this.vCachePTS,this.aCachePTS)}},{key:"isPlayingState",value:function(){return this.isPlaying}},{key:"_clearDecInterval",value:function(){this.decVFrameInterval&&window.clearInterval(this.decVFrameInterval),this.decVFrameInterval=null}},{key:"_checkPlayFinished",value:function(){return!(this.config.playMode!==h.PLAYER_MODE_VOD||!(!0===this.bufRecvStat&&(this.playPTS>=this.bufLastVDTS||this.audioWAudio&&this.playPTS>=this.bufLastADTS)||this.duration-this.playPTS0&&n-i>=t.frameTime-r){var e=t._videoQueue.shift();e.pts,o.renderFrame(t.yuv,e.data_y,e.data_u,e.data_v,e.line1,e.height),(r=u.GetMsTime()-n)>=t.frameTime&&(r=t.frameTime),i=n}}),2):this.playFrameInterval=window.setInterval((function(){if(n=u.GetMsTime(),e._videoQueue.length>0&&n-i>=e.frameTime-r){var t=e._videoQueue.shift(),s=0;if(e.isNewSeek||null===e.audioWAudio||void 0===e.audioWAudio||(s=1e3*(t.pts-e.audioWAudio.getAlignVPTS()),e.playPTS=Math.max(e.audioWAudio.getAlignVPTS(),e.playPTS)),i=n,e.playPTS=Math.max(t.pts,e.playPTS),e.isNewSeek&&e.seekTarget-e.frameDur>t.pts)return void(r=e.frameTime);if(e.isNewSeek&&(e.audioWAudio&&e.audioWAudio.setVoice(e.audioVoice),e.audioWAudio&&e.audioWAudio.play(),r=0,e.isNewSeek=!1,e.seekTarget=0),e.showScreen&&e.onRender&&e.onRender(t.line1,t.height,t.data_y,t.data_u,t.data_v),o.renderFrame(e.yuv,t.data_y,t.data_u,t.data_v,t.line1,t.height),e.onPlayingTime&&e.onPlayingTime(t.pts),!e.isNewSeek&&e.audioWAudio&&(s<0&&-1*s<=a||s>=0)){if(e.config.playMode===h.PLAYER_MODE_VOD)if(t.pts>=e.duration)e.onLoadCacheFinshed&&e.onLoadCacheFinshed(),e.onPlayingFinish&&e.onPlayingFinish(),e._clearDecInterval(),e.pause();else if(e._checkPlayFinished())return;r=u.GetMsTime()-n}else!e.isNewSeek&&e.audioWAudio&&(r=e.frameTime)}e._checkPlayFinished()}),1)}this.isNewSeek||this.audioWAudio&&this.audioWAudio.play()}},{key:"pause",value:function(){this.isPlaying=!1,this._pause(),this.isCacheV===h.CACHE_WITH_PLAY_SIGN&&(this.isCacheV=h.CACHE_WITH_NOPLAY_SIGN)}},{key:"_pause",value:function(){this.playFrameInterval&&window.clearInterval(this.playFrameInterval),this.playFrameInterval=null,this.audioWAudio&&this.audioWAudio.pause()}},{key:"seek",value:function(e){var t=this,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.openFrameCall=!1,this.pause(),this._clearDecInterval(),null!==this.avFeedVideoInterval&&(window.clearInterval(this.avFeedVideoInterval),this.avFeedVideoInterval=null),null!==this.avFeedAudioInterval&&(window.clearInterval(this.avFeedAudioInterval),this.avFeedAudioInterval=null),this.yuvMaxTime=0,this.playVPipe.length=0,this._videoQueue.length=0,this.audioWAudio&&this.audioWAudio.stop(),e&&e(),this.isNewSeek=!0,this.avSeekVState=!0,this.seekTarget=i.seekTime,null!==this.audioWAudio&&void 0!==this.audioWAudio&&(this.audioWAudio.setVoice(0),this.audioWAudio.resetStartParam(),this.audioWAudio.stop()),this._avFeedData(i.seekTime),setTimeout((function(){t.yuvMaxTime=0,t._videoQueue.length=0,t.openFrameCall=!0,t.frameCallTag+=1,t._decVFrameIntervalFunc()}),1e3)}},{key:"setVoice",value:function(e){this.audioVoice=e,this.audioWAudio&&this.audioWAudio.setVoice(e)}},{key:"cacheIsFull",value:function(){return this._videoQueue.length>=this._VIDEO_CACHE_LEN}},{key:"_checkDisplaySize",value:function(e,t,i){var n=t-e,r=this.config.width+Math.ceil(n/2),a=t/this.config.width>i/this.config.height,s=(r/t).toFixed(2),o=(this.config.height/i).toFixed(2),u=a?s:o,l=this.config.fixed,h=l?r:parseInt(t*u),d=l?this.config.height:parseInt(i*u);if(this.canvas.offsetWidth!=h||this.canvas.offsetHeight!=d){var c=parseInt((this.canvasBox.offsetHeight-d)/2),f=parseInt((this.canvasBox.offsetWidth-h)/2);c=c<0?0:c,f=f<0?0:f,this.canvas.style.marginTop=c+"px",this.canvas.style.marginLeft=f+"px",this.canvas.style.width=h+"px",this.canvas.style.height=d+"px"}return this.isCheckDisplay=!0,[h,d]}},{key:"_createYUVCanvas",value:function(){this.canvasBox=document.querySelector("#"+this.config.playerId),this.canvasBox.style.overflow="hidden",this.canvas=document.createElement("canvas"),this.canvas.style.width=this.canvasBox.clientWidth+"px",this.canvas.style.height=this.canvasBox.clientHeight+"px",this.canvas.style.top="0px",this.canvas.style.left="0px",this.canvasBox.appendChild(this.canvas),this.yuv=o.setupCanvas(this.canvas,{preserveDrawingBuffer:!1})}},{key:"_avRecvPackets",value:function(){var e=this;this.bufObject.cleanPipeline(),null!==this.avRecvInterval&&(window.clearInterval(this.avRecvInterval),this.avRecvInterval=null),!0===this.config.checkProbe?this.avRecvInterval=window.setInterval((function(){Module.cwrap("getSniffStreamPkg","number",["number"])(e.corePtr),e._avCheckRecvFinish()}),5):this.avRecvInterval=window.setInterval((function(){Module.cwrap("getSniffStreamPkgNoCheckProbe","number",["number"])(e.corePtr),e._avCheckRecvFinish()}),5),this._avFeedData(0,!1)}},{key:"_avCheckRecvFinish",value:function(){this.config.playMode===h.PLAYER_MODE_VOD&&this.duration-this.getMaxPTS()=t._VIDEO_CACHE_LEN&&(t.onSeekFinish&&t.onSeekFinish(),t.onPlayingTime&&t.onPlayingTime(e),t.play(),window.clearInterval(i),i=null)}),10);return!0}},{key:"_afterAvFeedSeekToStartWithUnFinBuffer",value:function(e){var t=this,i=this,n=window.setInterval((function(){t._videoQueue.length,i._videoQueue.length>=i._VIDEO_CACHE_LEN&&(i.onSeekFinish&&i.onSeekFinish(),i.onPlayingTime&&i.onPlayingTime(e),!1===i.reFull?i.play():i.reFull=!1,window.clearInterval(n),n=null)}),10);return!0}},{key:"_avFeedData",value:function(e){var t=this;if(this.playVPipe.length=0,this.audioWAudio&&this.audioWAudio.cleanQueue(),e<=0&&!1===this.bufOK){var i=0;if(t.avFeedVideoInterval=window.setInterval((function(){var n=t.bufObject.videoBuffer.length;if(n-1>i||t.duration>0&&t.duration-t.getMaxPTS()0){for(var s=0;s0&&t.playVPipe[t.playVPipe.length-1].pts>=t.bufLastVDTS&&(window.clearInterval(t.avFeedVideoInterval),t.avFeedVideoInterval=null,t.playVPipe[t.playVPipe.length-1].pts,t.bufLastVDTS,t.bufObject.videoBuffer,t.playVPipe)}else t.config.playMode===h.PLAYER_MODE_VOD&&t.playVPipe.length>0&&t.playVPipe[t.playVPipe.length-1].pts>=t.duration&&(window.clearInterval(t.avFeedVideoInterval),t.avFeedVideoInterval=null,t.playVPipe[t.playVPipe.length-1].pts,t.duration,t.bufObject.videoBuffer,t.playVPipe);t.avSeekVState&&(t.getMaxPTS(),t.duration,t.config.playMode===h.PLAYER_MODE_VOD&&(t._afterAvFeedSeekToStartWithFinishedBuffer(e),t.avSeekVState=!1))}),5),void 0!==t.audioWAudio&&null!==t.audioWAudio&&t.config.ignoreAudio<1){var n=0;t.avFeedAudioInterval=window.setInterval((function(){var e=t.bufObject.audioBuffer.length;if(e-1>n||t.duration-t.getMaxPTS()0&&t.audioWAudio.sampleQueue[t.audioWAudio.sampleQueue.length-1].pts>=t.bufLastADTS&&(window.clearInterval(t.avFeedAudioInterval),t.avFeedAudioInterval=null,t.audioWAudio.sampleQueue[t.audioWAudio.sampleQueue.length-1].pts,t.bufObject.audioBuffer)}else t.config.playMode===h.PLAYER_MODE_VOD&&t.audioWAudio.sampleQueue.length>0&&t.audioWAudio.sampleQueue[t.audioWAudio.sampleQueue.length-1].pts>=t.duration&&(window.clearInterval(t.avFeedAudioInterval),t.avFeedAudioInterval=null,t.audioWAudio.sampleQueue[t.audioWAudio.sampleQueue.length-1].pts,t.bufObject.audioBuffer)}),5)}}else{var r=this.bufObject.seekIDR(e),s=parseInt(r,10);this.playPTS=0;var o=s;if(this.avFeedVideoInterval=window.setInterval((function(){var i=t.bufObject.videoBuffer.length;if(i-1>o||t.duration-t.getMaxPTS()0){for(var r=0;r0&&t.playVPipe[t.playVPipe.length-1].pts>=t.bufLastVDTS&&(window.clearInterval(t.avFeedVideoInterval),t.avFeedVideoInterval=null)}else t.config.playMode===h.PLAYER_MODE_VOD&&t.playVPipe.length>0&&t.playVPipe[t.playVPipe.length-1].pts>=t.duration&&(window.clearInterval(t.avFeedVideoInterval),t.avFeedVideoInterval=null);t.avSeekVState&&(t.getMaxPTS(),t.duration,t.config.playMode===h.PLAYER_MODE_VOD&&(t._afterAvFeedSeekToStartWithUnFinBuffer(e),t.avSeekVState=!1))}),5),this.audioWAudio&&this.config.ignoreAudio<1){var u=parseInt(e,10);this.avFeedAudioInterval=window.setInterval((function(){var e=t.bufObject.audioBuffer.length;if(e-1>u||t.duration-t.getMaxPTS()0&&t.audioWAudio.sampleQueue[t.audioWAudio.sampleQueue.length-1].pts>=t.bufLastADTS&&(window.clearInterval(t.avFeedAudioInterval),t.avFeedAudioInterval=null)}else t.config.playMode===h.PLAYER_MODE_VOD&&t.audioWAudio.sampleQueue.length>0&&t.audioWAudio.sampleQueue[t.audioWAudio.sampleQueue.length-1].pts>=t.duration&&(window.clearInterval(t.avFeedAudioInterval),t.avFeedAudioInterval=null)}),5)}}}},{key:"_probeFinCallback",value:function(e,t,i,n,r,a,s,o,u){var d=this;this._createYUVCanvas(),h.V_CODEC_NAME_HEVC,this.config.fps=1*n,this.frameTime=1e3/this.config.fps,this.width=t,this.height=i,this.frameDur=1/this.config.fps,this.duration=e-this.frameDur,this.vCodecID=o,this.config.sampleRate=a,this.channels=s,this.audioIdx=r,this.duration<0&&(this.config.playMode=h.PLAYER_MODE_NOTIME_LIVE,this.frameTime,this.frameDur);for(var c=Module.HEAPU8.subarray(u,u+10),f=0;f=0&&this.config.ignoreAudio<1?this.audioNone=!1:this.audioNone=!0,h.V_CODEC_NAME_HEVC===this.vCodecID&&(!1===this.audioNone&&(void 0!==this.audioWAudio&&null!==this.audioWAudio&&(this.audioWAudio.stop(),this.audioWAudio=null),this.audioWAudio=l({sampleRate:a,appendType:h.APPEND_TYPE_FRAME}),this.audioWAudio.setDurationMs(1e3*e),this.onLoadCache&&this.audioWAudio.setOnLoadCache((function(){if(d.retryAuSampleNo,d.retryAuSampleNo<=5){d.pause(),d.onLoadCache&&d.onLoadCache();var e=window.setInterval((function(){return d.retryAuSampleNo,d.audioWAudio.sampleQueue.length,d.audioWAudio.sampleQueue.length>2?(d.onLoadCacheFinshed&&d.onLoadCacheFinshed(),d.play(),d.retryAuSampleNo=0,window.clearInterval(e),void(e=null)):(d.retryAuSampleNo+=1,d.retryAuSampleNo>5?(d.play(),d.onLoadCacheFinshed&&d.onLoadCacheFinshed(),window.clearInterval(e),void(e=null)):void 0)}),1e3)}}))),this._avRecvPackets(),this._decVFrameIntervalFunc()),this.onProbeFinish&&this.onProbeFinish()}},{key:"_ptsFixed2",value:function(e){return Math.ceil(100*e)/100}},{key:"_naluCallback",value:function(e,t,i,n,r,a,s,o){var u=this._ptsFixed2(a);o>0&&(u=a);var l=Module.HEAPU8.subarray(e,e+t),h=new Uint8Array(l);this.bufObject.appendFrameWithDts(u,s,h,!0,i),this.bufLastVDTS=Math.max(s,this.bufLastVDTS),this.vCachePTS=Math.max(u,this.vCachePTS),this.onCacheProcess&&this.onCacheProcess(this.getCachePTS())}},{key:"_samplesCallback",value:function(e,t,i,n){}},{key:"_aacFrameCallback",value:function(e,t,i,n){var r=this._ptsFixed2(n);if(this.audioWAudio){var a=Module.HEAPU8.subarray(e,e+t),s=new Uint8Array(a);this.bufObject.appendFrame(r,s,!1,!0),this.bufLastADTS=Math.max(r,this.bufLastADTS),this.aCachePTS=Math.max(r,this.aCachePTS),this.onCacheProcess&&this.onCacheProcess(this.getCachePTS())}}},{key:"_setLoadCache",value:function(){if(null===this.avFeedVideoInterval&&null===this.avFeedAudioInterval&&this.playVPipe.length<=0)return 1;if(this.isCacheV===h.CACHE_NO_LOADCACHE){var e=this.isPlaying;this.pause(),this.onLoadCache&&this.onLoadCache(),this.isCacheV=e?h.CACHE_WITH_PLAY_SIGN:h.CACHE_WITH_NOPLAY_SIGN}return 0}},{key:"_setLoadCacheFinished",value:function(){this.isCacheV!==h.CACHE_NO_LOADCACHE&&(this.isCacheV,this.onLoadCacheFinshed&&this.onLoadCacheFinshed(),this.isCacheV===h.CACHE_WITH_PLAY_SIGN&&this.play(),this.isCacheV=h.CACHE_NO_LOADCACHE)}},{key:"_createDecVframeInterval",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:10,t=this;null!==this.decVFrameInterval&&(window.clearInterval(this.decVFrameInterval),this.decVFrameInterval=null);var i=0;this.loopMs=e,this.decVFrameInterval=window.setInterval((function(){if(t._videoQueue.length<1?t._setLoadCache():t._videoQueue.length>=t._VIDEO_CACHE_LEN&&t._setLoadCacheFinished(),t._videoQueue.length0){100===t.loopMs&&t._createDecVframeInterval(10);var e=t.playVPipe.shift(),n=e.data,r=Module._malloc(n.length);Module.HEAP8.set(n,r);var a=parseInt(1e3*e.pts,10),s=parseInt(1e3*e.dts,10);t.yuvMaxTime=Math.max(e.pts,t.yuvMaxTime);var o=Module.cwrap("decodeVideoFrame","number",["number","number","number","number","number"])(t.corePtr,r,n.length,a,s,t.frameCallTag);o>0&&(i=o),Module._free(r),r=null}}else i=Module.cwrap("naluLListLength","number",["number"])(t.corePtr)}),e)}},{key:"_decVFrameIntervalFunc",value:function(){null==this.decVFrameInterval&&this._createDecVframeInterval(10)}},{key:"_frameCallback",value:function(e,t,i,n,r,a,s,o,u,l){if(this._videoQueue.length,!1===this.openFrameCall)return-1;if(l!==this.frameCallTag)return-2;if(u>this.yuvMaxTime+this.frameDur)return-3;if(this.isNewSeek&&this.seekTarget-u>3*this.frameDur)return-4;var h=this._videoQueue.length;if(this.canvas.width==n&&this.canvas.height==o||(this.canvas.width=n,this.canvas.height=o,this.isCheckDisplay)||this._checkDisplaySize(s,n,o),this.playPTS>u)return-5;var d=Module.HEAPU8.subarray(e,e+n*o),f=Module.HEAPU8.subarray(t,t+r*o/2),p=Module.HEAPU8.subarray(i,i+a*o/2),m=new Uint8Array(d),_=new Uint8Array(f),g=new Uint8Array(p),v=new c(m,_,g,n,r,a,s,o,u);if(h<=0||u>this._videoQueue[h-1].pts)this._videoQueue.push(v);else if(uthis._videoQueue[y].pts&&y+1this.yuvMaxTime+this.frameDur||this.isNewSeek&&this.seekTarget-u>3*this.frameDur)){var p=this._videoQueue.length;if(this.canvas.width==n&&this.canvas.height==o||(this.canvas.width=n,this.canvas.height=o,this.isCheckDisplay)||this._checkDisplaySize(s,n,o),!(this.playPTS>u)){var m=new c(h,d,f,n,r,a,s,o,u);if(p<=0||u>this._videoQueue[p-1].pts)this._videoQueue.push(m);else if(uthis._videoQueue[_].pts&&_+10){var e=this._videoQueue.shift();return e.pts,this.onRender&&this.onRender(e.line1,e.height,e.data_y,e.data_u,e.data_v),o.renderFrame(this.yuv,e.data_y,e.data_u,e.data_v,e.line1,e.height),!0}return!1}},{key:"setProbeSize",value:function(e){this.probeSize=e}},{key:"pushBuffer",value:function(e){if(void 0===this.corePtr||null===this.corePtr)return-1;var t=Module._malloc(e.length);Module.HEAP8.set(e,t);var i=Module.cwrap("pushSniffStreamData","number",["number","number","number","number"])(this.corePtr,t,e.length,this.probeSize);return i}}])&&n(t.prototype,i),f&&n(t,f),e}();i.CNativeCore=f},{"../consts":52,"../demuxer/buffer":66,"../demuxer/bufferFrame":67,"../render-engine/webgl-420p":81,"../version":84,"./audio-core":54,"./audio-native-core":55,"./av-common":56,"./cache":61,"./cacheYuv":62}],60:[function(e,t,i){"use strict";function n(e,t){for(var i=0;i0&&(t.getPackageTimeMS=a.GetMsTime()),t.pushPkg++,void 0!==t.AVGetInterval&&null!==t.AVGetInterval||(t.AVGetInterval=window.setInterval((function(){Module.cwrap("getBufferLengthApi","number",["number"])(t.corePtr)>t.config.probeSize&&(Module.cwrap("getSniffHttpFlvPkg","number",["number"])(t.corePtr),t.pushPkg-=1)}),5));break;case"close":t.AVGetInterval&&clearInterval(t.AVGetInterval),t.AVGetInterval=null;case"fetch-fin":break;case"fetch-error":t.onError&&t.onError(i.data)}}},{key:"_checkDisplaySize",value:function(e,t,i){var n=t-e,r=this.config.width+Math.ceil(n/2),a=t/this.config.width>i/this.config.height,s=(r/t).toFixed(2),o=(this.config.height/i).toFixed(2),u=a?s:o,l=this.config.fixed,h=l?r:parseInt(t*u),d=l?this.config.height:parseInt(i*u);if(this.CanvasObj.offsetWidth!=h||this.CanvasObj.offsetHeight!=d){var c=parseInt((this.canvasBox.offsetHeight-d)/2),f=parseInt((this.canvasBox.offsetWidth-h)/2);c=c<0?0:c,f=f<0?0:f,this.CanvasObj.style.marginTop=c+"px",this.CanvasObj.style.marginLeft=f+"px",this.CanvasObj.style.width=h+"px",this.CanvasObj.style.height=d+"px"}return this.isCheckDisplay=!0,[h,d]}},{key:"_ptsFixed2",value:function(e){return Math.ceil(100*e)/100}},{key:"_callbackProbe",value:function(e,t,i,n,r,a,u,l,h){for(var d=Module.HEAPU8.subarray(h,h+10),c=0;c100&&(f=o.DEFAULT_FPS,this.mediaInfo.noFPS=!0),this.vCodecID=l,this.config.fps=f,this.mediaInfo.fps=f,this.mediaInfo.size.width=t,this.mediaInfo.size.height=i,this.frameTime=Math.floor(1e3/(this.mediaInfo.fps+2)),this.CanvasObj.width==t&&this.CanvasObj.height==i||(this.CanvasObj.width=t,this.CanvasObj.height=i,this.isCheckDisplay)||this._checkDisplaySize(t,t,i),r>=0&&!1===this.mediaInfo.noFPS&&this.config.ignoreAudio<1?(void 0!==this.audioWAudio&&null!==this.audioWAudio&&(this.audioWAudio.stop(),this.audioWAudio=null),this.config.sampleRate=a,this.mediaInfo.sampleRate=a,this.audioWAudio=s({sampleRate:this.mediaInfo.sampleRate,appendType:o.APPEND_TYPE_FRAME}),this.audioWAudio.isLIVE=!0):this.mediaInfo.audioNone=!0,this.onProbeFinish&&this.onProbeFinish()}},{key:"_callbackYUV",value:function(e,t,i,n,r,a,s,o,u){var l=Module.HEAPU8.subarray(e,e+n*o),h=new Uint8Array(l),d=Module.HEAPU8.subarray(t,t+r*o/2),c=new Uint8Array(d),f=Module.HEAPU8.subarray(i,i+a*o/2),p={bufY:h,bufU:c,bufV:new Uint8Array(f),line_y:n,h:o,pts:u};this.YuvBuf.push(p),this.checkCacheState(),Module._free(l),l=null,Module._free(d),d=null,Module._free(f),f=null,!1===this.readyShowDone&&!0===this.playYUV()&&(this.readyShowDone=!0,this.onReadyShowDone&&this.onReadyShowDone(),this.audioWAudio||this.play())}},{key:"_callbackNALU",value:function(e,t,i,n,r,a,s){if(!1===this.readyKeyFrame){if(i<=0)return;this.readyKeyFrame=!0}var o=Module.HEAPU8.subarray(e,e+t),u=new Uint8Array(o);this.NaluBuf.push({bufData:u,len:t,isKey:i,w:n,h:r,pts:1e3*a,dts:1e3*s}),Module._free(o),o=null}},{key:"_callbackPCM",value:function(e){}},{key:"_callbackAAC",value:function(e,t,i,n){var r=this._ptsFixed2(n);if(this.audioWAudio){var a=Module.HEAPU8.subarray(e,e+t),s={pts:r,data:new Uint8Array(a)};this.audioWAudio.addSample(s),this.checkCacheState()}}},{key:"_decode",value:function(){var e=this;setTimeout((function(){if(null!==e.workerFetch){var t=e.NaluBuf.shift();if(null!=t){var i=Module._malloc(t.bufData.length);Module.HEAP8.set(t.bufData,i),Module.cwrap("decodeHttpFlvVideoFrame","number",["number","number","number","number","number"])(e.corePtr,i,t.bufData.length,t.pts,t.dts,0),Module._free(i),i=null}e._decode()}}),1)}},{key:"setScreen",value:function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0];this.showScreen=e}},{key:"checkCacheState",value:function(){var e=this.YuvBuf.length>=25&&(!0===this.mediaInfo.audioNone||this.audioWAudio&&this.audioWAudio.sampleQueue.length>=50);return!1===this.cache_status&&e&&(this.playInterval&&this.audioWAudio&&this.audioWAudio.play(),this.onLoadCacheFinshed&&this.onLoadCacheFinshed(),this.cache_status=!0),e}},{key:"setVoice",value:function(e){this.audioVoice=e,this.audioWAudio&&this.audioWAudio.setVoice(e)}},{key:"_removeBindFuncPtr",value:function(){null!==this._ptr_probeCallback&&Module.removeFunction(this._ptr_probeCallback),null!==this._ptr_frameCallback&&Module.removeFunction(this._ptr_frameCallback),null!==this._ptr_naluCallback&&Module.removeFunction(this._ptr_naluCallback),null!==this._ptr_sampleCallback&&Module.removeFunction(this._ptr_sampleCallback),null!==this._ptr_aacCallback&&Module.removeFunction(this._ptr_aacCallback),this._ptr_probeCallback=null,this._ptr_frameCallback=null,this._ptr_naluCallback=null,this._ptr_sampleCallback=null,this._ptr_aacCallback=null}},{key:"release",value:function(){return this.pause(),this.NaluBuf.length=0,this.YuvBuf.length=0,void 0!==this.workerFetch&&null!==this.workerFetch&&this.workerFetch.postMessage({cmd:"stop",data:"stop",msg:"stop"}),this.workerFetch=null,this.AVGetInterval&&clearInterval(this.AVGetInterval),this.AVGetInterval=null,this._removeBindFuncPtr(),void 0!==this.corePtr&&null!==this.corePtr&&Module.cwrap("releaseHttpFLV","number",["number"])(this.corePtr),this.playInterval&&clearInterval(this.playInterval),this.playInterval=null,this.audioWAudio&&this.audioWAudio.stop(),this.audioWAudio=null,void 0!==this.AVGLObj&&null!==this.AVGLObj&&(r.releaseContext(this.AVGLObj),this.AVGLObj=null),this.CanvasObj&&this.CanvasObj.remove(),this.CanvasObj=null,window.onclick=document.body.onclick=null,0}},{key:"isPlayingState",value:function(){return null!==this.playInterval&&void 0!==this.playInterval}},{key:"pause",value:function(){this.audioWAudio&&this.audioWAudio.pause(),this.playInterval&&clearInterval(this.playInterval),this.playInterval=null}},{key:"playYUV",value:function(){if(this.YuvBuf.length>0){var e=this.YuvBuf.shift();return this.onRender&&this.onRender(e.line_y,e.h,e.bufY,e.bufU,e.bufV),r.renderFrame(this.AVGLObj,e.bufY,e.bufU,e.bufV,e.line_y,e.h),!0}return!1}},{key:"play",value:function(){var e=this,t=this;if(!1===this.checkCacheState())return this.onLoadCache&&this.onLoadCache(),setTimeout((function(){e.play()}),100),!1;if(void 0===this.playInterval||null===this.playInterval){var i=0,n=0,s=0;!1===this.mediaInfo.audioNone&&this.audioWAudio&&!1===this.mediaInfo.noFPS?(this.playInterval=setInterval((function(){if(n=a.GetMsTime(),t.cache_status){if(n-i>=t.frameTime-s){var e=t.YuvBuf.shift();if(null!=e&&null!==e){var o=0;null!==t.audioWAudio&&void 0!==t.audioWAudio&&(o=1e3*(e.pts-t.audioWAudio.getAlignVPTS())),s=t.audioWAudio?o<0&&-1*o<=t.frameTime||o>=0?a.GetMsTime()-n+1:t.frameTime:a.GetMsTime()-n+1,t.showScreen&&t.onRender&&t.onRender(e.line_y,e.h,e.bufY,e.bufU,e.bufV),e.pts,r.renderFrame(t.AVGLObj,e.bufY,e.bufU,e.bufV,e.line_y,e.h)}(t.YuvBuf.length<=0||t.audioWAudio&&t.audioWAudio.sampleQueue.length<=0)&&(t.cache_status=!1,t.onLoadCache&&t.onLoadCache(),t.audioWAudio&&t.audioWAudio.pause()),i=n}}else s=t.frameTime}),1),this.audioWAudio&&this.audioWAudio.play()):this.playInterval=setInterval((function(){var e=t.YuvBuf.shift();null!=e&&null!==e&&(t.showScreen&&t.onRender&&t.onRender(e.line_y,e.h,e.bufY,e.bufU,e.bufV),r.renderFrame(t.AVGLObj,e.bufY,e.bufU,e.bufV,e.line_y,e.h)),t.YuvBuf.length<=0&&(t.cache_status=!1)}),t.frameTime)}}},{key:"start",value:function(e){var t=this;this.workerFetch=new Worker(a.GetScriptPath((function(){var e=null;self,self.onmessage=function(t){var i=t.data;switch(void 0===i.cmd||null===i.cmd?"":i.cmd){case"start":var n=i.data;(e=new WebSocket(n)).binaryType="arraybuffer",e.onopen=function(t){e.send("Hello WebSockets!")},e.onmessage=function(e){if(e.data instanceof ArrayBuffer){var t=e.data;t.byteLength>0&&postMessage({cmd:"fetch-chunk",data:new Uint8Array(t),msg:"fetch-chunk"})}},e.onclose=function(e){};break;case"stop":e&&e.close(),self.close(),self.postMessage({cmd:"close",data:"close",msg:"close"})}}}))),this.workerFetch.onmessage=function(e){t._workerFetch_onmessage(e,t)},this.workerFetch,this._ptr_probeCallback=Module.addFunction(this._callbackProbe.bind(this)),this._ptr_yuvCallback=Module.addFunction(this._callbackYUV.bind(this)),this._ptr_naluCallback=Module.addFunction(this._callbackNALU.bind(this)),this._ptr_sampleCallback=Module.addFunction(this._callbackPCM.bind(this)),this._ptr_aacCallback=Module.addFunction(this._callbackAAC.bind(this)),Module.cwrap("initializeSniffHttpFlvModule","number",["number","number","number","number","number","number"])(this.corePtr,this._ptr_probeCallback,this._ptr_yuvCallback,this._ptr_naluCallback,this._ptr_sampleCallback,this._ptr_aacCallback),this.AVGLObj=r.setupCanvas(this.CanvasObj,{preserveDrawingBuffer:!1}),this.workerFetch.postMessage({cmd:"start",data:e,msg:"start"}),this._decode()}}])&&n(t.prototype,i),u&&n(t,u),e}());i.CWsLiveCore=u},{"../consts":52,"../demuxer/buffer":66,"../demuxer/bufferFrame":67,"../render-engine/webgl-420p":81,"../version":84,"./audio-core":54,"./audio-native-core":55,"./av-common":56,"./cache":61,"./cacheYuv":62}],61:[function(e,t,i){(function(i){"use strict";e("./cacheYuv");i.CACHE_APPEND_STATUS_CODE={FAILED:-1,OVERFLOW:-2,OK:0,NOT_FULL:1,FULL:2,NULL:3},t.exports=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:60,t={limit:e,yuvCache:[],appendCacheByCacheYuv:function(e){e.pts;return t.yuvCache.length>=t.limit?CACHE_APPEND_STATUS_CODE.OVERFLOW:(t.yuvCache.push(e),t.yuvCache.length>=t.limit?CACHE_APPEND_STATUS_CODE.FULL:CACHE_APPEND_STATUS_CODE.NOT_FULL)},getState:function(){return t.yuvCache.length<=0?CACHE_APPEND_STATUS_CODE.NULL:t.yuvCache.length>=t.limit?CACHE_APPEND_STATUS_CODE.FULL:CACHE_APPEND_STATUS_CODE.NOT_FULL},cleanPipeline:function(){t.yuvCache.length=0},vYuv:function(){return t.yuvCache.length<=0?null:t.yuvCache.shift()}};return t}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./cacheYuv":62}],62:[function(e,t,i){"use strict";function n(e,t){for(var i=0;i>1;return r.indexOf(t)},GET_NALU_TYPE:function(e){var t=(126&e)>>1;if(t>=1&&t<=9)return n.DEFINE_P_FRAME;if(t>=16&&t<=21)return n.DEFINE_KEY_FRAME;var i=r.indexOf(t);return i>=0?r[i]:n.DEFINE_OTHERS_FRAME},PACK_NALU:function(e){var t=e.nalu,i=e.vlc.vlc;null==t.vps&&(t.vps=new Uint8Array);var n=new Uint8Array(t.vps.length+t.sps.length+t.pps.length+t.sei.length+i.length);return n.set(t.vps,0),n.set(t.sps,t.vps.length),n.set(t.pps,t.vps.length+t.sps.length),n.set(t.sei,t.vps.length+t.sps.length+t.pps.length),n.set(i,t.vps.length+t.sps.length+t.pps.length+t.sei.length),n}}},{"./hevc-header":63}],65:[function(e,t,i){"use strict";function n(e){return function(e){if(Array.isArray(e)){for(var t=0,i=new Array(e.length);t0&&void 0!==arguments[0]&&arguments[0];null!=t&&(t.showScreen=e)},setSize:function(e,i){t.config.width=e||l.DEFAULT_WIDTH,t.config.height=i||l.DEFAULT_HEIGHT},setFrameRate:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:25;t.config.fps=e,t.config.frameDurMs=1e3/e},setDurationMs:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:-1;t.durationMs=e,0==t.config.audioNone&&t.audio.setDurationMs(e)},setPlayingCall:function(e){t.onPlayingTime=e},setVoice:function(e){t.realVolume=e,0==t.config.audioNone&&t.audio.setVoice(t.realVolume)},isPlayingState:function(){return t.isPlaying||t.isCaching===l.CACHE_WITH_PLAY_SIGN},appendAACFrame:function(e){t.audio.addSample(e),t.aCachePTS=Math.max(e.pts,t.aCachePTS)},appendHevcFrame:function(e){var i;t.config.appendHevcType==l.APPEND_TYPE_STREAM?t.stream=new Uint8Array((i=n(t.stream)).concat.apply(i,n(e))):t.config.appendHevcType==l.APPEND_TYPE_FRAME&&(t.frameList.push(e),t.vCachePTS=Math.max(e.pts,t.vCachePTS))},getCachePTS:function(){return Math.max(t.vCachePTS,t.aCachePTS)},endAudio:function(){0==t.config.audioNone&&t.audio.stop()},cleanSample:function(){0==t.config.audioNone&&t.audio.cleanQueue()},cleanVideoQueue:function(){t.config.appendHevcType==l.APPEND_TYPE_STREAM?t.stream=new Uint8Array:t.config.appendHevcType==l.APPEND_TYPE_FRAME&&(t.frameList=[],t.frameList.length=0)},cleanCacheYUV:function(){t.cacheYuvBuf.cleanPipeline()},pause:function(){t.loop&&window.clearInterval(t.loop),t.loop=null,0==t.config.audioNone&&t.audio.pause(),t.isPlaying=!1,t.isCaching===l.CACHE_WITH_PLAY_SIGN&&(t.isCaching=l.CACHE_WITH_NOPLAY_SIGN)},checkFinished:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:l.PLAYER_MODE_VOD;return e==l.PLAYER_MODE_VOD&&t.cacheYuvBuf.yuvCache.length<=0&&(t.videoPTS.toFixed(1)>=(t.durationMs-t.config.frameDurMs)/1e3||t.noCacheFrame>=10)&&(null!=t.onPlayingFinish&&(l.PLAYER_MODE_VOD,t.frameList.length,t.cacheYuvBuf.yuvCache.length,t.videoPTS.toFixed(1),t.durationMs,t.config.frameDurMs,t.noCacheFrame,t.onPlayingFinish()),!0)},clearAllCache:function(){t.nowPacket=null,t.vCachePTS=0,t.aCachePTS=0,t.cleanSample(),t.cleanVideoQueue(),t.cleanCacheYUV()},seek:function(e){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.isPlaying;t.pause(),t.stopCacheThread(),t.clearAllCache(),e&&e(),t.isNewSeek=!0,t.flushDecoder=1,t.videoPTS=parseInt(i.seekTime);var r={seekPos:i.seekTime||-1,mode:i.mode||l.PLAYER_MODE_VOD,accurateSeek:i.accurateSeek||!0,seekEvent:i.seekEvent||!0,realPlay:n};t.cacheThread(),t.play(r)},getNalu1Packet:function(){var e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0],i=null,n=-1;if(t.config.appendHevcType==l.APPEND_TYPE_STREAM)i=t.nextNalu();else{if(t.config.appendHevcType!=l.APPEND_TYPE_FRAME)return null;var r=t.frameList.shift();if(!r)return null;i=r.data,n=r.pts,e&&(t.videoPTS=n)}return{nalBuf:i,pts:n}},decodeNalu1Frame:function(e,i){var n=Module._malloc(e.length);Module.HEAP8.set(e,n);var r=parseInt(1e3*i);Module.cwrap("decodeCodecContext","number",["number","number","number","number","number"])(t.vcodecerPtr,n,e.length,r,t.flushDecoder);return t.flushDecoder=0,Module._free(n),n=null,!1},cacheThread:function(){t.cacheLoop=window.setInterval((function(){if(t.cacheYuvBuf.getState()!=CACHE_APPEND_STATUS_CODE.FULL){var e=t.getNalu1Packet(!1);if(null!=e){var i=e.nalBuf,n=e.pts;t.decodeNalu1Frame(i,n,!0)}}}),10)},stopCacheThread:function(){null!==t.cacheLoop&&(window.clearInterval(t.cacheLoop),t.cacheLoop=null)},loadCache:function(){if(!(t.frameList.length<=3)){var e=t.isPlaying;if(t.cacheYuvBuf.yuvCache.length<=3){t.pause(),null!=t.onLoadCache&&t.onLoadCache(),t.isCaching=e?l.CACHE_WITH_PLAY_SIGN:l.CACHE_WITH_NOPLAY_SIGN;var i=t.frameList.length>30?30:t.frameList.length;null===t.cacheInterval&&(t.cacheInterval=window.setInterval((function(){t.cacheYuvBuf.yuvCache.length>=i&&(null!=t.onLoadCacheFinshed&&t.onLoadCacheFinshed(),window.clearInterval(t.cacheInterval),t.cacheInterval=null,t.isCaching===l.CACHE_WITH_PLAY_SIGN&&t.play(t.playParams),t.isCaching=l.CACHE_NO_LOADCACHE)}),40))}}},playFunc:function(){var e=!1;if(t.playParams.seekEvent||r.GetMsTime()-t.calcuteStartTime>=t.frameTime-t.preCostTime){e=!0;var i=!0;if(t.calcuteStartTime=r.GetMsTime(),t.config.audioNone)t.playFrameYUV(i,t.playParams.accurateSeek);else{t.fix_poc_err_skip>0&&(t.fix_poc_err_skip--,i=!1);var n=t.videoPTS-t.audio.getAlignVPTS();if(n>0)return void(t.playParams.seekEvent&&!t.config.audioNone&&t.audio.setVoice(0));if(i){if(!(i=-1*n<=1*t.frameTimeSec)){for(var a=parseInt(n/t.frameTimeSec),s=0;s=i&&(t.playFrameYUV(!0,t.playParams.accurateSeek),i+=1)}),1)}else t.videoPTS>=t.playParams.seekPos&&!t.isNewSeek||0===t.playParams.seekPos||0===t.playParams.seekPos?(t.frameTime=1e3/t.config.fps,t.frameTimeSec=t.frameTime/1e3,0==t.config.audioNone&&t.audio.play(),t.realVolume=t.config.audioNone?0:t.audio.voice,t.playParams.seekEvent&&(t.fix_poc_err_skip=10),t.loop=window.setInterval((function(){var e=r.GetMsTime();t.playFunc(),t.preCostTime=r.GetMsTime()-e}),1)):(t.loop=window.setInterval((function(){t.playFrameYUV(!1,t.playParams.accurateSeek),t.checkFinished(t.playParams.mode)?(window.clearInterval(t.loop),t.loop=null):t.videoPTS>=t.playParams.seekPos&&(window.clearInterval(t.loop),t.loop=null,t.play(t.playParams))}),1),t.isNewSeek=!1)},stop:function(){t.release(),Module.cwrap("initializeDecoder","number",["number"])(t.vcodecerPtr),t.stream=new Uint8Array},release:function(){return void 0!==t.yuv&&null!==t.yuv&&(u.releaseContext(t.yuv),t.yuv=null),t.endAudio(),t.cacheLoop&&window.clearInterval(t.cacheLoop),t.cacheLoop=null,t.loop&&window.clearInterval(t.loop),t.loop=null,t.pause(),null!==t.videoCallback&&Module.removeFunction(t.videoCallback),t.videoCallback=null,Module.cwrap("release","number",["number"])(t.vcodecerPtr),t.stream=null,t.frameList.length=0,t.durationMs=-1,t.videoPTS=0,t.isPlaying=!1,t.canvas.remove(),t.canvas=null,window.onclick=document.body.onclick=null,!0},nextNalu:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;if(t.stream.length<=4)return!1;for(var i=-1,n=0;n=t.stream.length){if(-1==i)return!1;var r=t.stream.subarray(i);return t.stream=new Uint8Array,r}var a="0 0 1"==t.stream.slice(0,3).join(" "),s="0 0 0 1"==t.stream.slice(0,4).join(" ");if(a||s){if(-1==i)i=n;else{if(e<=1){var o=t.stream.subarray(i,n);return t.stream=t.stream.subarray(n),o}e-=1}n+=3}}return!1},decodeSendPacket:function(e){var i=Module._malloc(e.length);Module.HEAP8.set(e,i);var n=Module.cwrap("decodeSendPacket","number",["number","number","number"])(t.vcodecerPtr,i,e.length);return Module._free(i),n},decodeRecvFrame:function(){return Module.cwrap("decodeRecv","number",["number"])(t.vcodecerPtr)},playYUV:function(){return t.playFrameYUV(!0,!0)},playFrameYUV:function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0],i=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=t.cacheYuvBuf.vYuv();if(null==n)return t.noCacheFrame+=1,e&&!t.playParams.seekEvent&&t.loadCache(),!1;t.noCacheFrame=0;var r=n.pts;return t.videoPTS=r,(!e&&i||e)&&e&&(t.onRender(n.width,n.height,n.imageBufferY,n.imageBufferB,n.imageBufferR),t.drawImage(n.width,n.height,n.imageBufferY,n.imageBufferB,n.imageBufferR)),e&&!t.playParams.seekEvent&&t.isPlaying&&t.loadCache(),!0},drawImage:function(e,i,n,r,a){if(t.canvas.width===e&&t.canvas.height==i||(t.canvas.width=e,t.canvas.height=i),t.showScreen&&null!=t.onRender&&t.onRender(e,i,n,r,a),!t.isCheckDisplay)t.checkDisplaySize(e,i);var s=e*i,o=e/2*(i/2),l=new Uint8Array(s+2*o);l.set(n,0),l.set(r,s),l.set(a,s+o),u.renderFrame(t.yuv,n,r,a,e,i)},debugYUV:function(e){t.debugYUVSwitch=!0,t.debugID=e},checkDisplaySize:function(e,i){var n=e/t.config.width>i/t.config.height,r=(t.config.width/e).toFixed(2),a=(t.config.height/i).toFixed(2),s=n?r:a,o=t.config.fixed,u=o?t.config.width:parseInt(e*s),l=o?t.config.height:parseInt(i*s);if(t.canvas.offsetWidth!=u||t.canvas.offsetHeight!=l){var h=parseInt((t.canvasBox.offsetHeight-l)/2),d=parseInt((t.canvasBox.offsetWidth-u)/2);t.canvas.style.marginTop=h+"px",t.canvas.style.marginLeft=d+"px",t.canvas.style.width=u+"px",t.canvas.style.height=l+"px"}return t.isCheckDisplay=!0,[u,l]},makeWasm:function(){if(null!=t.config.token){t.vcodecerPtr=Module.cwrap("registerPlayer","number",["string","string"])(t.config.token,h.PLAYER_VERSION),t.videoCallback=Module.addFunction((function(e,i,n,r,a,s,u,l,h){var d=Module.HEAPU8.subarray(e,e+r*l),c=Module.HEAPU8.subarray(i,i+a*l/2),f=Module.HEAPU8.subarray(n,n+s*l/2),p=new Uint8Array(d),m=new Uint8Array(c),_=new Uint8Array(f),g=1*h/1e3,v=new o.CacheYuvStruct(g,r,l,p,m,_);Module._free(d),d=null,Module._free(c),c=null,Module._free(f),f=null,t.cacheYuvBuf.appendCacheByCacheYuv(v)})),Module.cwrap("setCodecType","number",["number","number","number"])(t.vcodecerPtr,t.config.videoCodec,t.videoCallback);Module.cwrap("initializeDecoder","number",["number"])(t.vcodecerPtr)}},makeIt:function(){var e=document.querySelector("div#"+t.config.playerId),i=document.createElement("canvas");i.style.width=e.clientWidth+"px",i.style.height=e.clientHeight+"px",i.style.top="0px",i.style.left="0px",e.appendChild(i),t.canvasBox=e,t.canvas=i,t.yuv=u.setupCanvas(i,{preserveDrawingBuffer:!1}),0==t.config.audioNone&&(t.audio=a({sampleRate:t.config.sampleRate,appendType:t.config.appendHevcType})),t.isPlayLoadingFinish=1}};return t.makeWasm(),t.makeIt(),t.cacheThread(),t}},{"../consts":52,"../render-engine/webgl-420p":81,"../version":84,"./audio-core":54,"./av-common":56,"./cache":61,"./cacheYuv":62}],66:[function(e,t,i){"use strict";var n=e("./bufferFrame");t.exports=function(){var e={videoBuffer:[],audioBuffer:[],idrIdxBuffer:[],appendFrame:function(t,i){var r=!(arguments.length>2&&void 0!==arguments[2])||arguments[2],a=arguments.length>3&&void 0!==arguments[3]&&arguments[3],s=new n.BufferFrame(t,a,i,r),o=parseInt(t);return r?(e.videoBuffer.length-1>=o?e.videoBuffer[o].push(s):e.videoBuffer.push([s]),a&&!e.idrIdxBuffer.includes(t)&&e.idrIdxBuffer.push(t)):e.audioBuffer.length-1>=o&&null!=e.audioBuffer[o]&&null!=e.audioBuffer[o]?e.audioBuffer[o]&&e.audioBuffer[o].push(s):e.audioBuffer.push([s]),!0},appendFrameWithDts:function(t,i,r){var a=!(arguments.length>3&&void 0!==arguments[3])||arguments[3],s=arguments.length>4&&void 0!==arguments[4]&&arguments[4],o=n.ConstructWithDts(t,i,s,r,a),u=parseInt(i);return a?(e.videoBuffer.length-1>=u?e.videoBuffer[u].push(o):e.videoBuffer.push([o]),s&&!e.idrIdxBuffer.includes(i)&&e.idrIdxBuffer.push(i)):e.audioBuffer.length-1>=u&&null!=e.audioBuffer[u]&&null!=e.audioBuffer[u]?e.audioBuffer[u]&&e.audioBuffer[u].push(o):e.audioBuffer.push([o]),e.videoBuffer,e.idrIdxBuffer,!0},appendFrameByBufferFrame:function(t){var i=t.pts,n=parseInt(i);return t.video?(e.videoBuffer.length-1>=n?e.videoBuffer[n].push(t):e.videoBuffer.push([t]),isKey&&!e.idrIdxBuffer.includes(i)&&e.idrIdxBuffer.push(i)):e.audioBuffer.length-1>=n?e.audioBuffer[n].push(t):e.audioBuffer.push([t]),!0},cleanPipeline:function(){e.videoBuffer.length=0,e.audioBuffer.length=0},vFrame:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:-1;if(!(t<0||t>e.videoBuffer.length-1))return e.videoBuffer[t]},aFrame:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:-1;if(!(t<0||t>e.audioBuffer.length-1))return e.audioBuffer[t]},seekIDR:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:-1;if(e.idrIdxBuffer,e.videoBuffer,t<0)return null;if(e.idrIdxBuffer.includes(t))return t;for(var i=0;it||0===i&&e.idrIdxBuffer[i]>=t){for(var n=1;n>=0;n--){var r=i-n;if(r>=0)return e.idrIdxBuffer[r],e.idrIdxBuffer[r]}return e.idrIdxBuffer[i],j,e.idrIdxBuffer[i]}}};return e}},{"./bufferFrame":67}],67:[function(e,t,i){"use strict";function n(e,t){for(var i=0;i0&&s.length>r&&!s.warned){s.warned=!0;var o=new Error("Possible EventEmitter memory leak detected. "+s.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");o.name="MaxListenersExceededWarning",o.emitter=e,o.type=t,o.count=s.length,console&&console.warn}return e}function d(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function c(e,t,i){var n={fired:!1,wrapFn:void 0,target:e,type:t,listener:i},r=d.bind(n);return r.listener=i,n.wrapFn=r,r}function f(e,t,i){var n=e._events;if(void 0===n)return[];var r=n[t];return void 0===r?[]:"function"==typeof r?i?[r.listener||r]:[r]:i?function(e){for(var t=new Array(e.length),i=0;i0&&(s=t[0]),s instanceof Error)throw s;var o=new Error("Unhandled error."+(s?" ("+s.message+")":""));throw o.context=s,o}var u=a[e];if(void 0===u)return!1;if("function"==typeof u)r(u,this,t);else{var l=u.length,h=m(u,l);for(i=0;i=0;a--)if(i[a]===t||i[a].listener===t){s=i[a].listener,r=a;break}if(r<0)return this;0===r?i.shift():function(e,t){for(;t+1=0;n--)this.removeListener(e,t[n]);return this},s.prototype.listeners=function(e){return f(this,e,!0)},s.prototype.rawListeners=function(e){return f(this,e,!1)},s.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):p.call(e,t)},s.prototype.listenerCount=p,s.prototype.eventNames=function(){return this._eventsCount>0?t(this._events):[]}},"./node_modules/webworkify-webpack/index.js": +/*!**************************************************!*\ + !*** ./node_modules/webworkify-webpack/index.js ***! + \**************************************************/ +function(e,t,i){function n(e){var t={};function i(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.m=e,i.c=t,i.i=function(e){return e},i.d=function(e,t,n){i.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:n})},i.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/",i.oe=function(e){throw console.error(e),e};var n=i(i.s=ENTRY_MODULE);return n.default||n}function r(e){return(e+"").replace(/[.?*+^$[\]\\(){}|-]/g,"\\$&")}function a(e,t,n){var a={};a[n]=[];var s=t.toString(),o=s.match(/^function\s?\w*\(\w+,\s*\w+,\s*(\w+)\)/);if(!o)return a;for(var u,l=o[1],h=new RegExp("(\\\\n|\\W)"+r(l)+"\\(\\s*(/\\*.*?\\*/)?\\s*.*?([\\.|\\-|\\+|\\w|/|@]+).*?\\)","g");u=h.exec(s);)"dll-reference"!==u[3]&&a[n].push(u[3]);for(h=new RegExp("\\("+r(l)+'\\("(dll-reference\\s([\\.|\\-|\\+|\\w|/|@]+))"\\)\\)\\(\\s*(/\\*.*?\\*/)?\\s*.*?([\\.|\\-|\\+|\\w|/|@]+).*?\\)',"g");u=h.exec(s);)e[u[2]]||(a[n].push(u[1]),e[u[2]]=i(u[1]).m),a[u[2]]=a[u[2]]||[],a[u[2]].push(u[4]);for(var d,c=Object.keys(a),f=0;f0}),!1)}e.exports=function(e,t){t=t||{};var r={main:i.m},o=t.all?{main:Object.keys(r.main)}:function(e,t){for(var i={main:[t]},n={main:[]},r={main:{}};s(i);)for(var o=Object.keys(i),u=0;u=e[r]&&t0&&e[0].originalDts=t[r].dts&&et[n].lastSample.originalDts&&e=t[n].lastSample.originalDts&&(n===t.length-1||n0&&(r=this._searchNearestSegmentBefore(i.originalBeginDts)+1),this._lastAppendLocation=r,this._list.splice(r,0,i)},e.prototype.getLastSegmentBefore=function(e){var t=this._searchNearestSegmentBefore(e);return t>=0?this._list[t]:null},e.prototype.getLastSampleBefore=function(e){var t=this.getLastSegmentBefore(e);return null!=t?t.lastSample:null},e.prototype.getLastSyncPointBefore=function(e){for(var t=this._searchNearestSegmentBefore(e),i=this._list[t].syncPoints;0===i.length&&t>0;)t--,i=this._list[t].syncPoints;return i.length>0?i[i.length-1]:null},e}()},"./src/core/mse-controller.js": +/*!************************************!*\ + !*** ./src/core/mse-controller.js ***! + \************************************/ +function(e,t,i){i.r(t);var n=i( +/*! events */ +"./node_modules/events/events.js"),r=i.n(n),a=i( +/*! ../utils/logger.js */ +"./src/utils/logger.js"),s=i( +/*! ../utils/browser.js */ +"./src/utils/browser.js"),o=i( +/*! ./mse-events.js */ +"./src/core/mse-events.js"),u=i( +/*! ./media-segment-info.js */ +"./src/core/media-segment-info.js"),l=i( +/*! ../utils/exception.js */ +"./src/utils/exception.js"),h=function(){function e(e){this.TAG="MSEController",this._config=e,this._emitter=new(r()),this._config.isLive&&null==this._config.autoCleanupSourceBuffer&&(this._config.autoCleanupSourceBuffer=!0),this.e={onSourceOpen:this._onSourceOpen.bind(this),onSourceEnded:this._onSourceEnded.bind(this),onSourceClose:this._onSourceClose.bind(this),onSourceBufferError:this._onSourceBufferError.bind(this),onSourceBufferUpdateEnd:this._onSourceBufferUpdateEnd.bind(this)},this._mediaSource=null,this._mediaSourceObjectURL=null,this._mediaElement=null,this._isBufferFull=!1,this._hasPendingEos=!1,this._requireSetMediaDuration=!1,this._pendingMediaDuration=0,this._pendingSourceBufferInit=[],this._mimeTypes={video:null,audio:null},this._sourceBuffers={video:null,audio:null},this._lastInitSegments={video:null,audio:null},this._pendingSegments={video:[],audio:[]},this._pendingRemoveRanges={video:[],audio:[]},this._idrList=new u.IDRSampleList}return e.prototype.destroy=function(){(this._mediaElement||this._mediaSource)&&this.detachMediaElement(),this.e=null,this._emitter.removeAllListeners(),this._emitter=null},e.prototype.on=function(e,t){this._emitter.addListener(e,t)},e.prototype.off=function(e,t){this._emitter.removeListener(e,t)},e.prototype.attachMediaElement=function(e){if(this._mediaSource)throw new l.IllegalStateException("MediaSource has been attached to an HTMLMediaElement!");var t=this._mediaSource=new window.MediaSource;t.addEventListener("sourceopen",this.e.onSourceOpen),t.addEventListener("sourceended",this.e.onSourceEnded),t.addEventListener("sourceclose",this.e.onSourceClose),this._mediaElement=e,this._mediaSourceObjectURL=window.URL.createObjectURL(this._mediaSource),e.src=this._mediaSourceObjectURL},e.prototype.detachMediaElement=function(){if(this._mediaSource){var e=this._mediaSource;for(var t in this._sourceBuffers){var i=this._pendingSegments[t];i.splice(0,i.length),this._pendingSegments[t]=null,this._pendingRemoveRanges[t]=null,this._lastInitSegments[t]=null;var n=this._sourceBuffers[t];if(n){if("closed"!==e.readyState){try{e.removeSourceBuffer(n)}catch(e){a.default.e(this.TAG,e.message)}n.removeEventListener("error",this.e.onSourceBufferError),n.removeEventListener("updateend",this.e.onSourceBufferUpdateEnd)}this._mimeTypes[t]=null,this._sourceBuffers[t]=null}}if("open"===e.readyState)try{e.endOfStream()}catch(e){a.default.e(this.TAG,e.message)}e.removeEventListener("sourceopen",this.e.onSourceOpen),e.removeEventListener("sourceended",this.e.onSourceEnded),e.removeEventListener("sourceclose",this.e.onSourceClose),this._pendingSourceBufferInit=[],this._isBufferFull=!1,this._idrList.clear(),this._mediaSource=null}this._mediaElement&&(this._mediaElement.src="",this._mediaElement.removeAttribute("src"),this._mediaElement=null),this._mediaSourceObjectURL&&(window.URL.revokeObjectURL(this._mediaSourceObjectURL),this._mediaSourceObjectURL=null)},e.prototype.appendInitSegment=function(e,t){if(!this._mediaSource||"open"!==this._mediaSource.readyState)return this._pendingSourceBufferInit.push(e),void this._pendingSegments[e.type].push(e);var i=e,n=""+i.container;i.codec&&i.codec.length>0&&(n+=";codecs="+i.codec);var r=!1;if(a.default.v(this.TAG,"Received Initialization Segment, mimeType: "+n),this._lastInitSegments[i.type]=i,n!==this._mimeTypes[i.type]){if(this._mimeTypes[i.type])a.default.v(this.TAG,"Notice: "+i.type+" mimeType changed, origin: "+this._mimeTypes[i.type]+", target: "+n);else{r=!0;try{var u=this._sourceBuffers[i.type]=this._mediaSource.addSourceBuffer(n);u.addEventListener("error",this.e.onSourceBufferError),u.addEventListener("updateend",this.e.onSourceBufferUpdateEnd)}catch(e){return a.default.e(this.TAG,e.message),void this._emitter.emit(o.default.ERROR,{code:e.code,msg:e.message})}}this._mimeTypes[i.type]=n}t||this._pendingSegments[i.type].push(i),r||this._sourceBuffers[i.type]&&!this._sourceBuffers[i.type].updating&&this._doAppendSegments(),s.default.safari&&"audio/mpeg"===i.container&&i.mediaDuration>0&&(this._requireSetMediaDuration=!0,this._pendingMediaDuration=i.mediaDuration/1e3,this._updateMediaSourceDuration())},e.prototype.appendMediaSegment=function(e){var t=e;this._pendingSegments[t.type].push(t),this._config.autoCleanupSourceBuffer&&this._needCleanupSourceBuffer()&&this._doCleanupSourceBuffer();var i=this._sourceBuffers[t.type];!i||i.updating||this._hasPendingRemoveRanges()||this._doAppendSegments()},e.prototype.seek=function(e){for(var t in this._sourceBuffers)if(this._sourceBuffers[t]){var i=this._sourceBuffers[t];if("open"===this._mediaSource.readyState)try{i.abort()}catch(e){a.default.e(this.TAG,e.message)}this._idrList.clear();var n=this._pendingSegments[t];if(n.splice(0,n.length),"closed"!==this._mediaSource.readyState){for(var r=0;r=1&&e-n.start(0)>=this._config.autoCleanupMaxBackwardDuration)return!0}}return!1},e.prototype._doCleanupSourceBuffer=function(){var e=this._mediaElement.currentTime;for(var t in this._sourceBuffers){var i=this._sourceBuffers[t];if(i){for(var n=i.buffered,r=!1,a=0;a=this._config.autoCleanupMaxBackwardDuration){r=!0;var u=e-this._config.autoCleanupMinBackwardDuration;this._pendingRemoveRanges[t].push({start:s,end:u})}}else o0&&(isNaN(t)||i>t)&&(a.default.v(this.TAG,"Update MediaSource duration from "+t+" to "+i),this._mediaSource.duration=i),this._requireSetMediaDuration=!1,this._pendingMediaDuration=0}},e.prototype._doRemoveRanges=function(){for(var e in this._pendingRemoveRanges)if(this._sourceBuffers[e]&&!this._sourceBuffers[e].updating)for(var t=this._sourceBuffers[e],i=this._pendingRemoveRanges[e];i.length&&!t.updating;){var n=i.shift();t.remove(n.start,n.end)}},e.prototype._doAppendSegments=function(){var e=this._pendingSegments;for(var t in e)if(this._sourceBuffers[t]&&!this._sourceBuffers[t].updating&&e[t].length>0){var i=e[t].shift();if(i.timestampOffset){var n=this._sourceBuffers[t].timestampOffset,r=i.timestampOffset/1e3;Math.abs(n-r)>.1&&(a.default.v(this.TAG,"Update MPEG audio timestampOffset from "+n+" to "+r),this._sourceBuffers[t].timestampOffset=r),delete i.timestampOffset}if(!i.data||0===i.data.byteLength)continue;try{this._sourceBuffers[t].appendBuffer(i.data),this._isBufferFull=!1,"video"===t&&i.hasOwnProperty("info")&&this._idrList.appendArray(i.info.syncPoints)}catch(e){this._pendingSegments[t].unshift(i),22===e.code?(this._isBufferFull||this._emitter.emit(o.default.BUFFER_FULL),this._isBufferFull=!0):(a.default.e(this.TAG,t,e.message),this._emitter.emit(o.default.ERROR,{code:e.code,msg:e.message}))}}},e.prototype._onSourceOpen=function(){if(a.default.v(this.TAG,"MediaSource onSourceOpen"),this._mediaSource.removeEventListener("sourceopen",this.e.onSourceOpen),this._pendingSourceBufferInit.length>0)for(var e=this._pendingSourceBufferInit;e.length;){var t=e.shift();this.appendInitSegment(t,!0)}this._hasPendingSegments()&&this._doAppendSegments(),this._emitter.emit(o.default.SOURCE_OPEN)},e.prototype._onSourceEnded=function(){a.default.v(this.TAG,"MediaSource onSourceEnded")},e.prototype._onSourceClose=function(){a.default.v(this.TAG,"MediaSource onSourceClose"),this._mediaSource&&null!=this.e&&(this._mediaSource.removeEventListener("sourceopen",this.e.onSourceOpen),this._mediaSource.removeEventListener("sourceended",this.e.onSourceEnded),this._mediaSource.removeEventListener("sourceclose",this.e.onSourceClose))},e.prototype._hasPendingSegments=function(){var e=this._pendingSegments;return(e.video&&e.video.length)>0||e.audio&&e.audio.length>0},e.prototype._hasPendingRemoveRanges=function(){var e=this._pendingRemoveRanges;return(e.video&&e.video.length)>0||e.audio&&e.audio.length>0},e.prototype._onSourceBufferUpdateEnd=function(){this._requireSetMediaDuration?this._updateMediaSourceDuration():this._hasPendingRemoveRanges()?this._doRemoveRanges():this._hasPendingSegments()?this._doAppendSegments():this._hasPendingEos&&this.endOfStream(),this._emitter.emit(o.default.UPDATE_END)},e.prototype._onSourceBufferError=function(e){a.default.e(this.TAG,"SourceBuffer Error: "+e)},e}();t.default=h},"./src/core/mse-events.js": +/*!********************************!*\ + !*** ./src/core/mse-events.js ***! + \********************************/ +function(e,t,i){i.r(t),t.default={ERROR:"error",SOURCE_OPEN:"source_open",UPDATE_END:"update_end",BUFFER_FULL:"buffer_full"}},"./src/core/transmuxer.js": +/*!********************************!*\ + !*** ./src/core/transmuxer.js ***! + \********************************/ +function(e,t,i){i.r(t);var n=i( +/*! events */ +"./node_modules/events/events.js"),r=i.n(n),a=i( +/*! webworkify-webpack */ +"./node_modules/webworkify-webpack/index.js"),s=i.n(a),o=i( +/*! ../utils/logger.js */ +"./src/utils/logger.js"),u=i( +/*! ../utils/logging-control.js */ +"./src/utils/logging-control.js"),l=i( +/*! ./transmuxing-controller.js */ +"./src/core/transmuxing-controller.js"),h=i( +/*! ./transmuxing-events.js */ +"./src/core/transmuxing-events.js"),d=i( +/*! ./media-info.js */ +"./src/core/media-info.js"),c=function(){function e(e,t){if(this.TAG="Transmuxer",this._emitter=new(r()),t.enableWorker&&"undefined"!=typeof Worker)try{this._worker=s()( +/*! ./transmuxing-worker */ +"./src/core/transmuxing-worker.js"),this._workerDestroying=!1,this._worker.addEventListener("message",this._onWorkerMessage.bind(this)),this._worker.postMessage({cmd:"init",param:[e,t]}),this.e={onLoggingConfigChanged:this._onLoggingConfigChanged.bind(this)},u.default.registerListener(this.e.onLoggingConfigChanged),this._worker.postMessage({cmd:"logging_config",param:u.default.getConfig()})}catch(i){o.default.e(this.TAG,"Error while initialize transmuxing worker, fallback to inline transmuxing"),this._worker=null,this._controller=new l.default(e,t)}else this._controller=new l.default(e,t);if(this._controller){var i=this._controller;i.on(h.default.IO_ERROR,this._onIOError.bind(this)),i.on(h.default.DEMUX_ERROR,this._onDemuxError.bind(this)),i.on(h.default.INIT_SEGMENT,this._onInitSegment.bind(this)),i.on(h.default.MEDIA_SEGMENT,this._onMediaSegment.bind(this)),i.on(h.default.LOADING_COMPLETE,this._onLoadingComplete.bind(this)),i.on(h.default.RECOVERED_EARLY_EOF,this._onRecoveredEarlyEof.bind(this)),i.on(h.default.MEDIA_INFO,this._onMediaInfo.bind(this)),i.on(h.default.METADATA_ARRIVED,this._onMetaDataArrived.bind(this)),i.on(h.default.SCRIPTDATA_ARRIVED,this._onScriptDataArrived.bind(this)),i.on(h.default.STATISTICS_INFO,this._onStatisticsInfo.bind(this)),i.on(h.default.RECOMMEND_SEEKPOINT,this._onRecommendSeekpoint.bind(this))}}return e.prototype.destroy=function(){this._worker?this._workerDestroying||(this._workerDestroying=!0,this._worker.postMessage({cmd:"destroy"}),u.default.removeListener(this.e.onLoggingConfigChanged),this.e=null):(this._controller.destroy(),this._controller=null),this._emitter.removeAllListeners(),this._emitter=null},e.prototype.on=function(e,t){this._emitter.addListener(e,t)},e.prototype.off=function(e,t){this._emitter.removeListener(e,t)},e.prototype.hasWorker=function(){return null!=this._worker},e.prototype.open=function(){this._worker?this._worker.postMessage({cmd:"start"}):this._controller.start()},e.prototype.close=function(){this._worker?this._worker.postMessage({cmd:"stop"}):this._controller.stop()},e.prototype.seek=function(e){this._worker?this._worker.postMessage({cmd:"seek",param:e}):this._controller.seek(e)},e.prototype.pause=function(){this._worker?this._worker.postMessage({cmd:"pause"}):this._controller.pause()},e.prototype.resume=function(){this._worker?this._worker.postMessage({cmd:"resume"}):this._controller.resume()},e.prototype._onInitSegment=function(e,t){var i=this;Promise.resolve().then((function(){i._emitter.emit(h.default.INIT_SEGMENT,e,t)}))},e.prototype._onMediaSegment=function(e,t){var i=this;Promise.resolve().then((function(){i._emitter.emit(h.default.MEDIA_SEGMENT,e,t)}))},e.prototype._onLoadingComplete=function(){var e=this;Promise.resolve().then((function(){e._emitter.emit(h.default.LOADING_COMPLETE)}))},e.prototype._onRecoveredEarlyEof=function(){var e=this;Promise.resolve().then((function(){e._emitter.emit(h.default.RECOVERED_EARLY_EOF)}))},e.prototype._onMediaInfo=function(e){var t=this;Promise.resolve().then((function(){t._emitter.emit(h.default.MEDIA_INFO,e)}))},e.prototype._onMetaDataArrived=function(e){var t=this;Promise.resolve().then((function(){t._emitter.emit(h.default.METADATA_ARRIVED,e)}))},e.prototype._onScriptDataArrived=function(e){var t=this;Promise.resolve().then((function(){t._emitter.emit(h.default.SCRIPTDATA_ARRIVED,e)}))},e.prototype._onStatisticsInfo=function(e){var t=this;Promise.resolve().then((function(){t._emitter.emit(h.default.STATISTICS_INFO,e)}))},e.prototype._onIOError=function(e,t){var i=this;Promise.resolve().then((function(){i._emitter.emit(h.default.IO_ERROR,e,t)}))},e.prototype._onDemuxError=function(e,t){var i=this;Promise.resolve().then((function(){i._emitter.emit(h.default.DEMUX_ERROR,e,t)}))},e.prototype._onRecommendSeekpoint=function(e){var t=this;Promise.resolve().then((function(){t._emitter.emit(h.default.RECOMMEND_SEEKPOINT,e)}))},e.prototype._onLoggingConfigChanged=function(e){this._worker&&this._worker.postMessage({cmd:"logging_config",param:e})},e.prototype._onWorkerMessage=function(e){var t=e.data,i=t.data;if("destroyed"===t.msg||this._workerDestroying)return this._workerDestroying=!1,this._worker.terminate(),void(this._worker=null);switch(t.msg){case h.default.INIT_SEGMENT:case h.default.MEDIA_SEGMENT:this._emitter.emit(t.msg,i.type,i.data);break;case h.default.LOADING_COMPLETE:case h.default.RECOVERED_EARLY_EOF:this._emitter.emit(t.msg);break;case h.default.MEDIA_INFO:Object.setPrototypeOf(i,d.default.prototype),this._emitter.emit(t.msg,i);break;case h.default.METADATA_ARRIVED:case h.default.SCRIPTDATA_ARRIVED:case h.default.STATISTICS_INFO:this._emitter.emit(t.msg,i);break;case h.default.IO_ERROR:case h.default.DEMUX_ERROR:this._emitter.emit(t.msg,i.type,i.info);break;case h.default.RECOMMEND_SEEKPOINT:this._emitter.emit(t.msg,i);break;case"logcat_callback":o.default.emitter.emit("log",i.type,i.logcat)}},e}();t.default=c},"./src/core/transmuxing-controller.js": +/*!********************************************!*\ + !*** ./src/core/transmuxing-controller.js ***! + \********************************************/ +function(e,t,i){i.r(t);var n=i( +/*! events */ +"./node_modules/events/events.js"),r=i.n(n),a=i( +/*! ../utils/logger.js */ +"./src/utils/logger.js"),s=i( +/*! ../utils/browser.js */ +"./src/utils/browser.js"),o=i( +/*! ./media-info.js */ +"./src/core/media-info.js"),u=i( +/*! ../demux/flv-demuxer.js */ +"./src/demux/flv-demuxer.js"),l=i( +/*! ../remux/mp4-remuxer.js */ +"./src/remux/mp4-remuxer.js"),h=i( +/*! ../demux/demux-errors.js */ +"./src/demux/demux-errors.js"),d=i( +/*! ../io/io-controller.js */ +"./src/io/io-controller.js"),c=i( +/*! ./transmuxing-events.js */ +"./src/core/transmuxing-events.js"),f=function(){function e(e,t){this.TAG="TransmuxingController",this._emitter=new(r()),this._config=t,e.segments||(e.segments=[{duration:e.duration,filesize:e.filesize,url:e.url}]),"boolean"!=typeof e.cors&&(e.cors=!0),"boolean"!=typeof e.withCredentials&&(e.withCredentials=!1),this._mediaDataSource=e,this._currentSegmentIndex=0;var i=0;this._mediaDataSource.segments.forEach((function(n){n.timestampBase=i,i+=n.duration,n.cors=e.cors,n.withCredentials=e.withCredentials,t.referrerPolicy&&(n.referrerPolicy=t.referrerPolicy)})),isNaN(i)||this._mediaDataSource.duration===i||(this._mediaDataSource.duration=i),this._mediaInfo=null,this._demuxer=null,this._remuxer=null,this._ioctl=null,this._pendingSeekTime=null,this._pendingResolveSeekPoint=null,this._statisticsReporter=null}return e.prototype.destroy=function(){this._mediaInfo=null,this._mediaDataSource=null,this._statisticsReporter&&this._disableStatisticsReporter(),this._ioctl&&(this._ioctl.destroy(),this._ioctl=null),this._demuxer&&(this._demuxer.destroy(),this._demuxer=null),this._remuxer&&(this._remuxer.destroy(),this._remuxer=null),this._emitter.removeAllListeners(),this._emitter=null},e.prototype.on=function(e,t){this._emitter.addListener(e,t)},e.prototype.off=function(e,t){this._emitter.removeListener(e,t)},e.prototype.start=function(){this._loadSegment(0),this._enableStatisticsReporter()},e.prototype._loadSegment=function(e,t){this._currentSegmentIndex=e;var i=this._mediaDataSource.segments[e],n=this._ioctl=new d.default(i,this._config,e);n.onError=this._onIOException.bind(this),n.onSeeked=this._onIOSeeked.bind(this),n.onComplete=this._onIOComplete.bind(this),n.onRedirect=this._onIORedirect.bind(this),n.onRecoveredEarlyEof=this._onIORecoveredEarlyEof.bind(this),t?this._demuxer.bindDataSource(this._ioctl):n.onDataArrival=this._onInitChunkArrival.bind(this),n.open(t)},e.prototype.stop=function(){this._internalAbort(),this._disableStatisticsReporter()},e.prototype._internalAbort=function(){this._ioctl&&(this._ioctl.destroy(),this._ioctl=null)},e.prototype.pause=function(){this._ioctl&&this._ioctl.isWorking()&&(this._ioctl.pause(),this._disableStatisticsReporter())},e.prototype.resume=function(){this._ioctl&&this._ioctl.isPaused()&&(this._ioctl.resume(),this._enableStatisticsReporter())},e.prototype.seek=function(e){if(null!=this._mediaInfo&&this._mediaInfo.isSeekable()){var t=this._searchSegmentIndexContains(e);if(t===this._currentSegmentIndex){var i=this._mediaInfo.segments[t];if(null==i)this._pendingSeekTime=e;else{var n=i.getNearestKeyframe(e);this._remuxer.seek(n.milliseconds),this._ioctl.seek(n.fileposition),this._pendingResolveSeekPoint=n.milliseconds}}else{var r=this._mediaInfo.segments[t];null==r?(this._pendingSeekTime=e,this._internalAbort(),this._remuxer.seek(),this._remuxer.insertDiscontinuity(),this._loadSegment(t)):(n=r.getNearestKeyframe(e),this._internalAbort(),this._remuxer.seek(e),this._remuxer.insertDiscontinuity(),this._demuxer.resetMediaInfo(),this._demuxer.timestampBase=this._mediaDataSource.segments[t].timestampBase,this._loadSegment(t,n.fileposition),this._pendingResolveSeekPoint=n.milliseconds,this._reportSegmentMediaInfo(t))}this._enableStatisticsReporter()}},e.prototype._searchSegmentIndexContains=function(e){for(var t=this._mediaDataSource.segments,i=t.length-1,n=0;n0)this._demuxer.bindDataSource(this._ioctl),this._demuxer.timestampBase=this._mediaDataSource.segments[this._currentSegmentIndex].timestampBase,r=this._demuxer.parseChunks(e,t);else if((n=u.default.probe(e)).match){this._demuxer=new u.default(n,this._config),this._remuxer||(this._remuxer=new l.default(this._config));var s=this._mediaDataSource;null==s.duration||isNaN(s.duration)||(this._demuxer.overridedDuration=s.duration),"boolean"==typeof s.hasAudio&&(this._demuxer.overridedHasAudio=s.hasAudio),"boolean"==typeof s.hasVideo&&(this._demuxer.overridedHasVideo=s.hasVideo),this._demuxer.timestampBase=s.segments[this._currentSegmentIndex].timestampBase,this._demuxer.onError=this._onDemuxException.bind(this),this._demuxer.onMediaInfo=this._onMediaInfo.bind(this),this._demuxer.onMetaDataArrived=this._onMetaDataArrived.bind(this),this._demuxer.onScriptDataArrived=this._onScriptDataArrived.bind(this),this._remuxer.bindDataSource(this._demuxer.bindDataSource(this._ioctl)),this._remuxer.onInitSegment=this._onRemuxerInitSegmentArrival.bind(this),this._remuxer.onMediaSegment=this._onRemuxerMediaSegmentArrival.bind(this),r=this._demuxer.parseChunks(e,t)}else n=null,a.default.e(this.TAG,"Non-FLV, Unsupported media type!"),Promise.resolve().then((function(){i._internalAbort()})),this._emitter.emit(c.default.DEMUX_ERROR,h.default.FORMAT_UNSUPPORTED,"Non-FLV, Unsupported media type"),r=0;return r},e.prototype._onMediaInfo=function(e){var t=this;null==this._mediaInfo&&(this._mediaInfo=Object.assign({},e),this._mediaInfo.keyframesIndex=null,this._mediaInfo.segments=[],this._mediaInfo.segmentCount=this._mediaDataSource.segments.length,Object.setPrototypeOf(this._mediaInfo,o.default.prototype));var i=Object.assign({},e);Object.setPrototypeOf(i,o.default.prototype),this._mediaInfo.segments[this._currentSegmentIndex]=i,this._reportSegmentMediaInfo(this._currentSegmentIndex),null!=this._pendingSeekTime&&Promise.resolve().then((function(){var e=t._pendingSeekTime;t._pendingSeekTime=null,t.seek(e)}))},e.prototype._onMetaDataArrived=function(e){this._emitter.emit(c.default.METADATA_ARRIVED,e)},e.prototype._onScriptDataArrived=function(e){this._emitter.emit(c.default.SCRIPTDATA_ARRIVED,e)},e.prototype._onIOSeeked=function(){this._remuxer.insertDiscontinuity()},e.prototype._onIOComplete=function(e){var t=e+1;t0&&i[0].originalDts===n&&(n=i[0].pts),this._emitter.emit(c.default.RECOMMEND_SEEKPOINT,n)}},e.prototype._enableStatisticsReporter=function(){null==this._statisticsReporter&&(this._statisticsReporter=self.setInterval(this._reportStatisticsInfo.bind(this),this._config.statisticsInfoReportInterval))},e.prototype._disableStatisticsReporter=function(){this._statisticsReporter&&(self.clearInterval(this._statisticsReporter),this._statisticsReporter=null)},e.prototype._reportSegmentMediaInfo=function(e){var t=this._mediaInfo.segments[e],i=Object.assign({},t);i.duration=this._mediaInfo.duration,i.segmentCount=this._mediaInfo.segmentCount,delete i.segments,delete i.keyframesIndex,this._emitter.emit(c.default.MEDIA_INFO,i)},e.prototype._reportStatisticsInfo=function(){var e={};e.url=this._ioctl.currentURL,e.hasRedirect=this._ioctl.hasRedirect,e.hasRedirect&&(e.redirectedURL=this._ioctl.currentRedirectedURL),e.speed=this._ioctl.currentSpeed,e.loaderType=this._ioctl.loaderType,e.currentSegmentIndex=this._currentSegmentIndex,e.totalSegmentCount=this._mediaDataSource.segments.length,this._emitter.emit(c.default.STATISTICS_INFO,e)},e}();t.default=f},"./src/core/transmuxing-events.js": +/*!****************************************!*\ + !*** ./src/core/transmuxing-events.js ***! + \****************************************/ +function(e,t,i){i.r(t),t.default={IO_ERROR:"io_error",DEMUX_ERROR:"demux_error",INIT_SEGMENT:"init_segment",MEDIA_SEGMENT:"media_segment",LOADING_COMPLETE:"loading_complete",RECOVERED_EARLY_EOF:"recovered_early_eof",MEDIA_INFO:"media_info",METADATA_ARRIVED:"metadata_arrived",SCRIPTDATA_ARRIVED:"scriptdata_arrived",STATISTICS_INFO:"statistics_info",RECOMMEND_SEEKPOINT:"recommend_seekpoint"}},"./src/core/transmuxing-worker.js": +/*!****************************************!*\ + !*** ./src/core/transmuxing-worker.js ***! + \****************************************/ +function(e,t,i){i.r(t);var n=i( +/*! ../utils/logging-control.js */ +"./src/utils/logging-control.js"),r=i( +/*! ../utils/polyfill.js */ +"./src/utils/polyfill.js"),a=i( +/*! ./transmuxing-controller.js */ +"./src/core/transmuxing-controller.js"),s=i( +/*! ./transmuxing-events.js */ +"./src/core/transmuxing-events.js");t.default=function(e){var t=null,i=function(t,i){e.postMessage({msg:"logcat_callback",data:{type:t,logcat:i}})}.bind(this);function o(t,i){var n={msg:s.default.INIT_SEGMENT,data:{type:t,data:i}};e.postMessage(n,[i.data])}function u(t,i){var n={msg:s.default.MEDIA_SEGMENT,data:{type:t,data:i}};e.postMessage(n,[i.data])}function l(){var t={msg:s.default.LOADING_COMPLETE};e.postMessage(t)}function h(){var t={msg:s.default.RECOVERED_EARLY_EOF};e.postMessage(t)}function d(t){var i={msg:s.default.MEDIA_INFO,data:t};e.postMessage(i)}function c(t){var i={msg:s.default.METADATA_ARRIVED,data:t};e.postMessage(i)}function f(t){var i={msg:s.default.SCRIPTDATA_ARRIVED,data:t};e.postMessage(i)}function p(t){var i={msg:s.default.STATISTICS_INFO,data:t};e.postMessage(i)}function m(t,i){e.postMessage({msg:s.default.IO_ERROR,data:{type:t,info:i}})}function _(t,i){e.postMessage({msg:s.default.DEMUX_ERROR,data:{type:t,info:i}})}function g(t){e.postMessage({msg:s.default.RECOMMEND_SEEKPOINT,data:t})}r.default.install(),e.addEventListener("message",(function(r){switch(r.data.cmd){case"init":(t=new a.default(r.data.param[0],r.data.param[1])).on(s.default.IO_ERROR,m.bind(this)),t.on(s.default.DEMUX_ERROR,_.bind(this)),t.on(s.default.INIT_SEGMENT,o.bind(this)),t.on(s.default.MEDIA_SEGMENT,u.bind(this)),t.on(s.default.LOADING_COMPLETE,l.bind(this)),t.on(s.default.RECOVERED_EARLY_EOF,h.bind(this)),t.on(s.default.MEDIA_INFO,d.bind(this)),t.on(s.default.METADATA_ARRIVED,c.bind(this)),t.on(s.default.SCRIPTDATA_ARRIVED,f.bind(this)),t.on(s.default.STATISTICS_INFO,p.bind(this)),t.on(s.default.RECOMMEND_SEEKPOINT,g.bind(this));break;case"destroy":t&&(t.destroy(),t=null),e.postMessage({msg:"destroyed"});break;case"start":t.start();break;case"stop":t.stop();break;case"seek":t.seek(r.data.param);break;case"pause":t.pause();break;case"resume":t.resume();break;case"logging_config":var v=r.data.param;n.default.applyConfig(v),!0===v.enableCallback?n.default.addLogListener(i):n.default.removeLogListener(i)}}))}},"./src/demux/amf-parser.js": +/*!*********************************!*\ + !*** ./src/demux/amf-parser.js ***! + \*********************************/ +function(e,t,i){i.r(t);var n,r=i( +/*! ../utils/logger.js */ +"./src/utils/logger.js"),a=i( +/*! ../utils/utf8-conv.js */ +"./src/utils/utf8-conv.js"),s=i( +/*! ../utils/exception.js */ +"./src/utils/exception.js"),o=(n=new ArrayBuffer(2),new DataView(n).setInt16(0,256,!0),256===new Int16Array(n)[0]),u=function(){function e(){}return e.parseScriptData=function(t,i,n){var a={};try{var s=e.parseValue(t,i,n),o=e.parseValue(t,i+s.size,n-s.size);a[s.data]=o.data}catch(e){r.default.e("AMF",e.toString())}return a},e.parseObject=function(t,i,n){if(n<3)throw new s.IllegalStateException("Data not enough when parse ScriptDataObject");var r=e.parseString(t,i,n),a=e.parseValue(t,i+r.size,n-r.size),o=a.objectEnd;return{data:{name:r.data,value:a.data},size:r.size+a.size,objectEnd:o}},e.parseVariable=function(t,i,n){return e.parseObject(t,i,n)},e.parseString=function(e,t,i){if(i<2)throw new s.IllegalStateException("Data not enough when parse String");var n=new DataView(e,t,i).getUint16(0,!o);return{data:n>0?(0,a.default)(new Uint8Array(e,t+2,n)):"",size:2+n}},e.parseLongString=function(e,t,i){if(i<4)throw new s.IllegalStateException("Data not enough when parse LongString");var n=new DataView(e,t,i).getUint32(0,!o);return{data:n>0?(0,a.default)(new Uint8Array(e,t+4,n)):"",size:4+n}},e.parseDate=function(e,t,i){if(i<10)throw new s.IllegalStateException("Data size invalid when parse Date");var n=new DataView(e,t,i),r=n.getFloat64(0,!o),a=n.getInt16(8,!o);return{data:new Date(r+=60*a*1e3),size:10}},e.parseValue=function(t,i,n){if(n<1)throw new s.IllegalStateException("Data not enough when parse Value");var a,u=new DataView(t,i,n),l=1,h=u.getUint8(0),d=!1;try{switch(h){case 0:a=u.getFloat64(1,!o),l+=8;break;case 1:a=!!u.getUint8(1),l+=1;break;case 2:var c=e.parseString(t,i+1,n-1);a=c.data,l+=c.size;break;case 3:a={};var f=0;for(9==(16777215&u.getUint32(n-4,!o))&&(f=3);l32)throw new n.InvalidArgumentException("ExpGolomb: readBits() bits exceeded max 32bits!");if(e<=this._current_word_bits_left){var t=this._current_word>>>32-e;return this._current_word<<=e,this._current_word_bits_left-=e,t}var i=this._current_word_bits_left?this._current_word:0;i>>>=32-this._current_word_bits_left;var r=e-this._current_word_bits_left;this._fillCurrentWord();var a=Math.min(r,this._current_word_bits_left),s=this._current_word>>>32-a;return this._current_word<<=a,this._current_word_bits_left-=a,i=i<>>e))return this._current_word<<=e,this._current_word_bits_left-=e,e;return this._fillCurrentWord(),e+this._skipLeadingZero()},e.prototype.readUEG=function(){var e=this._skipLeadingZero();return this.readBits(e+1)-1},e.prototype.readSEG=function(){var e=this.readUEG();return 1&e?e+1>>>1:-1*(e>>>1)},e}();t.default=r},"./src/demux/flv-demuxer.js": +/*!**********************************!*\ + !*** ./src/demux/flv-demuxer.js ***! + \**********************************/ +function(e,t,i){i.r(t);var r=i( +/*! ../utils/logger.js */ +"./src/utils/logger.js"),a=i( +/*! ./amf-parser.js */ +"./src/demux/amf-parser.js"),s=i( +/*! ./sps-parser.js */ +"./src/demux/sps-parser.js"),o=i( +/*! ./hevc-sps-parser.js */ +"./src/demux/hevc-sps-parser.js"),u=i( +/*! ./demux-errors.js */ +"./src/demux/demux-errors.js"),l=i( +/*! ../core/media-info.js */ +"./src/core/media-info.js"),h=i( +/*! ../utils/exception.js */ +"./src/utils/exception.js"),d=function(){function e(e,t){var i;this.TAG="FLVDemuxer",this._config=t,this._onError=null,this._onMediaInfo=null,this._onMetaDataArrived=null,this._onScriptDataArrived=null,this._onTrackMetadata=null,this._onDataAvailable=null,this._dataOffset=e.dataOffset,this._firstParse=!0,this._dispatch=!1,this._hasAudio=e.hasAudioTrack,this._hasVideo=e.hasVideoTrack,this._hasAudioFlagOverrided=!1,this._hasVideoFlagOverrided=!1,this._audioInitialMetadataDispatched=!1,this._videoInitialMetadataDispatched=!1,this._mediaInfo=new l.default,this._mediaInfo.hasAudio=this._hasAudio,this._mediaInfo.hasVideo=this._hasVideo,this._metadata=null,this._audioMetadata=null,this._videoMetadata=null,this._naluLengthSize=4,this._timestampBase=0,this._timescale=1e3,this._duration=0,this._durationOverrided=!1,this._referenceFrameRate={fixed:!0,fps:23.976,fps_num:23976,fps_den:1e3},this._flvSoundRateTable=[5500,11025,22050,44100,48e3],this._mpegSamplingRates=[96e3,88200,64e3,48e3,44100,32e3,24e3,22050,16e3,12e3,11025,8e3,7350],this._mpegAudioV10SampleRateTable=[44100,48e3,32e3,0],this._mpegAudioV20SampleRateTable=[22050,24e3,16e3,0],this._mpegAudioV25SampleRateTable=[11025,12e3,8e3,0],this._mpegAudioL1BitRateTable=[0,32,64,96,128,160,192,224,256,288,320,352,384,416,448,-1],this._mpegAudioL2BitRateTable=[0,32,48,56,64,80,96,112,128,160,192,224,256,320,384,-1],this._mpegAudioL3BitRateTable=[0,32,40,48,56,64,80,96,112,128,160,192,224,256,320,-1],this._videoTrack={type:"video",id:1,sequenceNumber:0,samples:[],length:0},this._audioTrack={type:"audio",id:2,sequenceNumber:0,samples:[],length:0},this._littleEndian=(i=new ArrayBuffer(2),new DataView(i).setInt16(0,256,!0),256===new Int16Array(i)[0])}return e.prototype.destroy=function(){this._mediaInfo=null,this._metadata=null,this._audioMetadata=null,this._videoMetadata=null,this._videoTrack=null,this._audioTrack=null,this._onError=null,this._onMediaInfo=null,this._onMetaDataArrived=null,this._onScriptDataArrived=null,this._onTrackMetadata=null,this._onDataAvailable=null},e.probe=function(e){var t=new Uint8Array(e),i={match:!1};if(70!==t[0]||76!==t[1]||86!==t[2]||1!==t[3])return i;var n,r,a=(4&t[4])>>>2!=0,s=0!=(1&t[4]),o=(n=t)[r=5]<<24|n[r+1]<<16|n[r+2]<<8|n[r+3];return o<9?i:{match:!0,consumed:o,dataOffset:o,hasAudioTrack:a,hasVideoTrack:s}},e.prototype.bindDataSource=function(e){return e.onDataArrival=this.parseChunks.bind(this),this},Object.defineProperty(e.prototype,"onTrackMetadata",{get:function(){return this._onTrackMetadata},set:function(e){this._onTrackMetadata=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onMediaInfo",{get:function(){return this._onMediaInfo},set:function(e){this._onMediaInfo=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onMetaDataArrived",{get:function(){return this._onMetaDataArrived},set:function(e){this._onMetaDataArrived=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onScriptDataArrived",{get:function(){return this._onScriptDataArrived},set:function(e){this._onScriptDataArrived=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onError",{get:function(){return this._onError},set:function(e){this._onError=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onDataAvailable",{get:function(){return this._onDataAvailable},set:function(e){this._onDataAvailable=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"timestampBase",{get:function(){return this._timestampBase},set:function(e){this._timestampBase=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"overridedDuration",{get:function(){return this._duration},set:function(e){this._durationOverrided=!0,this._duration=e,this._mediaInfo.duration=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"overridedHasAudio",{set:function(e){this._hasAudioFlagOverrided=!0,this._hasAudio=e,this._mediaInfo.hasAudio=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"overridedHasVideo",{set:function(e){this._hasVideoFlagOverrided=!0,this._hasVideo=e,this._mediaInfo.hasVideo=e},enumerable:!1,configurable:!0}),e.prototype.resetMediaInfo=function(){this._mediaInfo=new l.default},e.prototype._isInitialMetadataDispatched=function(){return this._hasAudio&&this._hasVideo?this._audioInitialMetadataDispatched&&this._videoInitialMetadataDispatched:this._hasAudio&&!this._hasVideo?this._audioInitialMetadataDispatched:!(this._hasAudio||!this._hasVideo)&&this._videoInitialMetadataDispatched},e.prototype.parseChunks=function(t,i){if(!(this._onError&&this._onMediaInfo&&this._onTrackMetadata&&this._onDataAvailable))throw new h.IllegalStateException("Flv: onError & onMediaInfo & onTrackMetadata & onDataAvailable callback must be specified");var n=0,a=this._littleEndian;if(0===i){if(!(t.byteLength>13))return 0;n=e.probe(t).dataOffset}for(this._firstParse&&(this._firstParse=!1,i+n!==this._dataOffset&&r.default.w(this.TAG,"First time parsing but chunk byteStart invalid!"),0!==(s=new DataView(t,n)).getUint32(0,!a)&&r.default.w(this.TAG,"PrevTagSize0 !== 0 !!!"),n+=4);nt.byteLength)break;var o=s.getUint8(0),u=16777215&s.getUint32(0,!a);if(n+11+u+4>t.byteLength)break;if(8===o||9===o||18===o){var l=s.getUint8(4),d=s.getUint8(5),c=s.getUint8(6)|d<<8|l<<16|s.getUint8(7)<<24;0!=(16777215&s.getUint32(7,!a))&&r.default.w(this.TAG,"Meet tag which has StreamID != 0!");var f=n+11;switch(o){case 8:this._parseAudioData(t,f,u,c);break;case 9:this._parseVideoData(t,f,u,c,i+n);break;case 18:this._parseScriptData(t,f,u)}var p=s.getUint32(11+u,!a);p!==11+u&&r.default.w(this.TAG,"Invalid PrevTagSize "+p),n+=11+u+4}else r.default.w(this.TAG,"Unsupported tag type "+o+", skipped"),n+=11+u+4}return this._isInitialMetadataDispatched()&&this._dispatch&&(this._audioTrack.length||this._videoTrack.length)&&this._onDataAvailable(this._audioTrack,this._videoTrack),n},e.prototype._parseScriptData=function(e,t,i){var s=a.default.parseScriptData(e,t,i);if(s.hasOwnProperty("onMetaData")){if(null==s.onMetaData||"object"!==n(s.onMetaData))return void r.default.w(this.TAG,"Invalid onMetaData structure!");this._metadata&&r.default.w(this.TAG,"Found another onMetaData tag!"),this._metadata=s;var o=this._metadata.onMetaData;if(this._onMetaDataArrived&&this._onMetaDataArrived(Object.assign({},o)),"boolean"==typeof o.hasAudio&&!1===this._hasAudioFlagOverrided&&(this._hasAudio=o.hasAudio,this._mediaInfo.hasAudio=this._hasAudio),"boolean"==typeof o.hasVideo&&!1===this._hasVideoFlagOverrided&&(this._hasVideo=o.hasVideo,this._mediaInfo.hasVideo=this._hasVideo),"number"==typeof o.audiodatarate&&(this._mediaInfo.audioDataRate=o.audiodatarate),"number"==typeof o.videodatarate&&(this._mediaInfo.videoDataRate=o.videodatarate),"number"==typeof o.width&&(this._mediaInfo.width=o.width),"number"==typeof o.height&&(this._mediaInfo.height=o.height),"number"==typeof o.duration){if(!this._durationOverrided){var u=Math.floor(o.duration*this._timescale);this._duration=u,this._mediaInfo.duration=u}}else this._mediaInfo.duration=0;if("number"==typeof o.framerate){var l=Math.floor(1e3*o.framerate);if(l>0){var h=l/1e3;this._referenceFrameRate.fixed=!0,this._referenceFrameRate.fps=h,this._referenceFrameRate.fps_num=l,this._referenceFrameRate.fps_den=1e3,this._mediaInfo.fps=h}}if("object"===n(o.keyframes)){this._mediaInfo.hasKeyframesIndex=!0;var d=o.keyframes;this._mediaInfo.keyframesIndex=this._parseKeyframesIndex(d),o.keyframes=null}else this._mediaInfo.hasKeyframesIndex=!1;this._dispatch=!1,this._mediaInfo.metadata=o,r.default.v(this.TAG,"Parsed onMetaData"),this._mediaInfo.isComplete()&&this._onMediaInfo(this._mediaInfo)}Object.keys(s).length>0&&this._onScriptDataArrived&&this._onScriptDataArrived(Object.assign({},s))},e.prototype._parseKeyframesIndex=function(e){for(var t=[],i=[],n=1;n>>4;if(2===s||10===s){var o=0,l=(12&a)>>>2;if(l>=0&&l<=4){o=this._flvSoundRateTable[l];var h=1&a,d=this._audioMetadata,c=this._audioTrack;if(d||(!1===this._hasAudio&&!1===this._hasAudioFlagOverrided&&(this._hasAudio=!0,this._mediaInfo.hasAudio=!0),(d=this._audioMetadata={}).type="audio",d.id=c.id,d.timescale=this._timescale,d.duration=this._duration,d.audioSampleRate=o,d.channelCount=0===h?1:2),10===s){var f=this._parseAACAudioData(e,t+1,i-1);if(null==f)return;if(0===f.packetType){d.config&&r.default.w(this.TAG,"Found another AudioSpecificConfig!");var p=f.data;d.audioSampleRate=p.samplingRate,d.channelCount=p.channelCount,d.codec=p.codec,d.originalCodec=p.originalCodec,d.config=p.config,d.refSampleDuration=1024/d.audioSampleRate*d.timescale,r.default.v(this.TAG,"Parsed AudioSpecificConfig"),this._isInitialMetadataDispatched()?this._dispatch&&(this._audioTrack.length||this._videoTrack.length)&&this._onDataAvailable(this._audioTrack,this._videoTrack):this._audioInitialMetadataDispatched=!0,this._dispatch=!1,this._onTrackMetadata("audio",d),(g=this._mediaInfo).audioCodec=d.originalCodec,g.audioSampleRate=d.audioSampleRate,g.audioChannelCount=d.channelCount,g.hasVideo?null!=g.videoCodec&&(g.mimeType='video/x-flv; codecs="'+g.videoCodec+","+g.audioCodec+'"'):g.mimeType='video/x-flv; codecs="'+g.audioCodec+'"',g.isComplete()&&this._onMediaInfo(g)}else if(1===f.packetType){var m=this._timestampBase+n,_={unit:f.data,length:f.data.byteLength,dts:m,pts:m};c.samples.push(_),c.length+=f.data.length}else r.default.e(this.TAG,"Flv: Unsupported AAC data type "+f.packetType)}else if(2===s){if(!d.codec){var g;if(null==(p=this._parseMP3AudioData(e,t+1,i-1,!0)))return;d.audioSampleRate=p.samplingRate,d.channelCount=p.channelCount,d.codec=p.codec,d.originalCodec=p.originalCodec,d.refSampleDuration=1152/d.audioSampleRate*d.timescale,r.default.v(this.TAG,"Parsed MPEG Audio Frame Header"),this._audioInitialMetadataDispatched=!0,this._onTrackMetadata("audio",d),(g=this._mediaInfo).audioCodec=d.codec,g.audioSampleRate=d.audioSampleRate,g.audioChannelCount=d.channelCount,g.audioDataRate=p.bitRate,g.hasVideo?null!=g.videoCodec&&(g.mimeType='video/x-flv; codecs="'+g.videoCodec+","+g.audioCodec+'"'):g.mimeType='video/x-flv; codecs="'+g.audioCodec+'"',g.isComplete()&&this._onMediaInfo(g)}var v=this._parseMP3AudioData(e,t+1,i-1,!1);if(null==v)return;m=this._timestampBase+n;var y={unit:v,length:v.byteLength,dts:m,pts:m};c.samples.push(y),c.length+=v.length}}else this._onError(u.default.FORMAT_ERROR,"Flv: Invalid audio sample rate idx: "+l)}else this._onError(u.default.CODEC_UNSUPPORTED,"Flv: Unsupported audio codec idx: "+s)}},e.prototype._parseAACAudioData=function(e,t,i){if(!(i<=1)){var n={},a=new Uint8Array(e,t,i);return n.packetType=a[0],0===a[0]?n.data=this._parseAACAudioSpecificConfig(e,t+1,i-1):n.data=a.subarray(1),n}r.default.w(this.TAG,"Flv: Invalid AAC packet, missing AACPacketType or/and Data!")},e.prototype._parseAACAudioSpecificConfig=function(e,t,i){var n,r,a=new Uint8Array(e,t,i),s=null,o=0,l=null;if(o=n=a[0]>>>3,(r=(7&a[0])<<1|a[1]>>>7)<0||r>=this._mpegSamplingRates.length)this._onError(u.default.FORMAT_ERROR,"Flv: AAC invalid sampling frequency index!");else{var h=this._mpegSamplingRates[r],d=(120&a[1])>>>3;if(!(d<0||d>=8)){5===o&&(l=(7&a[1])<<1|a[2]>>>7,a[2]);var c=self.navigator.userAgent.toLowerCase();return-1!==c.indexOf("firefox")?r>=6?(o=5,s=new Array(4),l=r-3):(o=2,s=new Array(2),l=r):-1!==c.indexOf("android")?(o=2,s=new Array(2),l=r):(o=5,l=r,s=new Array(4),r>=6?l=r-3:1===d&&(o=2,s=new Array(2),l=r)),s[0]=o<<3,s[0]|=(15&r)>>>1,s[1]=(15&r)<<7,s[1]|=(15&d)<<3,5===o&&(s[1]|=(15&l)>>>1,s[2]=(1&l)<<7,s[2]|=8,s[3]=0),{config:s,samplingRate:h,channelCount:d,codec:"mp4a.40."+o,originalCodec:"mp4a.40."+n}}this._onError(u.default.FORMAT_ERROR,"Flv: AAC invalid channel configuration")}},e.prototype._parseMP3AudioData=function(e,t,i,n){if(!(i<4)){this._littleEndian;var a=new Uint8Array(e,t,i),s=null;if(n){if(255!==a[0])return;var o=a[1]>>>3&3,u=(6&a[1])>>1,l=(240&a[2])>>>4,h=(12&a[2])>>>2,d=3!=(a[3]>>>6&3)?2:1,c=0,f=0;switch(o){case 0:c=this._mpegAudioV25SampleRateTable[h];break;case 2:c=this._mpegAudioV20SampleRateTable[h];break;case 3:c=this._mpegAudioV10SampleRateTable[h]}switch(u){case 1:l>>4,l=15&s;7===l||12===l?7===l?this._parseAVCVideoPacket(e,t+1,i-1,n,a,o):12===l&&this._parseHVCVideoPacket(e,t+1,i-1,n,a,o):this._onError(u.default.CODEC_UNSUPPORTED,"Flv: Unsupported codec in video frame: "+l)}},e.prototype._parseAVCVideoPacket=function(e,t,i,n,a,s){if(i<4)r.default.w(this.TAG,"Flv: Invalid AVC packet, missing AVCPacketType or/and CompositionTime");else{var o=this._littleEndian,l=new DataView(e,t,i),h=l.getUint8(0),d=(16777215&l.getUint32(0,!o))<<8>>8;if(0===h)this._parseAVCDecoderConfigurationRecord(e,t+4,i-4);else if(1===h)this._parseAVCVideoData(e,t+4,i-4,n,a,s,d);else if(2!==h)return void this._onError(u.default.FORMAT_ERROR,"Flv: Invalid video packet type "+h)}},e.prototype._parseAVCDecoderConfigurationRecord=function(e,t,i){if(i<7)r.default.w(this.TAG,"Flv: Invalid AVCDecoderConfigurationRecord, lack of data!");else{var n=this._videoMetadata,a=this._videoTrack,o=this._littleEndian,l=new DataView(e,t,i);n?void 0!==n.avcc&&r.default.w(this.TAG,"Found another AVCDecoderConfigurationRecord!"):(!1===this._hasVideo&&!1===this._hasVideoFlagOverrided&&(this._hasVideo=!0,this._mediaInfo.hasVideo=!0),(n=this._videoMetadata={}).type="video",n.id=a.id,n.timescale=this._timescale,n.duration=this._duration);var h=l.getUint8(0),d=l.getUint8(1);if(l.getUint8(2),l.getUint8(3),1===h&&0!==d)if(this._naluLengthSize=1+(3&l.getUint8(4)),3===this._naluLengthSize||4===this._naluLengthSize){var c=31&l.getUint8(5);if(0!==c){c>1&&r.default.w(this.TAG,"Flv: Strange AVCDecoderConfigurationRecord: SPS Count = "+c);for(var f=6,p=0;p1&&r.default.w(this.TAG,"Flv: Strange AVCDecoderConfigurationRecord: PPS Count = "+A),f++,p=0;p=i){r.default.w(this.TAG,"Malformed Nalu near timestamp "+p+", offset = "+c+", dataSize = "+i);break}var _=l.getUint32(c,!u);if(3===f&&(_>>>=8),_>i-f)return void r.default.w(this.TAG,"Malformed Nalus near timestamp "+p+", NaluSize > DataSize!");var g=31&l.getUint8(c+f);5===g&&(m=!0);var v=new Uint8Array(e,t+c,f+_),y={type:g,data:v};h.push(y),d+=v.byteLength,c+=f+_}if(h.length){var b=this._videoTrack,S={units:h,length:d,isKeyframe:m,dts:p,cts:o,pts:p+o};m&&(S.fileposition=a),b.samples.push(S),b.length+=d}},e.prototype._parseHVCVideoPacket=function(e,t,i,n,a,s){if(i<4)r.default.w(this.TAG,"Flv: Invalid HVC packet, missing HVCPacketType or/and CompositionTime");else{var o=this._littleEndian,l=new DataView(e,t,i),h=l.getUint8(0),d=(16777215&l.getUint32(0,!o))<<8>>8;if(0===h)this._parseHVCDecoderConfigurationRecord(e,t+4,i-4);else if(1===h)this._parseHVCVideoData(e,t+4,i-4,n,a,s,d);else if(2!==h)return void this._onError(u.default.FORMAT_ERROR,"Flv: Invalid video packet type "+h)}},e.prototype._parseHVCDecoderConfigurationRecord=function(e,t,i){if(i<23)r.default.w(this.TAG,"Flv: Invalid HVCDecoderConfigurationRecord, lack of data!");else{var n=this._videoMetadata,a=this._videoTrack,s=this._littleEndian,l=new DataView(e,t,i);if(n?void 0!==n.avcc&&r.default.w(this.TAG,"Found another HVCDecoderConfigurationRecord!"):(!1===this._hasVideo&&!1===this._hasVideoFlagOverrided&&(this._hasVideo=!0,this._mediaInfo.hasVideo=!0),(n=this._videoMetadata={}).type="video",n.id=a.id,n.timescale=this._timescale,n.duration=this._duration),1===l.getUint8(0))if(this._naluLengthSize=1+(3&l.getUint8(21)),3===this._naluLengthSize||4===this._naluLengthSize){for(var h,d,c,f=l.getUint8(22),p=23,m=[],_=0;_1&&r.default.w(this.TAG,"Flv: Strange HVCDecoderConfigurationRecord: VPS Count = "+h),0!==d)if(d>1&&r.default.w(this.TAG,"Flv: Strange HVCDecoderConfigurationRecord: SPS Count = "+d),0!==c){c>1&&r.default.w(this.TAG,"Flv: Strange HVCDecoderConfigurationRecord: PPS Count = "+d);var T=m[0],E=o.default.parseSPS(T);n.codecWidth=E.codec_size.width,n.codecHeight=E.codec_size.height,n.presentWidth=E.present_size.width,n.presentHeight=E.present_size.height,n.profile=E.profile_string,n.level=E.level_string,n.profile_idc=E.profile_idc,n.level_idc=E.level_idc,n.bitDepth=E.bit_depth,n.chromaFormat=E.chroma_format,n.sarRatio=E.sar_ratio,n.frameRate=E.frame_rate,!1!==E.frame_rate.fixed&&0!==E.frame_rate.fps_num&&0!==E.frame_rate.fps_den||(n.frameRate=this._referenceFrameRate);var w=n.frameRate.fps_den,A=n.frameRate.fps_num;n.refSampleDuration=n.timescale*(w/A);var C="hvc1."+n.profile_idc+".1.L"+n.level_idc+".B0";n.codec=C;var k=this._mediaInfo;k.width=n.codecWidth,k.height=n.codecHeight,k.fps=n.frameRate.fps,k.profile=n.profile,k.level=n.level,k.refFrames=E.ref_frames,k.chromaFormat=E.chroma_format_string,k.sarNum=n.sarRatio.width,k.sarDen=n.sarRatio.height,k.videoCodec=C,k.hasAudio?null!=k.audioCodec&&(k.mimeType='video/x-flv; codecs="'+k.videoCodec+","+k.audioCodec+'"'):k.mimeType='video/x-flv; codecs="'+k.videoCodec+'"',k.isComplete()&&this._onMediaInfo(k),n.avcc=new Uint8Array(i),n.avcc.set(new Uint8Array(e,t,i),0),r.default.v(this.TAG,"Parsed HVCDecoderConfigurationRecord"),this._isInitialMetadataDispatched()?this._dispatch&&(this._audioTrack.length||this._videoTrack.length)&&this._onDataAvailable(this._audioTrack,this._videoTrack):this._videoInitialMetadataDispatched=!0,this._dispatch=!1,this._onTrackMetadata("video",n)}else this._onError(u.default.FORMAT_ERROR,"Flv: Invalid HVCDecoderConfigurationRecord: No PPS");else this._onError(u.default.FORMAT_ERROR,"Flv: Invalid HVCDecoderConfigurationRecord: No SPS");else this._onError(u.default.FORMAT_ERROR,"Flv: Invalid HVCDecoderConfigurationRecord: No VPS")}else this._onError(u.default.FORMAT_ERROR,"Flv: Strange NaluLengthSizeMinusOne: "+(this._naluLengthSize-1));else this._onError(u.default.FORMAT_ERROR,"Flv: Invalid HVCDecoderConfigurationRecord")}},e.prototype._parseHVCVideoData=function(e,t,i,n,a,s,o){for(var u=this._littleEndian,l=new DataView(e,t,i),h=[],d=0,c=0,f=this._naluLengthSize,p=this._timestampBase+n,m=1===s;c=i){r.default.w(this.TAG,"Malformed Nalu near timestamp "+p+", offset = "+c+", dataSize = "+i);break}var _=l.getUint32(c,!u);if(3===f&&(_>>>=8),_>i-f)return void r.default.w(this.TAG,"Malformed Nalus near timestamp "+p+", NaluSize > DataSize!");var g=l.getUint8(c+f)>>1&63;g>=16&&g<=23&&(m=!0);var v=new Uint8Array(e,t+c,f+_),y={type:g,data:v};h.push(y),d+=v.byteLength,c+=f+_}if(h.length){var b=this._videoTrack,S={units:h,length:d,isKeyframe:m,dts:p,cts:o,pts:p+o};m&&(S.fileposition=a),b.samples.push(S),b.length+=d}},e}();t.default=d},"./src/demux/hevc-sps-parser.js": +/*!**************************************!*\ + !*** ./src/demux/hevc-sps-parser.js ***! + \**************************************/ +function(e,t,i){i.r(t);var n=i( +/*! ./exp-golomb.js */ +"./src/demux/exp-golomb.js"),r=i( +/*! ./sps-parser.js */ +"./src/demux/sps-parser.js"),a=function(){function e(){}return e.parseSPS=function(t){var i=r.default._ebsp2rbsp(t),a=new n.default(i),s={};a.readBits(16),a.readBits(4);var o=a.readBits(3);a.readBits(1),e._hvcc_parse_ptl(a,s,o),a.readUEG();var u=0,l=a.readUEG();3==l&&(u=a.readBits(1)),s.sar_width=s.sar_height=1,s.conf_win_left_offset=s.conf_win_right_offset=s.conf_win_top_offset=s.conf_win_bottom_offset=0,s.def_disp_win_left_offset=s.def_disp_win_right_offset=s.def_disp_win_top_offset=s.def_disp_win_bottom_offset=0;var h=a.readUEG(),d=a.readUEG();a.readBits(1)&&(s.conf_win_left_offset=a.readUEG(),s.conf_win_right_offset=a.readUEG(),s.conf_win_top_offset=a.readUEG(),s.conf_win_bottom_offset=a.readUEG(),1===s.default_display_window_flag&&(s.conf_win_left_offset,s.def_disp_win_left_offset,s.conf_win_right_offset,s.def_disp_win_right_offset,s.conf_win_top_offset,s.def_disp_win_top_offset,s.conf_win_bottom_offset,s.def_disp_win_bottom_offset));var c=a.readUEG()+8;a.readUEG();for(var f=a.readUEG(),p=a.readBits(1)?0:o;p<=o;p++)e._skip_sub_layer_ordering_info(a);a.readUEG(),a.readUEG(),a.readUEG(),a.readUEG(),a.readUEG(),a.readUEG(),a.readBits(1)&&a.readBits(1)&&e._skip_scaling_list_data(a),a.readBits(1),a.readBits(1),a.readBits(1)&&(a.readBits(4),a.readBits(4),a.readUEG(),a.readUEG(),a.readBits(1));var m=[],_=a.readUEG();for(p=0;p<_;p++){var g=e._parse_rps(a,p,_,m);if(g<0)return g}if(a.readBits(1)){var v=a.readUEG();for(p=0;p32){for(var b=y/32,S=y%32,T=0;T0)for(u=i;u<8;u++)e.readBits(2);for(u=0;u=i)return-1;e.readBits(1),e.readUEG(),n[t]=0;for(var r=0;r<=n[t-1];r++){var a=0,s=e.readBits(1);s||(a=e.readBits(1)),(s||a)&&n[t]++}}else{var o=e.readUEG(),u=e.readUEG();for(n[t]=o+u,r=0;r1&&e.readSEG();for(var r=0;r0&&(t.fps=t.fps_num/t.fps_den);var i=0;e.readBits(1)&&(i=e.readUEG())>=0&&(t.fps/=i+1)},e._skip_hrd_parameters=function(t,i,n){var r=0,a=0;if(i&&(r=t.readBits(1),a=t.readBits(1),r||a)){var s=t.readBits(1);s&&t.readBits(19),t.readByte(),s&&t.readBits(4),t.readBits(15)}for(var o=0;o<=n;o++){var u=0,l=0,h=0,d=t.readBits(1);hvcc.fps_fixed=d,d||(h=t.readBits(1)),h?t.readUEG():l=t.readBits(1),l||(u=t.readUEG(t)),r&&e._skip_sub_layer_hrd_parameters(t,u,0),a&&e._skip_sub_layer_hrd_parameters(t,u,0)}},e.getProfileString=function(e){switch(e){case 1:return"Main";case 2:return"Main10";case 3:return"MainSP";case 4:return"Rext";case 9:return"SCC";default:return"Unknown"}},e.getLevelString=function(e){return(e/30).toFixed(1)},e.getChromaFormatString=function(e){switch(e){case 0:return"4:0:0";case 1:return"4:2:0";case 2:return"4:2:2";case 3:return"4:4:4";default:return"Unknown"}},e}();t.default=a},"./src/demux/sps-parser.js": +/*!*********************************!*\ + !*** ./src/demux/sps-parser.js ***! + \*********************************/ +function(e,t,i){i.r(t);var n=i( +/*! ./exp-golomb.js */ +"./src/demux/exp-golomb.js"),r=function(){function e(){}return e._ebsp2rbsp=function(e){for(var t=e,i=t.byteLength,n=new Uint8Array(i),r=0,a=0;a=2&&3===t[a]&&0===t[a-1]&&0===t[a-2]||(n[r]=t[a],r++);return new Uint8Array(n.buffer,0,r)},e.parseSPS=function(t){var i=e._ebsp2rbsp(t),r=new n.default(i);r.readByte();var a=r.readByte();r.readByte();var s=r.readByte();r.readUEG();var o=e.getProfileString(a),u=e.getLevelString(s),l=1,h=420,d=8;if((100===a||110===a||122===a||244===a||44===a||83===a||86===a||118===a||128===a||138===a||144===a)&&(3===(l=r.readUEG())&&r.readBits(1),l<=3&&(h=[0,420,422,444][l]),d=r.readUEG()+8,r.readUEG(),r.readBits(1),r.readBool()))for(var c=3!==l?8:12,f=0;f0&&L<16?(w=[1,12,10,16,40,24,20,32,80,18,15,64,160,4,3,2][L-1],A=[1,11,11,11,33,11,11,11,33,11,11,33,99,3,2,1][L-1]):255===L&&(w=r.readByte()<<8|r.readByte(),A=r.readByte()<<8|r.readByte())}if(r.readBool()&&r.readBool(),r.readBool()&&(r.readBits(4),r.readBool()&&r.readBits(24)),r.readBool()&&(r.readUEG(),r.readUEG()),r.readBool()){var x=r.readBits(32),R=r.readBits(32);k=r.readBool(),C=(P=R)/(I=2*x)}}var D=1;1===w&&1===A||(D=w/A);var O=0,U=0;0===l?(O=1,U=2-y):(O=3===l?1:2,U=(1===l?2:1)*(2-y));var M=16*(g+1),F=16*(v+1)*(2-y);M-=(b+S)*O,F-=(T+E)*U;var B=Math.ceil(M*D);return r.destroy(),r=null,{profile_string:o,level_string:u,bit_depth:d,ref_frames:_,chroma_format:h,chroma_format_string:e.getChromaFormatString(h),frame_rate:{fixed:k,fps:C,fps_den:I,fps_num:P},sar_ratio:{width:w,height:A},codec_size:{width:M,height:F},present_size:{width:B,height:F}}},e._skipScalingList=function(e,t){for(var i=8,n=8,r=0;r=15048,t=!a.default.msedge||e;return self.fetch&&self.ReadableStream&&t}catch(e){return!1}},t.prototype.destroy=function(){this.isWorking()&&this.abort(),e.prototype.destroy.call(this)},t.prototype.open=function(e,t){var i=this;this._dataSource=e,this._range=t;var r=e.url;this._config.reuseRedirectedURL&&null!=e.redirectedURL&&(r=e.redirectedURL);var a=this._seekHandler.getConfig(r,t),u=new self.Headers;if("object"===n(a.headers)){var l=a.headers;for(var h in l)l.hasOwnProperty(h)&&u.append(h,l[h])}var d={method:"GET",headers:u,mode:"cors",cache:"default",referrerPolicy:"no-referrer-when-downgrade"};if("object"===n(this._config.headers))for(var h in this._config.headers)u.append(h,this._config.headers[h]);!1===e.cors&&(d.mode="same-origin"),e.withCredentials&&(d.credentials="include"),e.referrerPolicy&&(d.referrerPolicy=e.referrerPolicy),self.AbortController&&(this._abortController=new self.AbortController,d.signal=this._abortController.signal),this._status=s.LoaderStatus.kConnecting,self.fetch(a.url,d).then((function(e){if(i._requestAbort)return i._status=s.LoaderStatus.kIdle,void e.body.cancel();if(e.ok&&e.status>=200&&e.status<=299){if(e.url!==a.url&&i._onURLRedirect){var t=i._seekHandler.removeURLParameters(e.url);i._onURLRedirect(t)}var n=e.headers.get("Content-Length");return null!=n&&(i._contentLength=parseInt(n),0!==i._contentLength&&i._onContentLengthKnown&&i._onContentLengthKnown(i._contentLength)),i._pump.call(i,e.body.getReader())}if(i._status=s.LoaderStatus.kError,!i._onError)throw new o.RuntimeException("FetchStreamLoader: Http code invalid, "+e.status+" "+e.statusText);i._onError(s.LoaderErrors.HTTP_STATUS_CODE_INVALID,{code:e.status,msg:e.statusText})})).catch((function(e){if(!i._abortController||!i._abortController.signal.aborted){if(i._status=s.LoaderStatus.kError,!i._onError)throw e;i._onError(s.LoaderErrors.EXCEPTION,{code:-1,msg:e.message})}}))},t.prototype.abort=function(){if(this._requestAbort=!0,(this._status!==s.LoaderStatus.kBuffering||!a.default.chrome)&&this._abortController)try{this._abortController.abort()}catch(e){}},t.prototype._pump=function(e){var t=this;return e.read().then((function(i){if(i.done)if(null!==t._contentLength&&t._receivedLength0&&(this._stashInitialSize=t.stashInitialSize),this._stashUsed=0,this._stashSize=this._stashInitialSize,this._bufferSize=3145728,this._stashBuffer=new ArrayBuffer(this._bufferSize),this._stashByteStart=0,this._enableStash=!0,!1===t.enableStashBuffer&&(this._enableStash=!1),this._loader=null,this._loaderClass=null,this._seekHandler=null,this._dataSource=e,this._isWebSocketURL=/wss?:\/\/(.+?)/.test(e.url),this._refTotalLength=e.filesize?e.filesize:null,this._totalLength=this._refTotalLength,this._fullRequestFlag=!1,this._currentRange=null,this._redirectedURL=null,this._speedNormalized=0,this._speedSampler=new r.default,this._speedNormalizeList=[64,128,256,384,512,768,1024,1536,2048,3072,4096],this._isEarlyEofReconnecting=!1,this._paused=!1,this._resumeFrom=0,this._onDataArrival=null,this._onSeeked=null,this._onError=null,this._onComplete=null,this._onRedirect=null,this._onRecoveredEarlyEof=null,this._selectSeekHandler(),this._selectLoader(),this._createLoader()}return e.prototype.destroy=function(){this._loader.isWorking()&&this._loader.abort(),this._loader.destroy(),this._loader=null,this._loaderClass=null,this._dataSource=null,this._stashBuffer=null,this._stashUsed=this._stashSize=this._bufferSize=this._stashByteStart=0,this._currentRange=null,this._speedSampler=null,this._isEarlyEofReconnecting=!1,this._onDataArrival=null,this._onSeeked=null,this._onError=null,this._onComplete=null,this._onRedirect=null,this._onRecoveredEarlyEof=null,this._extraData=null},e.prototype.isWorking=function(){return this._loader&&this._loader.isWorking()&&!this._paused},e.prototype.isPaused=function(){return this._paused},Object.defineProperty(e.prototype,"status",{get:function(){return this._loader.status},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"extraData",{get:function(){return this._extraData},set:function(e){this._extraData=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onDataArrival",{get:function(){return this._onDataArrival},set:function(e){this._onDataArrival=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onSeeked",{get:function(){return this._onSeeked},set:function(e){this._onSeeked=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onError",{get:function(){return this._onError},set:function(e){this._onError=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onComplete",{get:function(){return this._onComplete},set:function(e){this._onComplete=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onRedirect",{get:function(){return this._onRedirect},set:function(e){this._onRedirect=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onRecoveredEarlyEof",{get:function(){return this._onRecoveredEarlyEof},set:function(e){this._onRecoveredEarlyEof=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"currentURL",{get:function(){return this._dataSource.url},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"hasRedirect",{get:function(){return null!=this._redirectedURL||null!=this._dataSource.redirectedURL},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"currentRedirectedURL",{get:function(){return this._redirectedURL||this._dataSource.redirectedURL},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"currentSpeed",{get:function(){return this._loaderClass===u.default?this._loader.currentSpeed:this._speedSampler.lastSecondKBps},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"loaderType",{get:function(){return this._loader.type},enumerable:!1,configurable:!0}),e.prototype._selectSeekHandler=function(){var e=this._config;if("range"===e.seekType)this._seekHandler=new h.default(this._config.rangeLoadZeroStart);else if("param"===e.seekType){var t=e.seekParamStart||"bstart",i=e.seekParamEnd||"bend";this._seekHandler=new d.default(t,i)}else{if("custom"!==e.seekType)throw new c.InvalidArgumentException("Invalid seekType in config: "+e.seekType);if("function"!=typeof e.customSeekHandler)throw new c.InvalidArgumentException("Custom seekType specified in config but invalid customSeekHandler!");this._seekHandler=new e.customSeekHandler}},e.prototype._selectLoader=function(){if(null!=this._config.customLoader)this._loaderClass=this._config.customLoader;else if(this._isWebSocketURL)this._loaderClass=l.default;else if(s.default.isSupported())this._loaderClass=s.default;else if(o.default.isSupported())this._loaderClass=o.default;else{if(!u.default.isSupported())throw new c.RuntimeException("Your browser doesn't support xhr with arraybuffer responseType!");this._loaderClass=u.default}},e.prototype._createLoader=function(){this._loader=new this._loaderClass(this._seekHandler,this._config),!1===this._loader.needStashBuffer&&(this._enableStash=!1),this._loader.onContentLengthKnown=this._onContentLengthKnown.bind(this),this._loader.onURLRedirect=this._onURLRedirect.bind(this),this._loader.onDataArrival=this._onLoaderChunkArrival.bind(this),this._loader.onComplete=this._onLoaderComplete.bind(this),this._loader.onError=this._onLoaderError.bind(this)},e.prototype.open=function(e){this._currentRange={from:0,to:-1},e&&(this._currentRange.from=e),this._speedSampler.reset(),e||(this._fullRequestFlag=!0),this._loader.open(this._dataSource,Object.assign({},this._currentRange))},e.prototype.abort=function(){this._loader.abort(),this._paused&&(this._paused=!1,this._resumeFrom=0)},e.prototype.pause=function(){this.isWorking()&&(this._loader.abort(),0!==this._stashUsed?(this._resumeFrom=this._stashByteStart,this._currentRange.to=this._stashByteStart-1):this._resumeFrom=this._currentRange.to+1,this._stashUsed=0,this._stashByteStart=0,this._paused=!0)},e.prototype.resume=function(){if(this._paused){this._paused=!1;var e=this._resumeFrom;this._resumeFrom=0,this._internalSeek(e,!0)}},e.prototype.seek=function(e){this._paused=!1,this._stashUsed=0,this._stashByteStart=0,this._internalSeek(e,!0)},e.prototype._internalSeek=function(e,t){this._loader.isWorking()&&this._loader.abort(),this._flushStashBuffer(t),this._loader.destroy(),this._loader=null;var i={from:e,to:-1};this._currentRange={from:i.from,to:-1},this._speedSampler.reset(),this._stashSize=this._stashInitialSize,this._createLoader(),this._loader.open(this._dataSource,i),this._onSeeked&&this._onSeeked()},e.prototype.updateUrl=function(e){if(!e||"string"!=typeof e||0===e.length)throw new c.InvalidArgumentException("Url must be a non-empty string!");this._dataSource.url=e},e.prototype._expandBuffer=function(e){for(var t=this._stashSize;t+10485760){var n=new Uint8Array(this._stashBuffer,0,this._stashUsed);new Uint8Array(i,0,t).set(n,0)}this._stashBuffer=i,this._bufferSize=t}},e.prototype._normalizeSpeed=function(e){var t=this._speedNormalizeList,i=t.length-1,n=0,r=0,a=i;if(e=t[n]&&e=512&&e<=1024?Math.floor(1.5*e):2*e)>8192&&(t=8192);var i=1024*t+1048576;this._bufferSize0){var a=this._stashBuffer.slice(0,this._stashUsed);(u=this._dispatchChunks(a,this._stashByteStart))0&&(l=new Uint8Array(a,u),o.set(l,0),this._stashUsed=l.byteLength,this._stashByteStart+=u):(this._stashUsed=0,this._stashByteStart+=u),this._stashUsed+e.byteLength>this._bufferSize&&(this._expandBuffer(this._stashUsed+e.byteLength),o=new Uint8Array(this._stashBuffer,0,this._bufferSize)),o.set(new Uint8Array(e),this._stashUsed),this._stashUsed+=e.byteLength}else(u=this._dispatchChunks(e,t))this._bufferSize&&(this._expandBuffer(s),o=new Uint8Array(this._stashBuffer,0,this._bufferSize)),o.set(new Uint8Array(e,u),0),this._stashUsed+=s,this._stashByteStart=t+u);else if(0===this._stashUsed){var s;(u=this._dispatchChunks(e,t))this._bufferSize&&this._expandBuffer(s),(o=new Uint8Array(this._stashBuffer,0,this._bufferSize)).set(new Uint8Array(e,u),0),this._stashUsed+=s,this._stashByteStart=t+u)}else{var o,u;if(this._stashUsed+e.byteLength>this._bufferSize&&this._expandBuffer(this._stashUsed+e.byteLength),(o=new Uint8Array(this._stashBuffer,0,this._bufferSize)).set(new Uint8Array(e),this._stashUsed),this._stashUsed+=e.byteLength,(u=this._dispatchChunks(this._stashBuffer.slice(0,this._stashUsed),this._stashByteStart))0){var l=new Uint8Array(this._stashBuffer,u);o.set(l,0)}this._stashUsed-=u,this._stashByteStart+=u}}},e.prototype._flushStashBuffer=function(e){if(this._stashUsed>0){var t=this._stashBuffer.slice(0,this._stashUsed),i=this._dispatchChunks(t,this._stashByteStart),r=t.byteLength-i;if(i0){var a=new Uint8Array(this._stashBuffer,0,this._bufferSize),s=new Uint8Array(t,i);a.set(s,0),this._stashUsed=s.byteLength,this._stashByteStart+=i}return 0}n.default.w(this.TAG,r+" bytes unconsumed data remain when flush buffer, dropped")}return this._stashUsed=0,this._stashByteStart=0,r}return 0},e.prototype._onLoaderComplete=function(e,t){this._flushStashBuffer(!0),this._onComplete&&this._onComplete(this._extraData)},e.prototype._onLoaderError=function(e,t){switch(n.default.e(this.TAG,"Loader error, code = "+t.code+", msg = "+t.msg),this._flushStashBuffer(!1),this._isEarlyEofReconnecting&&(this._isEarlyEofReconnecting=!1,e=a.LoaderErrors.UNRECOVERABLE_EARLY_EOF),e){case a.LoaderErrors.EARLY_EOF:if(!this._config.isLive&&this._totalLength){var i=this._currentRange.to+1;return void(i0)for(var a=i.split("&"),s=0;s0;o[0]!==this._startName&&o[0]!==this._endName&&(u&&(r+="&"),r+=a[s])}return 0===r.length?t:t+"?"+r},e}();t.default=n},"./src/io/range-seek-handler.js": +/*!**************************************!*\ + !*** ./src/io/range-seek-handler.js ***! + \**************************************/ +function(e,t,i){i.r(t);var n=function(){function e(e){this._zeroStart=e||!1}return e.prototype.getConfig=function(e,t){var i={};if(0!==t.from||-1!==t.to){var n=void 0;n=-1!==t.to?"bytes="+t.from.toString()+"-"+t.to.toString():"bytes="+t.from.toString()+"-",i.Range=n}else this._zeroStart&&(i.Range="bytes=0-");return{url:e,headers:i}},e.prototype.removeURLParameters=function(e){return e},e}();t.default=n},"./src/io/speed-sampler.js": +/*!*********************************!*\ + !*** ./src/io/speed-sampler.js ***! + \*********************************/ +function(e,t,i){i.r(t);var n=function(){function e(){this._firstCheckpoint=0,this._lastCheckpoint=0,this._intervalBytes=0,this._totalBytes=0,this._lastSecondBytes=0,self.performance&&self.performance.now?this._now=self.performance.now.bind(self.performance):this._now=Date.now}return e.prototype.reset=function(){this._firstCheckpoint=this._lastCheckpoint=0,this._totalBytes=this._intervalBytes=0,this._lastSecondBytes=0},e.prototype.addBytes=function(e){0===this._firstCheckpoint?(this._firstCheckpoint=this._now(),this._lastCheckpoint=this._firstCheckpoint,this._intervalBytes+=e,this._totalBytes+=e):this._now()-this._lastCheckpoint<1e3?(this._intervalBytes+=e,this._totalBytes+=e):(this._lastSecondBytes=this._intervalBytes,this._intervalBytes=e,this._totalBytes+=e,this._lastCheckpoint=this._now())},Object.defineProperty(e.prototype,"currentKBps",{get:function(){this.addBytes(0);var e=(this._now()-this._lastCheckpoint)/1e3;return 0==e&&(e=1),this._intervalBytes/e/1024},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"lastSecondKBps",{get:function(){return this.addBytes(0),0!==this._lastSecondBytes?this._lastSecondBytes/1024:this._now()-this._lastCheckpoint>=500?this.currentKBps:0},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"averageKBps",{get:function(){var e=(this._now()-this._firstCheckpoint)/1e3;return this._totalBytes/e/1024},enumerable:!1,configurable:!0}),e}();t.default=n},"./src/io/websocket-loader.js": +/*!************************************!*\ + !*** ./src/io/websocket-loader.js ***! + \************************************/ +function(e,t,i){i.r(t);var n,r=i( +/*! ./loader.js */ +"./src/io/loader.js"),a=i( +/*! ../utils/exception.js */ +"./src/utils/exception.js"),s=(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])})(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)}),o=function(e){function t(){var t=e.call(this,"websocket-loader")||this;return t.TAG="WebSocketLoader",t._needStash=!0,t._ws=null,t._requestAbort=!1,t._receivedLength=0,t}return s(t,e),t.isSupported=function(){try{return void 0!==self.WebSocket}catch(e){return!1}},t.prototype.destroy=function(){this._ws&&this.abort(),e.prototype.destroy.call(this)},t.prototype.open=function(e){try{var t=this._ws=new self.WebSocket(e.url);t.binaryType="arraybuffer",t.onopen=this._onWebSocketOpen.bind(this),t.onclose=this._onWebSocketClose.bind(this),t.onmessage=this._onWebSocketMessage.bind(this),t.onerror=this._onWebSocketError.bind(this),this._status=r.LoaderStatus.kConnecting}catch(e){this._status=r.LoaderStatus.kError;var i={code:e.code,msg:e.message};if(!this._onError)throw new a.RuntimeException(i.msg);this._onError(r.LoaderErrors.EXCEPTION,i)}},t.prototype.abort=function(){var e=this._ws;!e||0!==e.readyState&&1!==e.readyState||(this._requestAbort=!0,e.close()),this._ws=null,this._status=r.LoaderStatus.kComplete},t.prototype._onWebSocketOpen=function(e){this._status=r.LoaderStatus.kBuffering},t.prototype._onWebSocketClose=function(e){!0!==this._requestAbort?(this._status=r.LoaderStatus.kComplete,this._onComplete&&this._onComplete(0,this._receivedLength-1)):this._requestAbort=!1},t.prototype._onWebSocketMessage=function(e){var t=this;if(e.data instanceof ArrayBuffer)this._dispatchArrayBuffer(e.data);else if(e.data instanceof Blob){var i=new FileReader;i.onload=function(){t._dispatchArrayBuffer(i.result)},i.readAsArrayBuffer(e.data)}else{this._status=r.LoaderStatus.kError;var n={code:-1,msg:"Unsupported WebSocket message type: "+e.data.constructor.name};if(!this._onError)throw new a.RuntimeException(n.msg);this._onError(r.LoaderErrors.EXCEPTION,n)}},t.prototype._dispatchArrayBuffer=function(e){var t=e,i=this._receivedLength;this._receivedLength+=t.byteLength,this._onDataArrival&&this._onDataArrival(t,i,this._receivedLength)},t.prototype._onWebSocketError=function(e){this._status=r.LoaderStatus.kError;var t={code:e.code,msg:e.message};if(!this._onError)throw new a.RuntimeException(t.msg);this._onError(r.LoaderErrors.EXCEPTION,t)},t}(r.BaseLoader);t.default=o},"./src/io/xhr-moz-chunked-loader.js": +/*!******************************************!*\ + !*** ./src/io/xhr-moz-chunked-loader.js ***! + \******************************************/ +function(e,t,i){i.r(t);var r,a=i( +/*! ../utils/logger.js */ +"./src/utils/logger.js"),s=i( +/*! ./loader.js */ +"./src/io/loader.js"),o=i( +/*! ../utils/exception.js */ +"./src/utils/exception.js"),u=(r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])})(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function i(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)}),l=function(e){function t(t,i){var n=e.call(this,"xhr-moz-chunked-loader")||this;return n.TAG="MozChunkedLoader",n._seekHandler=t,n._config=i,n._needStash=!0,n._xhr=null,n._requestAbort=!1,n._contentLength=null,n._receivedLength=0,n}return u(t,e),t.isSupported=function(){try{var e=new XMLHttpRequest;return e.open("GET","https://example.com",!0),e.responseType="moz-chunked-arraybuffer","moz-chunked-arraybuffer"===e.responseType}catch(e){return a.default.w("MozChunkedLoader",e.message),!1}},t.prototype.destroy=function(){this.isWorking()&&this.abort(),this._xhr&&(this._xhr.onreadystatechange=null,this._xhr.onprogress=null,this._xhr.onloadend=null,this._xhr.onerror=null,this._xhr=null),e.prototype.destroy.call(this)},t.prototype.open=function(e,t){this._dataSource=e,this._range=t;var i=e.url;this._config.reuseRedirectedURL&&null!=e.redirectedURL&&(i=e.redirectedURL);var r=this._seekHandler.getConfig(i,t);this._requestURL=r.url;var a=this._xhr=new XMLHttpRequest;if(a.open("GET",r.url,!0),a.responseType="moz-chunked-arraybuffer",a.onreadystatechange=this._onReadyStateChange.bind(this),a.onprogress=this._onProgress.bind(this),a.onloadend=this._onLoadEnd.bind(this),a.onerror=this._onXhrError.bind(this),e.withCredentials&&(a.withCredentials=!0),"object"===n(r.headers)){var o=r.headers;for(var u in o)o.hasOwnProperty(u)&&a.setRequestHeader(u,o[u])}if("object"===n(this._config.headers))for(var u in o=this._config.headers)o.hasOwnProperty(u)&&a.setRequestHeader(u,o[u]);this._status=s.LoaderStatus.kConnecting,a.send()},t.prototype.abort=function(){this._requestAbort=!0,this._xhr&&this._xhr.abort(),this._status=s.LoaderStatus.kComplete},t.prototype._onReadyStateChange=function(e){var t=e.target;if(2===t.readyState){if(null!=t.responseURL&&t.responseURL!==this._requestURL&&this._onURLRedirect){var i=this._seekHandler.removeURLParameters(t.responseURL);this._onURLRedirect(i)}if(0!==t.status&&(t.status<200||t.status>299)){if(this._status=s.LoaderStatus.kError,!this._onError)throw new o.RuntimeException("MozChunkedLoader: Http code invalid, "+t.status+" "+t.statusText);this._onError(s.LoaderErrors.HTTP_STATUS_CODE_INVALID,{code:t.status,msg:t.statusText})}else this._status=s.LoaderStatus.kBuffering}},t.prototype._onProgress=function(e){if(this._status!==s.LoaderStatus.kError){null===this._contentLength&&null!==e.total&&0!==e.total&&(this._contentLength=e.total,this._onContentLengthKnown&&this._onContentLengthKnown(this._contentLength));var t=e.target.response,i=this._range.from+this._receivedLength;this._receivedLength+=t.byteLength,this._onDataArrival&&this._onDataArrival(t,i,this._receivedLength)}},t.prototype._onLoadEnd=function(e){!0!==this._requestAbort?this._status!==s.LoaderStatus.kError&&(this._status=s.LoaderStatus.kComplete,this._onComplete&&this._onComplete(this._range.from,this._range.from+this._receivedLength-1)):this._requestAbort=!1},t.prototype._onXhrError=function(e){this._status=s.LoaderStatus.kError;var t=0,i=null;if(this._contentLength&&e.loaded=this._contentLength&&(i=this._range.from+this._contentLength-1),this._currentRequestRange={from:t,to:i},this._internalOpen(this._dataSource,this._currentRequestRange)},t.prototype._internalOpen=function(e,t){this._lastTimeLoaded=0;var i=e.url;this._config.reuseRedirectedURL&&(null!=this._currentRedirectedURL?i=this._currentRedirectedURL:null!=e.redirectedURL&&(i=e.redirectedURL));var r=this._seekHandler.getConfig(i,t);this._currentRequestURL=r.url;var a=this._xhr=new XMLHttpRequest;if(a.open("GET",r.url,!0),a.responseType="arraybuffer",a.onreadystatechange=this._onReadyStateChange.bind(this),a.onprogress=this._onProgress.bind(this),a.onload=this._onLoad.bind(this),a.onerror=this._onXhrError.bind(this),e.withCredentials&&(a.withCredentials=!0),"object"===n(r.headers)){var s=r.headers;for(var o in s)s.hasOwnProperty(o)&&a.setRequestHeader(o,s[o])}if("object"===n(this._config.headers))for(var o in s=this._config.headers)s.hasOwnProperty(o)&&a.setRequestHeader(o,s[o]);a.send()},t.prototype.abort=function(){this._requestAbort=!0,this._internalAbort(),this._status=o.LoaderStatus.kComplete},t.prototype._internalAbort=function(){this._xhr&&(this._xhr.onreadystatechange=null,this._xhr.onprogress=null,this._xhr.onload=null,this._xhr.onerror=null,this._xhr.abort(),this._xhr=null)},t.prototype._onReadyStateChange=function(e){var t=e.target;if(2===t.readyState){if(null!=t.responseURL){var i=this._seekHandler.removeURLParameters(t.responseURL);t.responseURL!==this._currentRequestURL&&i!==this._currentRedirectedURL&&(this._currentRedirectedURL=i,this._onURLRedirect&&this._onURLRedirect(i))}if(t.status>=200&&t.status<=299){if(this._waitForTotalLength)return;this._status=o.LoaderStatus.kBuffering}else{if(this._status=o.LoaderStatus.kError,!this._onError)throw new u.RuntimeException("RangeLoader: Http code invalid, "+t.status+" "+t.statusText);this._onError(o.LoaderErrors.HTTP_STATUS_CODE_INVALID,{code:t.status,msg:t.statusText})}}},t.prototype._onProgress=function(e){if(this._status!==o.LoaderStatus.kError){if(null===this._contentLength){var t=!1;if(this._waitForTotalLength){this._waitForTotalLength=!1,this._totalLengthReceived=!0,t=!0;var i=e.total;this._internalAbort(),null!=i&0!==i&&(this._totalLength=i)}if(-1===this._range.to?this._contentLength=this._totalLength-this._range.from:this._contentLength=this._range.to-this._range.from+1,t)return void this._openSubRange();this._onContentLengthKnown&&this._onContentLengthKnown(this._contentLength)}var n=e.loaded-this._lastTimeLoaded;this._lastTimeLoaded=e.loaded,this._speedSampler.addBytes(n)}},t.prototype._normalizeSpeed=function(e){var t=this._chunkSizeKBList,i=t.length-1,n=0,r=0,a=i;if(e=t[n]&&e=3&&(t=this._speedSampler.currentKBps)),0!==t){var i=this._normalizeSpeed(t);this._currentSpeedNormalized!==i&&(this._currentSpeedNormalized=i,this._currentChunkSizeKB=i)}var n=e.target.response,r=this._range.from+this._receivedLength;this._receivedLength+=n.byteLength;var a=!1;null!=this._contentLength&&this._receivedLength0&&this._receivedLength0&&(this._requestSetTime=!0,this._mediaElement.currentTime=0),this._transmuxer=new l.default(this._mediaDataSource,this._config),this._transmuxer.on(h.default.INIT_SEGMENT,(function(t,i){e._msectl.appendInitSegment(i)})),this._transmuxer.on(h.default.MEDIA_SEGMENT,(function(t,i){if(e._msectl.appendMediaSegment(i),e._config.lazyLoad&&!e._config.isLive){var n=e._mediaElement.currentTime;i.info.endDts>=1e3*(n+e._config.lazyLoadMaxDuration)&&null==e._progressChecker&&(s.default.v(e.TAG,"Maximum buffering duration exceeded, suspend transmuxing task"),e._suspendTransmuxer())}})),this._transmuxer.on(h.default.LOADING_COMPLETE,(function(){e._msectl.endOfStream(),e._emitter.emit(u.default.LOADING_COMPLETE)})),this._transmuxer.on(h.default.RECOVERED_EARLY_EOF,(function(){e._emitter.emit(u.default.RECOVERED_EARLY_EOF)})),this._transmuxer.on(h.default.IO_ERROR,(function(t,i){e._emitter.emit(u.default.ERROR,f.ErrorTypes.NETWORK_ERROR,t,i)})),this._transmuxer.on(h.default.DEMUX_ERROR,(function(t,i){e._emitter.emit(u.default.ERROR,f.ErrorTypes.MEDIA_ERROR,t,{code:-1,msg:i})})),this._transmuxer.on(h.default.MEDIA_INFO,(function(t){e._mediaInfo=t,e._emitter.emit(u.default.MEDIA_INFO,Object.assign({},t))})),this._transmuxer.on(h.default.METADATA_ARRIVED,(function(t){e._emitter.emit(u.default.METADATA_ARRIVED,t)})),this._transmuxer.on(h.default.SCRIPTDATA_ARRIVED,(function(t){e._emitter.emit(u.default.SCRIPTDATA_ARRIVED,t)})),this._transmuxer.on(h.default.STATISTICS_INFO,(function(t){e._statisticsInfo=e._fillStatisticsInfo(t),e._emitter.emit(u.default.STATISTICS_INFO,Object.assign({},e._statisticsInfo))})),this._transmuxer.on(h.default.RECOMMEND_SEEKPOINT,(function(t){e._mediaElement&&!e._config.accurateSeek&&(e._requestSetTime=!0,e._mediaElement.currentTime=t/1e3)})),this._transmuxer.open()))},e.prototype.unload=function(){this._mediaElement&&this._mediaElement.pause(),this._msectl&&this._msectl.seek(0),this._transmuxer&&(this._transmuxer.close(),this._transmuxer.destroy(),this._transmuxer=null)},e.prototype.play=function(){return this._mediaElement.play()},e.prototype.pause=function(){this._mediaElement.pause()},Object.defineProperty(e.prototype,"type",{get:function(){return this._type},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"buffered",{get:function(){return this._mediaElement.buffered},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"duration",{get:function(){return this._mediaElement.duration},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"volume",{get:function(){return this._mediaElement.volume},set:function(e){this._mediaElement.volume=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"muted",{get:function(){return this._mediaElement.muted},set:function(e){this._mediaElement.muted=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"currentTime",{get:function(){return this._mediaElement?this._mediaElement.currentTime:0},set:function(e){this._mediaElement?this._internalSeek(e):this._pendingSeekTime=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"mediaInfo",{get:function(){return Object.assign({},this._mediaInfo)},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"statisticsInfo",{get:function(){return null==this._statisticsInfo&&(this._statisticsInfo={}),this._statisticsInfo=this._fillStatisticsInfo(this._statisticsInfo),Object.assign({},this._statisticsInfo)},enumerable:!1,configurable:!0}),e.prototype._fillStatisticsInfo=function(e){if(e.playerType=this._type,!(this._mediaElement instanceof HTMLVideoElement))return e;var t=!0,i=0,n=0;if(this._mediaElement.getVideoPlaybackQuality){var r=this._mediaElement.getVideoPlaybackQuality();i=r.totalVideoFrames,n=r.droppedVideoFrames}else null!=this._mediaElement.webkitDecodedFrameCount?(i=this._mediaElement.webkitDecodedFrameCount,n=this._mediaElement.webkitDroppedFrameCount):t=!1;return t&&(e.decodedFrames=i,e.droppedFrames=n),e},e.prototype._onmseUpdateEnd=function(){if(this._config.lazyLoad&&!this._config.isLive){for(var e=this._mediaElement.buffered,t=this._mediaElement.currentTime,i=0,n=0;n=t+this._config.lazyLoadMaxDuration&&null==this._progressChecker&&(s.default.v(this.TAG,"Maximum buffering duration exceeded, suspend transmuxing task"),this._suspendTransmuxer())}},e.prototype._onmseBufferFull=function(){s.default.v(this.TAG,"MSE SourceBuffer is full, suspend transmuxing task"),null==this._progressChecker&&this._suspendTransmuxer()},e.prototype._suspendTransmuxer=function(){this._transmuxer&&(this._transmuxer.pause(),null==this._progressChecker&&(this._progressChecker=window.setInterval(this._checkProgressAndResume.bind(this),1e3)))},e.prototype._checkProgressAndResume=function(){for(var e=this._mediaElement.currentTime,t=this._mediaElement.buffered,i=!1,n=0;n=r&&e=a-this._config.lazyLoadRecoverDuration&&(i=!0);break}}i&&(window.clearInterval(this._progressChecker),this._progressChecker=null,i&&(s.default.v(this.TAG,"Continue loading from paused position"),this._transmuxer.resume()))},e.prototype._isTimepointBuffered=function(e){for(var t=this._mediaElement.buffered,i=0;i=n&&e0){var r=this._mediaElement.buffered.start(0);(r<1&&e0&&t.currentTime0){var n=i.start(0);if(n<1&&t0&&(this._mediaElement.currentTime=0),this._mediaElement.preload="auto",this._mediaElement.load(),this._statisticsReporter=window.setInterval(this._reportStatisticsInfo.bind(this),this._config.statisticsInfoReportInterval)},e.prototype.unload=function(){this._mediaElement&&(this._mediaElement.src="",this._mediaElement.removeAttribute("src")),null!=this._statisticsReporter&&(window.clearInterval(this._statisticsReporter),this._statisticsReporter=null)},e.prototype.play=function(){return this._mediaElement.play()},e.prototype.pause=function(){this._mediaElement.pause()},Object.defineProperty(e.prototype,"type",{get:function(){return this._type},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"buffered",{get:function(){return this._mediaElement.buffered},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"duration",{get:function(){return this._mediaElement.duration},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"volume",{get:function(){return this._mediaElement.volume},set:function(e){this._mediaElement.volume=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"muted",{get:function(){return this._mediaElement.muted},set:function(e){this._mediaElement.muted=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"currentTime",{get:function(){return this._mediaElement?this._mediaElement.currentTime:0},set:function(e){this._mediaElement?this._mediaElement.currentTime=e:this._pendingSeekTime=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"mediaInfo",{get:function(){var e={mimeType:(this._mediaElement instanceof HTMLAudioElement?"audio/":"video/")+this._mediaDataSource.type};return this._mediaElement&&(e.duration=Math.floor(1e3*this._mediaElement.duration),this._mediaElement instanceof HTMLVideoElement&&(e.width=this._mediaElement.videoWidth,e.height=this._mediaElement.videoHeight)),e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"statisticsInfo",{get:function(){var e={playerType:this._type,url:this._mediaDataSource.url};if(!(this._mediaElement instanceof HTMLVideoElement))return e;var t=!0,i=0,n=0;if(this._mediaElement.getVideoPlaybackQuality){var r=this._mediaElement.getVideoPlaybackQuality();i=r.totalVideoFrames,n=r.droppedVideoFrames}else null!=this._mediaElement.webkitDecodedFrameCount?(i=this._mediaElement.webkitDecodedFrameCount,n=this._mediaElement.webkitDroppedFrameCount):t=!1;return t&&(e.decodedFrames=i,e.droppedFrames=n),e},enumerable:!1,configurable:!0}),e.prototype._onvLoadedMetadata=function(e){null!=this._pendingSeekTime&&(this._mediaElement.currentTime=this._pendingSeekTime,this._pendingSeekTime=null),this._emitter.emit(s.default.MEDIA_INFO,this.mediaInfo)},e.prototype._reportStatisticsInfo=function(){this._emitter.emit(s.default.STATISTICS_INFO,this.statisticsInfo)},e}();t.default=l},"./src/player/player-errors.js": +/*!*************************************!*\ + !*** ./src/player/player-errors.js ***! + \*************************************/ +function(e,t,i){i.r(t),i.d(t,{ErrorTypes:function(){return a},ErrorDetails:function(){return s}});var n=i( +/*! ../io/loader.js */ +"./src/io/loader.js"),r=i( +/*! ../demux/demux-errors.js */ +"./src/demux/demux-errors.js"),a={NETWORK_ERROR:"NetworkError",MEDIA_ERROR:"MediaError",OTHER_ERROR:"OtherError"},s={NETWORK_EXCEPTION:n.LoaderErrors.EXCEPTION,NETWORK_STATUS_CODE_INVALID:n.LoaderErrors.HTTP_STATUS_CODE_INVALID,NETWORK_TIMEOUT:n.LoaderErrors.CONNECTING_TIMEOUT,NETWORK_UNRECOVERABLE_EARLY_EOF:n.LoaderErrors.UNRECOVERABLE_EARLY_EOF,MEDIA_MSE_ERROR:"MediaMSEError",MEDIA_FORMAT_ERROR:r.default.FORMAT_ERROR,MEDIA_FORMAT_UNSUPPORTED:r.default.FORMAT_UNSUPPORTED,MEDIA_CODEC_UNSUPPORTED:r.default.CODEC_UNSUPPORTED}},"./src/player/player-events.js": +/*!*************************************!*\ + !*** ./src/player/player-events.js ***! + \*************************************/ +function(e,t,i){i.r(t),t.default={ERROR:"error",LOADING_COMPLETE:"loading_complete",RECOVERED_EARLY_EOF:"recovered_early_eof",MEDIA_INFO:"media_info",METADATA_ARRIVED:"metadata_arrived",SCRIPTDATA_ARRIVED:"scriptdata_arrived",STATISTICS_INFO:"statistics_info"}},"./src/remux/aac-silent.js": +/*!*********************************!*\ + !*** ./src/remux/aac-silent.js ***! + \*********************************/ +function(e,t,i){i.r(t);var n=function(){function e(){}return e.getSilentFrame=function(e,t){if("mp4a.40.2"===e){if(1===t)return new Uint8Array([0,200,0,128,35,128]);if(2===t)return new Uint8Array([33,0,73,144,2,25,0,35,128]);if(3===t)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,142]);if(4===t)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,128,44,128,8,2,56]);if(5===t)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,130,48,4,153,0,33,144,2,56]);if(6===t)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,130,48,4,153,0,33,144,2,0,178,0,32,8,224])}else{if(1===t)return new Uint8Array([1,64,34,128,163,78,230,128,186,8,0,0,0,28,6,241,193,10,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,94]);if(2===t)return new Uint8Array([1,64,34,128,163,94,230,128,186,8,0,0,0,0,149,0,6,241,161,10,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,94]);if(3===t)return new Uint8Array([1,64,34,128,163,94,230,128,186,8,0,0,0,0,149,0,6,241,161,10,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,94])}return null},e}();t.default=n},"./src/remux/mp4-generator.js": +/*!************************************!*\ + !*** ./src/remux/mp4-generator.js ***! + \************************************/ +function(e,t,i){i.r(t);var n=function(){function e(){}return e.init=function(){for(var t in e.types={hvc1:[],hvcC:[],avc1:[],avcC:[],btrt:[],dinf:[],dref:[],esds:[],ftyp:[],hdlr:[],mdat:[],mdhd:[],mdia:[],mfhd:[],minf:[],moof:[],moov:[],mp4a:[],mvex:[],mvhd:[],sdtp:[],stbl:[],stco:[],stsc:[],stsd:[],stsz:[],stts:[],tfdt:[],tfhd:[],traf:[],trak:[],trun:[],trex:[],tkhd:[],vmhd:[],smhd:[],pasp:[],".mp3":[]},e.types)e.types.hasOwnProperty(t)&&(e.types[t]=[t.charCodeAt(0),t.charCodeAt(1),t.charCodeAt(2),t.charCodeAt(3)]);var i=e.constants={};i.FTYP=new Uint8Array([105,115,111,109,0,0,0,1,105,115,111,109,97,118,99,49]),i.STSD_PREFIX=new Uint8Array([0,0,0,0,0,0,0,1]),i.STTS=new Uint8Array([0,0,0,0,0,0,0,0]),i.STSC=i.STCO=i.STTS,i.STSZ=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0]),i.HDLR_VIDEO=new Uint8Array([0,0,0,0,0,0,0,0,118,105,100,101,0,0,0,0,0,0,0,0,0,0,0,0,86,105,100,101,111,72,97,110,100,108,101,114,0]),i.HDLR_AUDIO=new Uint8Array([0,0,0,0,0,0,0,0,115,111,117,110,0,0,0,0,0,0,0,0,0,0,0,0,83,111,117,110,100,72,97,110,100,108,101,114,0]),i.DREF=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,12,117,114,108,32,0,0,0,1]),i.SMHD=new Uint8Array([0,0,0,0,0,0,0,0]),i.VMHD=new Uint8Array([0,0,0,1,0,0,0,0,0,0,0,0])},e.box=function(e){for(var t=8,i=null,n=Array.prototype.slice.call(arguments,1),r=n.length,a=0;a>>24&255,i[1]=t>>>16&255,i[2]=t>>>8&255,i[3]=255&t,i.set(e,4);var s=8;for(a=0;a>>24&255,t>>>16&255,t>>>8&255,255&t,i>>>24&255,i>>>16&255,i>>>8&255,255&i,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255]))},e.trak=function(t){return e.box(e.types.trak,e.tkhd(t),e.mdia(t))},e.tkhd=function(t){var i=t.id,n=t.duration,r=t.presentWidth,a=t.presentHeight;return e.box(e.types.tkhd,new Uint8Array([0,0,0,7,0,0,0,0,0,0,0,0,i>>>24&255,i>>>16&255,i>>>8&255,255&i,0,0,0,0,n>>>24&255,n>>>16&255,n>>>8&255,255&n,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,r>>>8&255,255&r,0,0,a>>>8&255,255&a,0,0]))},e.mdia=function(t){return e.box(e.types.mdia,e.mdhd(t),e.hdlr(t),e.minf(t))},e.mdhd=function(t){var i=t.timescale,n=t.duration;return e.box(e.types.mdhd,new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,i>>>24&255,i>>>16&255,i>>>8&255,255&i,n>>>24&255,n>>>16&255,n>>>8&255,255&n,85,196,0,0]))},e.hdlr=function(t){var i=null;return i="audio"===t.type?e.constants.HDLR_AUDIO:e.constants.HDLR_VIDEO,e.box(e.types.hdlr,i)},e.minf=function(t){var i=null;return i="audio"===t.type?e.box(e.types.smhd,e.constants.SMHD):e.box(e.types.vmhd,e.constants.VMHD),e.box(e.types.minf,i,e.dinf(),e.stbl(t))},e.dinf=function(){return e.box(e.types.dinf,e.box(e.types.dref,e.constants.DREF))},e.stbl=function(t){return e.box(e.types.stbl,e.stsd(t),e.box(e.types.stts,e.constants.STTS),e.box(e.types.stsc,e.constants.STSC),e.box(e.types.stsz,e.constants.STSZ),e.box(e.types.stco,e.constants.STCO))},e.stsd=function(t){return"audio"===t.type?"mp3"===t.codec?e.box(e.types.stsd,e.constants.STSD_PREFIX,e.mp3(t)):e.box(e.types.stsd,e.constants.STSD_PREFIX,e.mp4a(t)):e.box(e.types.stsd,e.constants.STSD_PREFIX,e.avc1(t))},e.mp3=function(t){var i=t.channelCount,n=t.audioSampleRate,r=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,i,0,16,0,0,0,0,n>>>8&255,255&n,0,0]);return e.box(e.types[".mp3"],r)},e.mp4a=function(t){var i=t.channelCount,n=t.audioSampleRate,r=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,i,0,16,0,0,0,0,n>>>8&255,255&n,0,0]);return e.box(e.types.mp4a,r,e.esds(t))},e.esds=function(t){var i=t.config||[],n=i.length,r=new Uint8Array([0,0,0,0,3,23+n,0,1,0,4,15+n,64,21,0,0,0,0,0,0,0,0,0,0,0,5].concat([n]).concat(i).concat([6,1,2]));return e.box(e.types.esds,r)},e.avc1=function(t){var i=t.avcc,n=t.codecWidth,r=t.codecHeight,a=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,n>>>8&255,255&n,r>>>8&255,255&r,0,72,0,0,0,72,0,0,0,0,0,0,0,1,10,120,113,113,47,102,108,118,46,106,115,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,255,255]);return t.codec.indexOf("avc1")>=0?e.box(e.types.avc1,a,e.box(e.types.avcC,i)):e.box(e.types.hvc1,a,e.box(e.types.hvcC,i))},e.mvex=function(t){return e.box(e.types.mvex,e.trex(t))},e.trex=function(t){var i=t.id,n=new Uint8Array([0,0,0,0,i>>>24&255,i>>>16&255,i>>>8&255,255&i,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1]);return e.box(e.types.trex,n)},e.moof=function(t,i){return e.box(e.types.moof,e.mfhd(t.sequenceNumber),e.traf(t,i))},e.mfhd=function(t){var i=new Uint8Array([0,0,0,0,t>>>24&255,t>>>16&255,t>>>8&255,255&t]);return e.box(e.types.mfhd,i)},e.traf=function(t,i){var n=t.id,r=e.box(e.types.tfhd,new Uint8Array([0,0,0,0,n>>>24&255,n>>>16&255,n>>>8&255,255&n])),a=e.box(e.types.tfdt,new Uint8Array([0,0,0,0,i>>>24&255,i>>>16&255,i>>>8&255,255&i])),s=e.sdtp(t),o=e.trun(t,s.byteLength+16+16+8+16+8+8);return e.box(e.types.traf,r,a,o,s)},e.sdtp=function(t){for(var i=t.samples||[],n=i.length,r=new Uint8Array(4+n),a=0;a>>24&255,r>>>16&255,r>>>8&255,255&r,i>>>24&255,i>>>16&255,i>>>8&255,255&i],0);for(var o=0;o>>24&255,u>>>16&255,u>>>8&255,255&u,l>>>24&255,l>>>16&255,l>>>8&255,255&l,h.isLeading<<2|h.dependsOn,h.isDependedOn<<6|h.hasRedundancy<<4|h.isNonSync,0,0,d>>>24&255,d>>>16&255,d>>>8&255,255&d],12+16*o)}return e.box(e.types.trun,s)},e.mdat=function(t){return e.box(e.types.mdat,t)},e}();n.init(),t.default=n},"./src/remux/mp4-remuxer.js": +/*!**********************************!*\ + !*** ./src/remux/mp4-remuxer.js ***! + \**********************************/ +function(e,t,i){i.r(t);var n=i( +/*! ../utils/logger.js */ +"./src/utils/logger.js"),r=i( +/*! ./mp4-generator.js */ +"./src/remux/mp4-generator.js"),a=i( +/*! ./aac-silent.js */ +"./src/remux/aac-silent.js"),s=i( +/*! ../utils/browser.js */ +"./src/utils/browser.js"),o=i( +/*! ../core/media-segment-info.js */ +"./src/core/media-segment-info.js"),u=i( +/*! ../utils/exception.js */ +"./src/utils/exception.js"),l=function(){function e(e){this.TAG="MP4Remuxer",this._config=e,this._isLive=!0===e.isLive,this._dtsBase=-1,this._dtsBaseInited=!1,this._audioDtsBase=1/0,this._videoDtsBase=1/0,this._audioNextDts=void 0,this._videoNextDts=void 0,this._audioStashedLastSample=null,this._videoStashedLastSample=null,this._audioMeta=null,this._videoMeta=null,this._audioSegmentInfoList=new o.MediaSegmentInfoList("audio"),this._videoSegmentInfoList=new o.MediaSegmentInfoList("video"),this._onInitSegment=null,this._onMediaSegment=null,this._forceFirstIDR=!(!s.default.chrome||!(s.default.version.major<50||50===s.default.version.major&&s.default.version.build<2661)),this._fillSilentAfterSeek=s.default.msedge||s.default.msie,this._mp3UseMpegAudio=!s.default.firefox,this._fillAudioTimestampGap=this._config.fixAudioTimestampGap}return e.prototype.destroy=function(){this._dtsBase=-1,this._dtsBaseInited=!1,this._audioMeta=null,this._videoMeta=null,this._audioSegmentInfoList.clear(),this._audioSegmentInfoList=null,this._videoSegmentInfoList.clear(),this._videoSegmentInfoList=null,this._onInitSegment=null,this._onMediaSegment=null},e.prototype.bindDataSource=function(e){return e.onDataAvailable=this.remux.bind(this),e.onTrackMetadata=this._onTrackMetadataReceived.bind(this),this},Object.defineProperty(e.prototype,"onInitSegment",{get:function(){return this._onInitSegment},set:function(e){this._onInitSegment=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onMediaSegment",{get:function(){return this._onMediaSegment},set:function(e){this._onMediaSegment=e},enumerable:!1,configurable:!0}),e.prototype.insertDiscontinuity=function(){this._audioNextDts=this._videoNextDts=void 0},e.prototype.seek=function(e){this._audioStashedLastSample=null,this._videoStashedLastSample=null,this._videoSegmentInfoList.clear(),this._audioSegmentInfoList.clear()},e.prototype.remux=function(e,t){if(!this._onMediaSegment)throw new u.IllegalStateException("MP4Remuxer: onMediaSegment callback must be specificed!");this._dtsBaseInited||this._calculateDtsBase(e,t),this._remuxVideo(t),this._remuxAudio(e)},e.prototype._onTrackMetadataReceived=function(e,t){var i=null,n="mp4",a=t.codec;if("audio"===e)this._audioMeta=t,"mp3"===t.codec&&this._mp3UseMpegAudio?(n="mpeg",a="",i=new Uint8Array):i=r.default.generateInitSegment(t);else{if("video"!==e)return;this._videoMeta=t,i=r.default.generateInitSegment(t)}if(!this._onInitSegment)throw new u.IllegalStateException("MP4Remuxer: onInitSegment callback must be specified!");this._onInitSegment(e,{type:e,data:i.buffer,codec:a,container:e+"/"+n,mediaDuration:t.duration})},e.prototype._calculateDtsBase=function(e,t){this._dtsBaseInited||(e.samples&&e.samples.length&&(this._audioDtsBase=e.samples[0].dts),t.samples&&t.samples.length&&(this._videoDtsBase=t.samples[0].dts),this._dtsBase=Math.min(this._audioDtsBase,this._videoDtsBase),this._dtsBaseInited=!0)},e.prototype.flushStashedSamples=function(){var e=this._videoStashedLastSample,t=this._audioStashedLastSample,i={type:"video",id:1,sequenceNumber:0,samples:[],length:0};null!=e&&(i.samples.push(e),i.length=e.length);var n={type:"audio",id:2,sequenceNumber:0,samples:[],length:0};null!=t&&(n.samples.push(t),n.length=t.length),this._videoStashedLastSample=null,this._audioStashedLastSample=null,this._remuxVideo(i,!0),this._remuxAudio(n,!0)},e.prototype._remuxAudio=function(e,t){if(null!=this._audioMeta){var i,u=e,l=u.samples,h=void 0,d=-1,c=this._audioMeta.refSampleDuration,f="mp3"===this._audioMeta.codec&&this._mp3UseMpegAudio,p=this._dtsBaseInited&&void 0===this._audioNextDts,m=!1;if(l&&0!==l.length&&(1!==l.length||t)){var _=0,g=null,v=0;f?(_=0,v=u.length):(_=8,v=8+u.length);var y=null;if(l.length>1&&(v-=(y=l.pop()).length),null!=this._audioStashedLastSample){var b=this._audioStashedLastSample;this._audioStashedLastSample=null,l.unshift(b),v+=b.length}null!=y&&(this._audioStashedLastSample=y);var S=l[0].dts-this._dtsBase;if(this._audioNextDts)h=S-this._audioNextDts;else if(this._audioSegmentInfoList.isEmpty())h=0,this._fillSilentAfterSeek&&!this._videoSegmentInfoList.isEmpty()&&"mp3"!==this._audioMeta.originalCodec&&(m=!0);else{var T=this._audioSegmentInfoList.getLastSampleBefore(S);if(null!=T){var E=S-(T.originalDts+T.duration);E<=3&&(E=0),h=S-(T.dts+T.duration+E)}else h=0}if(m){var w=S-h,A=this._videoSegmentInfoList.getLastSegmentBefore(S);if(null!=A&&A.beginDts=3*c&&this._fillAudioTimestampGap&&!s.default.safari){R=!0;var M,F=Math.floor(h/c);n.default.w(this.TAG,"Large audio timestamp gap detected, may cause AV sync to drift. Silent frames will be generated to avoid unsync.\noriginalDts: "+x+" ms, curRefDts: "+U+" ms, dtsCorrection: "+Math.round(h)+" ms, generate: "+F+" frames"),C=Math.floor(U),O=Math.floor(U+c)-C,null==(M=a.default.getSilentFrame(this._audioMeta.originalCodec,this._audioMeta.channelCount))&&(n.default.w(this.TAG,"Unable to generate silent frame for "+this._audioMeta.originalCodec+" with "+this._audioMeta.channelCount+" channels, repeat last frame"),M=L),D=[];for(var B=0;B=1?P[P.length-1].duration:Math.floor(c),this._audioNextDts=C+O;-1===d&&(d=C),P.push({dts:C,pts:C,cts:0,unit:b.unit,size:b.unit.byteLength,duration:O,originalDts:x,flags:{isLeading:0,dependsOn:1,isDependedOn:0,hasRedundancy:0}}),R&&P.push.apply(P,D)}}if(0===P.length)return u.samples=[],void(u.length=0);for(f?g=new Uint8Array(v):((g=new Uint8Array(v))[0]=v>>>24&255,g[1]=v>>>16&255,g[2]=v>>>8&255,g[3]=255&v,g.set(r.default.types.mdat,4)),I=0;I1&&(f-=(p=s.pop()).length),null!=this._videoStashedLastSample){var m=this._videoStashedLastSample;this._videoStashedLastSample=null,s.unshift(m),f+=m.length}null!=p&&(this._videoStashedLastSample=p);var _=s[0].dts-this._dtsBase;if(this._videoNextDts)u=_-this._videoNextDts;else if(this._videoSegmentInfoList.isEmpty())u=0;else{var g=this._videoSegmentInfoList.getLastSampleBefore(_);if(null!=g){var v=_-(g.originalDts+g.duration);v<=3&&(v=0),u=_-(g.dts+g.duration+v)}else u=0}for(var y=new o.MediaSegmentInfo,b=[],S=0;S=1?b[b.length-1].duration:Math.floor(this._videoMeta.refSampleDuration),E){var P=new o.SampleInfo(w,C,k,m.dts,!0);P.fileposition=m.fileposition,y.appendSyncPoint(P)}b.push({dts:w,pts:C,cts:A,units:m.units,size:m.length,isKeyframe:E,duration:k,originalDts:T,flags:{isLeading:0,dependsOn:E?2:1,isDependedOn:E?1:0,hasRedundancy:0,isNonSync:E?0:1}})}for((c=new Uint8Array(f))[0]=f>>>24&255,c[1]=f>>>16&255,c[2]=f>>>8&255,c[3]=255&f,c.set(r.default.types.mdat,4),S=0;S=0&&/(rv)(?::| )([\w.]+)/.exec(e)||e.indexOf("compatible")<0&&/(firefox)[ \/]([\w.]+)/.exec(e)||[],i=/(ipad)/.exec(e)||/(ipod)/.exec(e)||/(windows phone)/.exec(e)||/(iphone)/.exec(e)||/(kindle)/.exec(e)||/(android)/.exec(e)||/(windows)/.exec(e)||/(mac)/.exec(e)||/(linux)/.exec(e)||/(cros)/.exec(e)||[],r={browser:t[5]||t[3]||t[1]||"",version:t[2]||t[4]||"0",majorVersion:t[4]||t[2]||"0",platform:i[0]||""},a={};if(r.browser){a[r.browser]=!0;var s=r.majorVersion.split(".");a.version={major:parseInt(r.majorVersion,10),string:r.version},s.length>1&&(a.version.minor=parseInt(s[1],10)),s.length>2&&(a.version.build=parseInt(s[2],10))}for(var o in r.platform&&(a[r.platform]=!0),(a.chrome||a.opr||a.safari)&&(a.webkit=!0),(a.rv||a.iemobile)&&(a.rv&&delete a.rv,r.browser="msie",a.msie=!0),a.edge&&(delete a.edge,r.browser="msedge",a.msedge=!0),a.opr&&(r.browser="opera",a.opera=!0),a.safari&&a.android&&(r.browser="android",a.android=!0),a.name=r.browser,a.platform=r.platform,n)n.hasOwnProperty(o)&&delete n[o];Object.assign(n,a)}(),t.default=n},"./src/utils/exception.js": +/*!********************************!*\ + !*** ./src/utils/exception.js ***! + \********************************/ +function(e,t,i){i.r(t),i.d(t,{RuntimeException:function(){return a},IllegalStateException:function(){return s},InvalidArgumentException:function(){return o},NotImplementedException:function(){return u}});var n,r=(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])})(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)}),a=function(){function e(e){this._message=e}return Object.defineProperty(e.prototype,"name",{get:function(){return"RuntimeException"},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"message",{get:function(){return this._message},enumerable:!1,configurable:!0}),e.prototype.toString=function(){return this.name+": "+this.message},e}(),s=function(e){function t(t){return e.call(this,t)||this}return r(t,e),Object.defineProperty(t.prototype,"name",{get:function(){return"IllegalStateException"},enumerable:!1,configurable:!0}),t}(a),o=function(e){function t(t){return e.call(this,t)||this}return r(t,e),Object.defineProperty(t.prototype,"name",{get:function(){return"InvalidArgumentException"},enumerable:!1,configurable:!0}),t}(a),u=function(e){function t(t){return e.call(this,t)||this}return r(t,e),Object.defineProperty(t.prototype,"name",{get:function(){return"NotImplementedException"},enumerable:!1,configurable:!0}),t}(a)},"./src/utils/logger.js": +/*!*****************************!*\ + !*** ./src/utils/logger.js ***! + \*****************************/ +function(e,t,i){i.r(t);var n=i( +/*! events */ +"./node_modules/events/events.js"),r=i.n(n),a=function(){function e(){}return e.e=function(t,i){t&&!e.FORCE_GLOBAL_TAG||(t=e.GLOBAL_TAG);var n="["+t+"] > "+i;e.ENABLE_CALLBACK&&e.emitter.emit("log","error",n),e.ENABLE_ERROR&&(console.error?console.error(n):console.warn)},e.i=function(t,i){t&&!e.FORCE_GLOBAL_TAG||(t=e.GLOBAL_TAG);var n="["+t+"] > "+i;e.ENABLE_CALLBACK&&e.emitter.emit("log","info",n),e.ENABLE_INFO&&console.info&&console.info(n)},e.w=function(t,i){t&&!e.FORCE_GLOBAL_TAG||(t=e.GLOBAL_TAG);var n="["+t+"] > "+i;e.ENABLE_CALLBACK&&e.emitter.emit("log","warn",n),e.ENABLE_WARN&&console.warn},e.d=function(t,i){t&&!e.FORCE_GLOBAL_TAG||(t=e.GLOBAL_TAG);var n="["+t+"] > "+i;e.ENABLE_CALLBACK&&e.emitter.emit("log","debug",n),e.ENABLE_DEBUG&&console.debug&&console.debug(n)},e.v=function(t,i){t&&!e.FORCE_GLOBAL_TAG||(t=e.GLOBAL_TAG);var n="["+t+"] > "+i;e.ENABLE_CALLBACK&&e.emitter.emit("log","verbose",n),e.ENABLE_VERBOSE},e}();a.GLOBAL_TAG="flv.js",a.FORCE_GLOBAL_TAG=!1,a.ENABLE_ERROR=!0,a.ENABLE_INFO=!0,a.ENABLE_WARN=!0,a.ENABLE_DEBUG=!0,a.ENABLE_VERBOSE=!0,a.ENABLE_CALLBACK=!1,a.emitter=new(r()),t.default=a},"./src/utils/logging-control.js": +/*!**************************************!*\ + !*** ./src/utils/logging-control.js ***! + \**************************************/ +function(e,t,i){i.r(t);var n=i( +/*! events */ +"./node_modules/events/events.js"),r=i.n(n),a=i( +/*! ./logger.js */ +"./src/utils/logger.js"),s=function(){function e(){}return Object.defineProperty(e,"forceGlobalTag",{get:function(){return a.default.FORCE_GLOBAL_TAG},set:function(t){a.default.FORCE_GLOBAL_TAG=t,e._notifyChange()},enumerable:!1,configurable:!0}),Object.defineProperty(e,"globalTag",{get:function(){return a.default.GLOBAL_TAG},set:function(t){a.default.GLOBAL_TAG=t,e._notifyChange()},enumerable:!1,configurable:!0}),Object.defineProperty(e,"enableAll",{get:function(){return a.default.ENABLE_VERBOSE&&a.default.ENABLE_DEBUG&&a.default.ENABLE_INFO&&a.default.ENABLE_WARN&&a.default.ENABLE_ERROR},set:function(t){a.default.ENABLE_VERBOSE=t,a.default.ENABLE_DEBUG=t,a.default.ENABLE_INFO=t,a.default.ENABLE_WARN=t,a.default.ENABLE_ERROR=t,e._notifyChange()},enumerable:!1,configurable:!0}),Object.defineProperty(e,"enableDebug",{get:function(){return a.default.ENABLE_DEBUG},set:function(t){a.default.ENABLE_DEBUG=t,e._notifyChange()},enumerable:!1,configurable:!0}),Object.defineProperty(e,"enableVerbose",{get:function(){return a.default.ENABLE_VERBOSE},set:function(t){a.default.ENABLE_VERBOSE=t,e._notifyChange()},enumerable:!1,configurable:!0}),Object.defineProperty(e,"enableInfo",{get:function(){return a.default.ENABLE_INFO},set:function(t){a.default.ENABLE_INFO=t,e._notifyChange()},enumerable:!1,configurable:!0}),Object.defineProperty(e,"enableWarn",{get:function(){return a.default.ENABLE_WARN},set:function(t){a.default.ENABLE_WARN=t,e._notifyChange()},enumerable:!1,configurable:!0}),Object.defineProperty(e,"enableError",{get:function(){return a.default.ENABLE_ERROR},set:function(t){a.default.ENABLE_ERROR=t,e._notifyChange()},enumerable:!1,configurable:!0}),e.getConfig=function(){return{globalTag:a.default.GLOBAL_TAG,forceGlobalTag:a.default.FORCE_GLOBAL_TAG,enableVerbose:a.default.ENABLE_VERBOSE,enableDebug:a.default.ENABLE_DEBUG,enableInfo:a.default.ENABLE_INFO,enableWarn:a.default.ENABLE_WARN,enableError:a.default.ENABLE_ERROR,enableCallback:a.default.ENABLE_CALLBACK}},e.applyConfig=function(e){a.default.GLOBAL_TAG=e.globalTag,a.default.FORCE_GLOBAL_TAG=e.forceGlobalTag,a.default.ENABLE_VERBOSE=e.enableVerbose,a.default.ENABLE_DEBUG=e.enableDebug,a.default.ENABLE_INFO=e.enableInfo,a.default.ENABLE_WARN=e.enableWarn,a.default.ENABLE_ERROR=e.enableError,a.default.ENABLE_CALLBACK=e.enableCallback},e._notifyChange=function(){var t=e.emitter;if(t.listenerCount("change")>0){var i=e.getConfig();t.emit("change",i)}},e.registerListener=function(t){e.emitter.addListener("change",t)},e.removeListener=function(t){e.emitter.removeListener("change",t)},e.addLogListener=function(t){a.default.emitter.addListener("log",t),a.default.emitter.listenerCount("log")>0&&(a.default.ENABLE_CALLBACK=!0,e._notifyChange())},e.removeLogListener=function(t){a.default.emitter.removeListener("log",t),0===a.default.emitter.listenerCount("log")&&(a.default.ENABLE_CALLBACK=!1,e._notifyChange())},e}();s.emitter=new(r()),t.default=s},"./src/utils/polyfill.js": +/*!*******************************!*\ + !*** ./src/utils/polyfill.js ***! + \*******************************/ +function(e,t,i){i.r(t);var n=function(){function e(){}return e.install=function(){Object.setPrototypeOf=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},Object.assign=Object.assign||function(e){if(null==e)throw new TypeError("Cannot convert undefined or null to object");for(var t=Object(e),i=1;i=128){t.push(String.fromCharCode(65535&s)),r+=2;continue}}else if(i[r]<240){if(n(i,r,2)&&(s=(15&i[r])<<12|(63&i[r+1])<<6|63&i[r+2])>=2048&&55296!=(63488&s)){t.push(String.fromCharCode(65535&s)),r+=3;continue}}else if(i[r]<248){var s;if(n(i,r,3)&&(s=(7&i[r])<<18|(63&i[r+1])<<12|(63&i[r+2])<<6|63&i[r+3])>65536&&s<1114112){s-=65536,t.push(String.fromCharCode(s>>>10|55296)),t.push(String.fromCharCode(1023&s|56320)),r+=4;continue}}t.push(String.fromCharCode(65533)),++r}return t.join("")}}},i={};function r(e){var n=i[e];if(void 0!==n)return n.exports;var a=i[e]={exports:{}};return t[e].call(a.exports,a,a.exports,r),a.exports}return r.m=t,r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var i in t)r.o(t,i)&&!r.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:t[i]})},r.g=function(){if("object"===("undefined"==typeof globalThis?"undefined":n(globalThis)))return globalThis;try{return this||new Function("return this")()}catch(e){if("object"===("undefined"==typeof window?"undefined":n(window)))return window}}(),r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r("./src/index.js")}()},"object"===(void 0===i?"undefined":n(i))&&"object"===(void 0===t?"undefined":n(t))?t.exports=a():"function"==typeof define&&define.amd?define([],a):"object"===(void 0===i?"undefined":n(i))?i.flvjshevc=a():r.flvjshevc=a()}).call(this,e("_process"))},{_process:44}],69:[function(e,t,i){"use strict";function n(e,t){for(var i=0;i0&&i.extensionInfo.vHeight>0&&(i.size.width=i.extensionInfo.vWidth,i.size.height=i.extensionInfo.vHeight)),i.mediaInfo.duration,null!=i.onDemuxed&&i.onDemuxed(i.onReadyOBJ);for(var e=!1;void 0!==i.mpegTsObj&&null!==i.mpegTsObj;){var n=i.mpegTsObj.readPacket();if(n.size<=0)break;var r=n.dtime>0?n.dtime:n.ptime;if(!(r<0)){if(0==n.type){r<=i.vPreFramePTS&&(e=!0);var a=u.PACK_NALU(n.layer),o=1==n.keyframe,l=1==e?r+i.vStartTime:r,h=new s.BufferFrame(l,o,a,!0);i.bufObject.appendFrame(h.pts,h.data,!0,h.isKey),i.vPreFramePTS=l,null!=i.onSamples&&i.onSamples(i.onReadyOBJ,h)}else if(r<=i.aPreFramePTS&&(e=!0),"aac"==i.mediaInfo.aCodec)for(var d=n.data,c=0;c=3?(i._onTsReady(e),window.clearInterval(i.timerTsWasm),i.timerTsWasm=null):(i.mpegTsWasmRetryLoadTimes+=1,i.mpegTsObj.initDemuxer())}),3e3)}},{key:"_onTsReady",value:function(e){var t=this;t.hls.fetchM3u8(e),t.mpegTsWasmState=!0,t.timerFeed=window.setInterval((function(){if(t.tsList.length>0&&0==t.lockWait.state)try{var e=t.tsList.shift();if(null!=e){var i=e.streamURI,n=e.streamDur;t.lockWait.state=!0,t.lockWait.lockMember.dur=n,t.mpegTsObj.isLive=t.hls.isLive(),t.mpegTsObj.demuxURL(i)}else console.error("_onTsReady need wait ")}catch(e){console.error("onTsReady ERROR:",e),t.lockWait.state=!1}}),50)}},{key:"release",value:function(){this.hls&&this.hls.release(),this.hls=null,this.timerFeed&&window.clearInterval(this.timerFeed),this.timerFeed=null,this.timerTsWasm&&window.clearInterval(this.timerTsWasm),this.timerTsWasm=null}},{key:"bindReady",value:function(e){this.onReadyOBJ=e}},{key:"popBuffer",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:-1;return t<0?null:1===e?t+1>this.bufObject.videoBuffer.length?null:this.bufObject.vFrame(t):2===e?t+1>this.bufObject.audioBuffer.length?null:this.bufObject.aFrame(t):void 0}},{key:"getVLen",value:function(){return this.bufObject.videoBuffer.length}},{key:"getALen",value:function(){return this.bufObject.audioBuffer.length}},{key:"getLastIdx",value:function(){return this.bufObject.videoBuffer.length-1}},{key:"getALastIdx",value:function(){return this.bufObject.audioBuffer.length-1}},{key:"getACodec",value:function(){return this.aCodec}},{key:"getVCodec",value:function(){return this.vCodec}},{key:"getDurationMs",value:function(){return this.durationMs}},{key:"getFPS",value:function(){return this.fps}},{key:"getSampleRate",value:function(){return this.sampleRate}},{key:"getSampleChannel",value:function(){return this.aChannel}},{key:"getSize",value:function(){return this.size}},{key:"seek",value:function(e){if(e>=0){var t=this.bufObject.seekIDR(e);this.seekPos=t}}}])&&n(t.prototype,i),h&&n(t,h),e}();i.M3u8=h},{"../consts":52,"../decoder/hevc-imp":64,"./buffer":66,"./bufferFrame":67,"./m3u8base":70,"./mpegts/mpeg.js":74}],70:[function(e,t,i){"use strict";function n(e,t){for(var i=0;i ",t),setTimeout((function(){i.fetchM3u8(e)}),500)}))}},{key:"_uriParse",value:function(e){this._preURI="";var t=e.split("://"),i=null,n=null;if(t.length<1)return!1;t.length>1?(i=t[0],n=t[1].split("/"),this._preURI=i+"://"):n=t[0].split("/");for(var r=0;rp&&(o=p);var m=n[l+=1],_=null;if(m.indexOf("http")>=0)_=m;else{if("/"===m[0]){var g=this._preURI.split("//"),v=g[g.length-1].split("/");this._preURI=g[0]+"//"+v[0]}_=this._preURI+m}this._slices.indexOf(_)<0&&(this._slices.push(_),this._slices[this._slices.length-1],null!=this.onTransportStream&&this.onTransportStream(_,p))}}}if(this._slices.length>s.hlsSliceLimit&&this._type==r.PLAYER_IN_TYPE_M3U8_LIVE&&(this._slices=this._slices.slice(-1*s.hlsSliceLimit)),null!=this.onFinished){var y={type:this._type,duration:-1};this.onFinished(y)}return o}},{key:"_readTag",value:function(e){var t=s.tagParse.exec(e);return null!==t?{key:t[1],value:t[3]}:null}}])&&n(t.prototype,i),o&&n(t,o),e}();i.M3u8Base=o},{"../consts":52}],71:[function(e,t,i){"use strict";var n=e("mp4box"),r=e("../decoder/hevc-header"),a=e("../decoder/hevc-imp"),s=e("./buffer"),o=e("../consts"),u={96e3:0,88200:1,64e3:2,48e3:3,44100:4,32e3:5,24e3:6,22050:7,16e3:8,12e3:9,11025:10,8e3:11,7350:12,Reserved:13,"frequency is written explictly":15},l=function(e){for(var t=[],i=0;i1&&void 0!==arguments[1]&&arguments[1],i=null;return t?((i=e)[0]=r.DEFINE_STARTCODE[0],i[1]=r.DEFINE_STARTCODE[1],i[2]=r.DEFINE_STARTCODE[2],i[3]=r.DEFINE_STARTCODE[3]):((i=new Uint8Array(r.DEFINE_STARTCODE.length+e.length)).set(r.DEFINE_STARTCODE,0),i.set(e,r.DEFINE_STARTCODE.length)),i},h.prototype.setAACAdts=function(e){var t=null,i=this.aacProfile,n=u[this.sampleRate],r=new Uint8Array(7),a=r.length+e.length;return r[0]=255,r[1]=241,r[2]=(i-1<<6)+(n<<2)+0,r[3]=128+(a>>11),r[4]=(2047&a)>>3,r[5]=31+((7&a)<<5),r[6]=252,(t=new Uint8Array(a)).set(r,0),t.set(e,r.length),t},h.prototype.demux=function(){var e=this;e.seekPos=-1,e.mp4boxfile=n.createFile(),e.movieInfo=null,e.videoCodec=null,e.durationMs=-1,e.fps=-1,e.sampleRate=-1,e.aacProfile=2,e.size={width:-1,height:-1},e.bufObject=s(),e.audioNone=!1,e.naluHeader={vps:null,sps:null,pps:null,sei:null},e.mp4boxfile.onError=function(e){},this.mp4boxfile.onReady=function(t){for(var i in e.movieInfo=t,t.tracks)"VideoHandler"!==t.tracks[i].name&&"video"!==t.tracks[i].type||(t.tracks[i].codec,t.tracks[i].codec.indexOf("hev")>=0||t.tracks[i].codec.indexOf("hvc")>=0?e.videoCodec=o.CODEC_H265:t.tracks[i].codec.indexOf("avc")>=0&&(e.videoCodec=o.CODEC_H264));var n=-1;if(n=t.videoTracks[0].samples_duration/t.videoTracks[0].timescale,e.durationMs=1e3*n,e.fps=t.videoTracks[0].nb_samples/n,e.seekDiffTime=1/e.fps,e.size.width=t.videoTracks[0].track_width,e.size.height=t.videoTracks[0].track_height,t.audioTracks.length>0){e.sampleRate=t.audioTracks[0].audio.sample_rate;var r=t.audioTracks[0].codec.split(".");e.aacProfile=r[r.length-1]}else e.audioNone=!0;null!=e.onMp4BoxReady&&e.onMp4BoxReady(e.videoCodec),e.videoCodec===o.CODEC_H265?(e.initializeAllSourceBuffers(),e.mp4boxfile.start()):(e.videoCodec,o.CODEC_H264)},e.mp4boxfile.onSamples=function(t,i,n){var s=window.setInterval((function(){for(var i=0;i3?e.naluHeader.sei=e.setStartCode(_[3][0].data,!1):e.naluHeader.sei=new Uint8Array,e.naluHeader}else e.videoCodec==o.CODEC_H264&&(e.naluHeader.vps=new Uint8Array,e.naluHeader.sps=e.setStartCode(f.SPS[0].nalu,!1),e.naluHeader.pps=e.setStartCode(f.PPS[0].nalu,!1),e.naluHeader.sei=new Uint8Array);h[4].toString(16),e.naluHeader.vps[4].toString(16),l(e.naluHeader.vps),l(h);var g=e.setStartCode(h.subarray(0,e.naluHeader.vps.length),!0);if(l(g),h[4]===e.naluHeader.vps[4]){var v=e.naluHeader.vps.length+4,y=e.naluHeader.vps.length+e.naluHeader.sps.length+4,b=e.naluHeader.vps.length+e.naluHeader.sps.length+e.naluHeader.pps.length+4;if(e.naluHeader.sei.length<=0&&e.naluHeader.sps.length>0&&h[v]===e.naluHeader.sps[4]&&e.naluHeader.pps.length>0&&h[y]===e.naluHeader.pps[4]&&78===h[b]){h[e.naluHeader.vps.length+4],e.naluHeader.sps[4],h[e.naluHeader.vps.length+e.naluHeader.sps.length+4],e.naluHeader.pps[4],h[e.naluHeader.vps.length+e.naluHeader.sps.length+e.naluHeader.pps.length+4];for(var S=0,T=0;T4&&h[4]===e.naluHeader.sei[4]){var E=h.subarray(0,10),w=new Uint8Array(e.naluHeader.vps.length+E.length);w.set(E,0),w.set(e.naluHeader.vps,E.length),w[3]=1,e.naluHeader.vps=null,e.naluHeader.vps=new Uint8Array(w),w=null,E=null,(h=h.subarray(10))[4],e.naluHeader.vps[4],e.naluHeader.vps}else if(0===e.naluHeader.sei.length&&78===h[4]){h=e.setStartCode(h,!0);for(var A=0,C=0;C1&&void 0!==arguments[1]?arguments[1]:0;return e.fileStart=t,this.mp4boxfile.appendBuffer(e)},h.prototype.finishBuffer=function(){this.mp4boxfile.flush()},h.prototype.play=function(){},h.prototype.getVideoCoder=function(){return this.videoCodec},h.prototype.getDurationMs=function(){return this.durationMs},h.prototype.getFPS=function(){return this.fps},h.prototype.getSampleRate=function(){return this.sampleRate},h.prototype.getSize=function(){return this.size},h.prototype.seek=function(e){if(e>=0){var t=this.bufObject.seekIDR(e);this.seekPos=t}},h.prototype.popBuffer=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:-1;return t<0?null:1==e?this.bufObject.vFrame(t):2==e?this.bufObject.aFrame(t):void 0},h.prototype.addBuffer=function(e){var t=e.id;this.mp4boxfile.setExtractionOptions(t)},h.prototype.initializeAllSourceBuffers=function(){if(this.movieInfo){for(var e=this.movieInfo,t=0;t>5)}},{key:"sliceAACFrames",value:function(e,t){for(var i=[],n=e,r=0;r>4==15){var a=this._getPktLen(t[r+3],t[r+4],t[r+5]);if(a<=0)continue;var s=t.subarray(r,r+a),o=new Uint8Array(a);o.set(s,0),i.push({ptime:n,data:o}),n+=this.frameDurSec,r+=a}else r+=1;return i}}])&&n(t.prototype,i),r&&n(t,r),e}();i.AACDecoder=r},{}],74:[function(e,t,i){(function(t){"use strict";function n(e,t){for(var i=0;i ",e),n=null})).catch((function(i){console.error("demuxerTsInit ERROR fetch ERROR ==> ",i),t._releaseOffset(),t.onDemuxedFailed&&t.onDemuxedFailed(i,e)}))}},{key:"_releaseOffset",value:function(){void 0!==this.offsetDemux&&null!==this.offsetDemux&&(Module._free(this.offsetDemux),this.offsetDemux=null)}},{key:"_demuxCore",value:function(e){if(this._releaseOffset(),this._refreshDemuxer(),!(e.length<=0)){this.offsetDemux=Module._malloc(e.length),Module.HEAP8.set(e,this.offsetDemux);var t=Module.cwrap("demuxBox","number",["number","number","number"])(this.offsetDemux,e.length,this.isLive);Module._free(this.offsetDemux),this.offsetDemux=null,t>=0&&(this._setMediaInfo(),this._setExtensionInfo(),null!=this.onDemuxed&&this.onDemuxed())}}},{key:"_setMediaInfo",value:function(){var e=Module.cwrap("getMediaInfo","number",[])(),t=Module.HEAPU32[e/4],i=Module.HEAPU32[e/4+1],n=Module.HEAPF64[e/8+1],s=Module.HEAPF64[e/8+1+1],o=Module.HEAPF64[e/8+1+1+1],u=Module.HEAPF64[e/8+1+1+1+1],l=Module.HEAPU32[e/4+2+2+2+2+2];this.mediaAttr.vFps=n,this.mediaAttr.vGop=l,this.mediaAttr.vDuration=s,this.mediaAttr.aDuration=o,this.mediaAttr.duration=u;var h=Module.cwrap("getAudioCodecID","number",[])();h>=0?(this.mediaAttr.aCodec=a.CODEC_OFFSET_TABLE[h],this.mediaAttr.sampleRate=t>0?t:a.DEFAULT_SAMPLERATE,this.mediaAttr.sampleChannel=i>=0?i:a.DEFAULT_CHANNEL):(this.mediaAttr.sampleRate=0,this.mediaAttr.sampleChannel=0,this.mediaAttr.audioNone=!0);var d=Module.cwrap("getVideoCodecID","number",[])();d>=0&&(this.mediaAttr.vCodec=a.CODEC_OFFSET_TABLE[d]),null==this.aacDec?this.aacDec=new r.AACDecoder(this.mediaAttr):this.aacDec.updateConfig(this.mediaAttr)}},{key:"_setExtensionInfo",value:function(){var e=Module.cwrap("getExtensionInfo","number",[])(),t=Module.HEAPU32[e/4],i=Module.HEAPU32[e/4+1];this.extensionInfo.vWidth=t,this.extensionInfo.vHeight=i}},{key:"readMediaInfo",value:function(){return this.mediaAttr}},{key:"readExtensionInfo",value:function(){return this.extensionInfo}},{key:"readAudioNone",value:function(){return this.mediaAttr.audioNone}},{key:"_readLayer",value:function(){null===this.naluLayer?this.naluLayer={vps:null,sps:null,pps:null,sei:null}:(this.naluLayer.vps=null,this.naluLayer.sps=null,this.naluLayer.pps=null,this.naluLayer.sei=null),null===this.vlcLayer?this.vlcLayer={vlc:null}:this.vlcLayer.vlc=null;var e=Module.cwrap("getSPSLen","number",[])(),t=Module.cwrap("getSPS","number",[])();if(!(e<0)){var i=Module.HEAPU8.subarray(t,t+e);this.naluLayer.sps=new Uint8Array(e),this.naluLayer.sps.set(i,0);var n=Module.cwrap("getPPSLen","number",[])(),r=Module.cwrap("getPPS","number",[])(),s=Module.HEAPU8.subarray(r,r+n);this.naluLayer.pps=new Uint8Array(n),this.naluLayer.pps.set(s,0);var o=Module.cwrap("getSEILen","number",[])(),u=Module.cwrap("getSEI","number",[])(),l=Module.HEAPU8.subarray(u,u+o);this.naluLayer.sei=new Uint8Array(o),this.naluLayer.sei.set(l,0);var h=Module.cwrap("getVLCLen","number",[])(),d=Module.cwrap("getVLC","number",[])(),c=Module.HEAPU8.subarray(d,d+h);if(this.vlcLayer.vlc=new Uint8Array(h),this.vlcLayer.vlc.set(c,0),this.mediaAttr.vCodec==a.DEF_HEVC||this.mediaAttr.vCodec==a.DEF_H265){var f=Module.cwrap("getVPSLen","number",[])(),p=Module.cwrap("getVPS","number",[])(),m=Module.HEAPU8.subarray(p,p+f);this.naluLayer.vps=new Uint8Array(f),this.naluLayer.vps.set(m,0),Module._free(m),m=null}else this.mediaAttr.vCodec==a.DEF_AVC||(this.mediaAttr.vCodec,a.DEF_H264);return Module._free(i),i=null,Module._free(s),s=null,Module._free(l),l=null,Module._free(c),c=null,{nalu:this.naluLayer,vlc:this.vlcLayer}}}},{key:"isHEVC",value:function(){return this.mediaAttr.vCodec==a.DEF_HEVC||this.mediaAttr.vCodec==a.DEF_H265}},{key:"readPacket",value:function(){var e=Module.cwrap("getPacket","number",[])(),t=Module.HEAPU32[e/4],i=Module.HEAPU32[e/4+1],n=Module.HEAPF64[e/8+1],r=Module.HEAPF64[e/8+1+1],s=Module.HEAPU32[e/4+1+1+2+2],o=Module.HEAPU32[e/4+1+1+2+2+1],u=Module.HEAPU8.subarray(o,o+i),l=this._readLayer(),h={type:t,size:i,ptime:n,dtime:r,keyframe:s,src:u,data:1==t&&this.mediaAttr.aCodec==a.DEF_AAC?this.aacDec.sliceAACFrames(n,u):u,layer:l};return Module._free(u),u=null,h}},{key:"_refreshDemuxer",value:function(){this.releaseTsDemuxer(),this._initDemuxer()}},{key:"_initDemuxer",value:function(){Module.cwrap("initTsMissile","number",[])(),Module.cwrap("initializeDemuxer","number",[])()}},{key:"releaseTsDemuxer",value:function(){Module.cwrap("exitTsMissile","number",[])()}}])&&n(i.prototype,s),o&&n(i,o),e}();i.MPEG_JS=s}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./consts":72,"./decoder/aac":73}],75:[function(e,t,i){"use strict";function n(e,t){for(var i=0;i0&&e.extensionInfo.vHeight>0&&(e.size.width=e.extensionInfo.vWidth,e.size.height=e.extensionInfo.vHeight);for(var t=null;!((t=e.mpegTsObj.readPacket()).size<=0);){var i=t.dtime;if(0==t.type){var n=s.PACK_NALU(t.layer),r=1==t.keyframe;e.bufObject.appendFrame(i,n,!0,r)}else if("aac"==e.mediaInfo.aCodec)for(var a=t.data,o=0;o0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:-1;return t<0?null:1==e?this.bufObject.vFrame(t):2==e?this.bufObject.aFrame(t):void 0}},{key:"isHEVC",value:function(){return this.mpegTsObj.isHEVC()}},{key:"getACodec",value:function(){return this.aCodec}},{key:"getVCodec",value:function(){return this.vCodec}},{key:"getAudioNone",value:function(){return this.mpegTsObj.mediaAttr.audioNone}},{key:"getDurationMs",value:function(){return this.durationMs}},{key:"getFPS",value:function(){return this.fps}},{key:"getSampleRate",value:function(){return this.sampleRate}},{key:"getSize",value:function(){return this.size}},{key:"seek",value:function(e){if(e>=0){var t=this.bufObject.seekIDR(e);this.seekPos=t}}}])&&n(t.prototype,i),o&&n(t,o),e}();i.MpegTs=o},{"../decoder/hevc-imp":64,"./buffer":66,"./mpegts/mpeg.js":74}],76:[function(e,t,i){(function(t){"use strict";function n(e){return(n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function r(e,t){for(var i=0;i0&&(i=!0),this.configFormat.type===v.PLAYER_IN_TYPE_RAW_265&&(i=!0,this.playMode=v.PLAYER_MODE_NOTIME_LIVE),this.playParam={durationMs:0,fps:0,sampleRate:0,size:{width:0,height:0},audioNone:i,videoCodec:v.CODEC_H265},y.UI.createPlayerRender(this.configFormat.playerId,this.configFormat.playerW,this.configFormat.playerH),!1===this._isSupportWASM())return this._makeMP4Player(!1),0;if(!1===this.configFormat.extInfo.hevc)return Module.cwrap("AVPlayerInit","number",["string","string"])(this.configFormat.token,"0.0.0"),this._makeMP4Player(!0),0;var n=window.setInterval((function(){t.STATICE_MEM_playerIndexPtr===e.playerIndex&&(t.STATICE_MEM_playerIndexPtr,e.playerIndex,window.WebAssembly?(t.STATIC_MEM_wasmDecoderState,1==t.STATIC_MEM_wasmDecoderState&&(e._makeMP4Player(),t.STATICE_MEM_playerIndexPtr+=1,window.clearInterval(n),n=null)):(/iPhone|iPad/.test(window.navigator.userAgent),t.STATICE_MEM_playerIndexPtr+=1,window.clearInterval(n),n=null))}),500)}},{key:"release",value:function(){return void 0!==this.player&&null!==this.player&&(this.player,this.playParam.videoCodec===v.CODEC_H265&&this.player?(this.configFormat.type==v.PLAYER_IN_TYPE_M3U8&&void 0!==this.hlsObj&&null!==this.hlsObj&&this.hlsObj.release(),this.player.release()):this.player.release(),void 0!==this.snapshotCanvasContext&&null!==this.snapshotCanvasContext&&(b.releaseContext(this.snapshotCanvasContext),this.snapshotCanvasContext=null,void 0!==this.snapshotYuvLastFrame&&null!==this.snapshotYuvLastFrame&&(this.snapshotYuvLastFrame.luma=null,this.snapshotYuvLastFrame.chromaB=null,this.snapshotYuvLastFrame.chromaR=null,this.snapshotYuvLastFrame.width=0,this.snapshotYuvLastFrame.height=0)),void 0!==this.workerFetch&&null!==this.workerFetch&&(this.workerFetch.postMessage({cmd:"stop",params:"",type:this.mediaExtProtocol}),this.workerFetch.onmessage=null),void 0!==this.workerParse&&null!==this.workerParse&&(this.workerParse.postMessage({cmd:"stop",params:""}),this.workerParse.onmessage=null),this.workerFetch=null,this.workerParse=null,this.configFormat.extInfo.readyShow=!0,window.onclick=document.body.onclick=null,window.g_players={},!0)}},{key:"debugYUV",value:function(e){this.player.debugYUV(e)}},{key:"setPlaybackRate",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;return!(this.playParam.videoCodec===v.CODEC_H265||e<=0||void 0===this.player||null===this.player)&&this.player.setPlaybackRate(e)}},{key:"getPlaybackRate",value:function(){return void 0!==this.player&&null!==this.player&&(this.playParam.videoCodec===v.CODEC_H265?1:this.player.getPlaybackRate())}},{key:"setRenderScreen",value:function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return void 0!==this.player&&null!==this.player&&(this.player.setScreen(e),!0)}},{key:"play",value:function(){if(void 0===this.player||null===this.player)return!1;if(this.playParam.videoCodec===v.CODEC_H265){var e={seekPos:this._getSeekTarget(),mode:this.playMode,accurateSeek:this.configFormat.accurateSeek,seekEvent:!1,realPlay:!0};this.player.play(e)}else this.player.play();return!0}},{key:"pause",value:function(){return void 0!==this.player&&null!==this.player&&(this.player.pause(),!0)}},{key:"isPlaying",value:function(){return void 0!==this.player&&null!==this.player&&this.player.isPlayingState()}},{key:"setVoice",value:function(e){return!(e<0||void 0===this.player||null===this.player||(this.volume=e,this.player&&this.player.setVoice(e),0))}},{key:"getVolume",value:function(){return this.volume}},{key:"mediaInfo",value:function(){var e={meta:this.playParam,videoType:this.playMode};return e.meta.isHEVC=0===this.playParam.videoCodec,e}},{key:"snapshot",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;return null===e||void 0!==this.playParam&&null!==this.playParam&&(0===this.playParam.videoCodec?(this.player.setScreen(!0),e.width=this.snapshotYuvLastFrame.width,e.height=this.snapshotYuvLastFrame.height,this.snapshotYuvLastFrame,void 0!==this.snapshotCanvasContext&&null!==this.snapshotCanvasContext||(this.snapshotCanvasContext=b.setupCanvas(e,{preserveDrawingBuffer:!1})),b.renderFrame(this.snapshotCanvasContext,this.snapshotYuvLastFrame.luma,this.snapshotYuvLastFrame.chromaB,this.snapshotYuvLastFrame.chromaR,this.snapshotYuvLastFrame.width,this.snapshotYuvLastFrame.height)):(e.width=this.playParam.size.width,e.height=this.playParam.size.height,e.getContext("2d").drawImage(this.player.videoTag,0,0,e.width,e.height))),null}},{key:"_seekHLS",value:function(e,t,i){if(void 0===this.player||null===this.player)return!1;setTimeout((function(){t.player.getCachePTS(),t.player.getCachePTS()>e?i():t._seekHLS(e,t,i)}),100)}},{key:"seek",value:function(e){if(void 0===this.player||null===this.player)return!1;var t=this;this.seekTarget=e,this.onSeekStart&&this.onSeekStart(e),this.timerFeed&&(window.clearInterval(this.timerFeed),this.timerFeed=null);var i=this._getSeekTarget();return this.playParam.videoCodec===v.CODEC_H264?(this.player.seek(e),this.onSeekFinish&&this.onSeekFinish()):this.configFormat.extInfo.core===v.PLAYER_CORE_TYPE_CNATIVE?(this.pause(),this._seekHLS(e,this,(function(){t.player.seek((function(){}),{seekTime:i,mode:t.playMode,accurateSeek:t.configFormat.accurateSeek})}))):this._seekHLS(e,this,(function(){t.player.seek((function(){t.configFormat.type==v.PLAYER_IN_TYPE_MP4?t.mp4Obj.seek(e):t.configFormat.type==v.PLAYER_IN_TYPE_TS||t.configFormat.type==v.PLAYER_IN_TYPE_MPEGTS?t.mpegTsObj.seek(e):t.configFormat.type==v.PLAYER_IN_TYPE_M3U8&&(t.hlsObj.onSamples=null,t.hlsObj.seek(e));var i,n=(i=0,i=t.configFormat.accurateSeek?e:t._getBoxBufSeekIDR(),parseInt(i)),r=parseInt(t._getBoxBufSeekIDR())||0;t._avFeedMP4Data(r,n)}),{seekTime:i,mode:t.playMode,accurateSeek:t.configFormat.accurateSeek})})),!0}},{key:"fullScreen",value:function(){if(this.autoScreenClose=!0,this.player.vCodecID,this.player,this.player.vCodecID===v.V_CODEC_NAME_HEVC){var e=document.querySelector("#"+this.configFormat.playerId),t=e.getElementsByTagName("canvas")[0];e.style.width=this.screenW+"px",e.style.height=this.screenH+"px";var i=this._checkScreenDisplaySize(this.screenW,this.screenH,this.playParam.size.width,this.playParam.size.height);t.style.marginTop=i[0]+"px",t.style.marginLeft=i[1]+"px",t.style.width=i[2]+"px",t.style.height=i[3]+"px",this._requestFullScreen(e)}else this._requestFullScreen(this.player.videoTag)}},{key:"closeFullScreen",value:function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0];if(!1===e&&(this.autoScreenClose=!1,this._exitFull()),this.player.vCodecID===v.V_CODEC_NAME_HEVC){var t=document.querySelector("#"+this.configFormat.playerId),i=t.getElementsByTagName("canvas")[0];t.style.width=this.configFormat.playerW+"px",t.style.height=this.configFormat.playerH+"px";var n=this._checkScreenDisplaySize(this.configFormat.playerW,this.configFormat.playerH,this.playParam.size.width,this.playParam.size.height);i.style.marginTop=n[0]+"px",i.style.marginLeft=n[1]+"px",i.style.width=n[2]+"px",i.style.height=n[3]+"px"}}},{key:"playNextFrame",value:function(){return this.pause(),void 0!==this.playParam&&null!==this.playParam&&(0===this.playParam.videoCodec?this.player.playYUV():this.player.nativeNextFrame(),!0)}},{key:"resize",value:function(e,t){if(void 0!==this.player&&null!==this.player){if(!(e&&t&&this.playParam.size.width&&this.playParam.size.height))return!1;var i=this.playParam.size.width,n=this.playParam.size.height,r=0===this.playParam.videoCodec,a=document.querySelector("#"+this.configFormat.playerId);if(a.style.width=e+"px",a.style.height=t+"px",!0===r){var s=a.getElementsByTagName("canvas")[0],o=function(e,t){var r=i/e>n/t,a=(e/i).toFixed(2),s=(t/n).toFixed(2),o=r?a:s,u=parseInt(i*o,10),l=parseInt(n*o,10);return[parseInt((t-l)/2,10),parseInt((e-u)/2,10),u,l]}(e,t);s.style.marginTop=o[0]+"px",s.style.marginLeft=o[1]+"px",s.style.width=o[2]+"px",s.style.height=o[3]+"px"}else{var u=a.getElementsByTagName("video")[0];u.style.width=e+"px",u.style.height=t+"px"}return!0}return!1}},{key:"_checkScreenDisplaySize",value:function(e,t,i,n){var r=i/e>n/t,a=(e/i).toFixed(2),s=(t/n).toFixed(2),o=r?a:s,u=this.fixed?e:parseInt(i*o),l=this.fixed?t:parseInt(n*o);return[parseInt((t-l)/2),parseInt((e-u)/2),u,l]}},{key:"_isFullScreen",value:function(){var e=document.fullscreenElement||document.mozFullscreenElement||document.webkitFullscreenElement;return document.fullscreenEnabled||document.mozFullscreenEnabled||document.webkitFullscreenEnabled,null!=e}},{key:"_requestFullScreen",value:function(e){e.requestFullscreen?e.requestFullscreen():e.mozRequestFullScreen?e.mozRequestFullScreen():e.msRequestFullscreen?e.msRequestFullscreen():e.webkitRequestFullscreen&&e.webkitRequestFullScreen()}},{key:"_exitFull",value:function(){document.exitFullscreen?document.exitFullscreen():document.webkitExitFullscreen?document.webkitExitFullscreen():document.mozCancelFullScreen?document.mozCancelFullScreen():document.msExitFullscreen&&document.msExitFullscreen()}},{key:"_durationText",value:function(e){if(e<0)return"Play";var t=Math.round(e);return Math.floor(t/3600)+":"+Math.floor(t%3600/60)+":"+Math.floor(t%60)}},{key:"_getSeekTarget",value:function(){return this.configFormat.accurateSeek?this.seekTarget:this._getBoxBufSeekIDR()}},{key:"_getBoxBufSeekIDR",value:function(){return this.configFormat.type==v.PLAYER_IN_TYPE_MP4?this.mp4Obj.seekPos:this.configFormat.type==v.PLAYER_IN_TYPE_TS||this.configFormat.type==v.PLAYER_IN_TYPE_MPEGTS?this.mpegTsObj.seekPos:this.configFormat.type==v.PLAYER_IN_TYPE_M3U8?this.hlsObj.seekPos:void 0}},{key:"_playControl",value:function(){this.isPlaying()?this.pause():this.play()}},{key:"_avFeedMP4Data",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;if(void 0===this.player||null===this.player)return!1;var r=parseInt(this.playParam.durationMs/1e3);this.player.clearAllCache(),this.timerFeed=window.setInterval((function(){var a=null,s=null,o=!0,u=!0;if(e.configFormat.type==v.PLAYER_IN_TYPE_MP4?(a=e.mp4Obj.popBuffer(1,t),s=e.mp4Obj.audioNone?null:e.mp4Obj.popBuffer(2,i)):e.configFormat.type==v.PLAYER_IN_TYPE_TS||e.configFormat.type==v.PLAYER_IN_TYPE_MPEGTS?(a=e.mpegTsObj.popBuffer(1,t),s=e.mpegTsObj.getAudioNone()?null:e.mpegTsObj.popBuffer(2,i)):e.configFormat.type==v.PLAYER_IN_TYPE_M3U8&&(a=e.hlsObj.popBuffer(1,t),s=e.hlsObj.audioNone?null:e.hlsObj.popBuffer(2,i),t=e.hlsObj.getLastIdx()&&(o=!1),i=e.hlsObj.getALastIdx()&&(u=!1)),!0===o&&null!=a)for(var l=0;lr)return window.clearInterval(e.timerFeed),e.timerFeed=null,e.player.vCachePTS,e.player.aCachePTS,void(null!=n&&n())}),5)}},{key:"_isSupportWASM",value:function(){window.document;var e=window.navigator,t=e.userAgent.toLowerCase(),i="ipad"==t.match(/ipad/i),r="iphone os"==t.match(/iphone os/i),a="iPad"==t.match(/iPad/i),s="iPhone os"==t.match(/iPhone os/i),o="midp"==t.match(/midp/i),u="rv:1.2.3.4"==t.match(/rv:1.2.3.4/i),l="ucweb"==t.match(/ucweb/i),h="android"==t.match(/android/i),d="Android"==t.match(/Android/i),c="windows ce"==t.match(/windows ce/i),f="windows mobile"==t.match(/windows mobile/i);if(i||r||a||s||o||u||l||h||d||c||f)return!1;var m=function(){try{if("object"===("undefined"==typeof WebAssembly?"undefined":n(WebAssembly))&&"function"==typeof WebAssembly.instantiate){var e=new WebAssembly.Module(Uint8Array.of(0,97,115,109,1,0,0,0));if(e instanceof WebAssembly.Module)return new WebAssembly.Instance(e)instanceof WebAssembly.Instance}}catch(e){}return!1}();if(!1===m)return!1;if(!0===m){var _=p.BrowserJudge(),g=_[0],v=_[1];if("Chrome"===g&&v<85)return!1;if(g.indexOf("360")>=0)return!1;if(/Safari/.test(e.userAgent)&&!/Chrome/.test(e.userAgent)&&v>13)return!1}return!0}},{key:"_makeMP4Player",value:function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0],t=this;if(this._isSupportWASM(),!1===this._isSupportWASM()||!0===e){if(this.configFormat.type==v.PLAYER_IN_TYPE_MP4)t.mediaExtFormat===v.PLAYER_IN_TYPE_FLV?this._flvJsPlayer(this.playParam.durationMs,t.playParam.audioNone):this._makeNativePlayer();else if(this.configFormat.type==v.PLAYER_IN_TYPE_TS||this.configFormat.type==v.PLAYER_IN_TYPE_MPEGTS)this._mpegTsNv3rdPlayer(-1,!1);else if(this.configFormat.type==v.PLAYER_IN_TYPE_M3U8)this._videoJsPlayer();else if(this.configFormat.type===v.PLAYER_IN_TYPE_RAW_265)return-1;return 1}return this.mediaExtProtocol===v.URI_PROTOCOL_WEBSOCKET_DESC?(this.configFormat.type,this.configFormat.type===v.PLAYER_IN_TYPE_RAW_265?this._raw265Entry():this._cWsFLVDecoderEntry(),0):(null!=this.configFormat.extInfo.core&&null!==this.configFormat.extInfo.core&&this.configFormat.extInfo.core===v.PLAYER_CORE_TYPE_CNATIVE?this._cDemuxDecoderEntry():this.configFormat.type==v.PLAYER_IN_TYPE_MP4?this.configFormat.extInfo.moovStartFlag?this._mp4EntryVodStream():this._mp4Entry():this.configFormat.type==v.PLAYER_IN_TYPE_TS||this.configFormat.type==v.PLAYER_IN_TYPE_MPEGTS?this._mpegTsEntry():this.configFormat.type==v.PLAYER_IN_TYPE_M3U8?this._m3u8Entry():this.configFormat.type===v.PLAYER_IN_TYPE_RAW_265&&this._raw265Entry(),0)}},{key:"_makeMP4PlayerViewEvent",value:function(e,t,i,n){var r=this,s=arguments.length>4&&void 0!==arguments[4]&&arguments[4],o=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null,u=this;if(this.playParam.durationMs=e,this.playParam.fps=t,this.playParam.sampleRate=i,this.playParam.size=n,this.playParam.audioNone=s,this.playParam.videoCodec=o||v.CODEC_H265,this.playParam,(this.configFormat.type==v.PLAYER_IN_TYPE_M3U8&&this.hlsConf.hlsType==v.PLAYER_IN_TYPE_M3U8_LIVE||this.configFormat.type==v.PLAYER_IN_TYPE_RAW_265)&&(this.playMode=v.PLAYER_MODE_NOTIME_LIVE),u.configFormat.extInfo.autoCrop){var l=document.querySelector("#"+this.configFormat.playerId),h=n.width/n.height,d=this.configFormat.playerW/this.configFormat.playerH;h>d?l.style.height=this.configFormat.playerW/h+"px":h0&&void 0!==arguments[0]?arguments[0]:0,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,n=arguments.length>3?arguments[3]:void 0,r=arguments.length>4&&void 0!==arguments[4]?arguments[4]:0,a=arguments.length>5?arguments[5]:void 0,o=this;this.playParam.durationMs=e,this.playParam.fps=t,this.playParam.sampleRate=i,this.playParam.size=n,this.playParam.audioNone=r,this.playParam.videoCodec=a||v.CODEC_H264,this.configFormat.type==v.PLAYER_IN_TYPE_M3U8&&this.hlsConf.hlsType==v.PLAYER_IN_TYPE_M3U8_LIVE&&(this.playMode=v.PLAYER_MODE_NOTIME_LIVE),this.player=new s.Mp4Player({width:this.configFormat.playerW,height:this.configFormat.playerH,sampleRate:i,fps:t,appendHevcType:v.APPEND_TYPE_FRAME,fixed:!1,playerId:this.configFormat.playerId,audioNone:r,token:this.configFormat.token,videoCodec:a,autoPlay:this.configFormat.extInfo.autoPlay});var u=0,l=window.setInterval((function(){u++,void 0!==o.player&&null!==o.player||(window.clearInterval(l),l=null),u>v.DEFAULT_PLAYERE_LOAD_TIMEOUT&&(o.player.release(),o.player=null,o._cDemuxDecoderEntry(0,!0),window.clearInterval(l),l=null)}),1e3);this.player.makeIt(this.videoURL),this.player.onPlayingTime=function(t){o._durationText(t),o._durationText(e/1e3),null!=o.onPlayTime&&o.onPlayTime(t)},this.player.onPlayingFinish=function(){null!=o.onPlayFinish&&o.onPlayFinish()},this.player.onLoadFinish=function(){window.clearInterval(l),l=null,o.playParam.durationMs=1e3*o.player.duration,o.playParam.size=o.player.getSize(),o.onLoadFinish&&o.onLoadFinish(),o.onReadyShowDone&&o.onReadyShowDone()},this.player.onPlayState=function(e){o.onPlayState&&o.onPlayState(e)},this.player.onCacheProcess=function(e){o.onCacheProcess&&o.onCacheProcess(e)}}},{key:"_initMp4BoxObject",value:function(){var e=this;this.timerFeed=null,this.mp4Obj=new m,this.mp4Obj.onMp4BoxReady=function(t){var i=e.mp4Obj.getFPS(),n=T(i,e.mp4Obj.getDurationMs()),r=e.mp4Obj.getSampleRate(),a=e.mp4Obj.getSize(),s=e.mp4Obj.getVideoCoder();t===v.CODEC_H265?(e._makeMP4PlayerViewEvent(n,i,r,a,e.mp4Obj.audioNone,s),parseInt(n/1e3),e._avFeedMP4Data(0,0)):e._makeNativePlayer(n,i,r,a,e.mp4Obj.audioNone,s)}}},{key:"_mp4Entry",value:function(){var e=this,t=this;fetch(this.videoURL).then((function(e){return e.arrayBuffer()})).then((function(i){t._initMp4BoxObject(),e.mp4Obj.demux(),e.mp4Obj.appendBufferData(i,0),e.mp4Obj.finishBuffer(),e.mp4Obj.seek(-1)}))}},{key:"_mp4EntryVodStream",value:function(){var e=this,t=this;this.timerFeed=null,this.mp4Obj=new m,this._initMp4BoxObject(),this.mp4Obj.demux();var i=0,n=!1,r=window.setInterval((function(){n||(n=!0,fetch(e.videoURL).then((function(e){return function e(n){return n.read().then((function(a){if(a.done)return t.mp4Obj.finishBuffer(),t.mp4Obj.seek(-1),void window.clearInterval(r);var s=a.value;return t.mp4Obj.appendBufferData(s.buffer,i),i+=s.byteLength,e(n)}))}(e.body.getReader())})).catch((function(e){})))}),1)}},{key:"_cDemuxDecoderEntry",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,i=arguments.length>1&&void 0!==arguments[1]&&arguments[1];this.configFormat.type;var n=this,r=!1,a=new AbortController,s=a.signal,u={width:this.configFormat.playerW,height:this.configFormat.playerH,playerId:this.configFormat.playerId,token:this.configFormat.token,readyShow:this.configFormat.extInfo.readyShow,checkProbe:this.configFormat.extInfo.checkProbe,ignoreAudio:this.configFormat.extInfo.ignoreAudio,playMode:this.playMode,autoPlay:this.configFormat.extInfo.autoPlay,defaultFps:this.configFormat.extInfo.rawFps,cacheLength:this.configFormat.extInfo.cacheLength};this.player=new o.CNativeCore(u),window.g_players[this.player.corePtr]=this.player,this.player.onReadyShowDone=function(){n.configFormat.extInfo.readyShow=!1,n.onReadyShowDone&&n.onReadyShowDone()},this.player.onRelease=function(){a.abort()},this.player.onProbeFinish=function(){r=!0,n.player.config,n.player.audioNone,n.playParam.fps=n.player.config.fps,n.playParam.durationMs=T(n.playParam.fps,1e3*n.player.duration),n.player.duration<0&&(n.playMode=v.PLAYER_MODE_NOTIME_LIVE,n.playParam.durationMs=-1),n.playParam.sampleRate=n.player.config.sampleRate,n.playParam.size={width:n.player.width,height:n.player.height},n.playParam.audioNone=n.player.audioNone,n.player.vCodecID===v.V_CODEC_NAME_HEVC?(n.playParam.videoCodec=v.CODEC_H265,n.playParam.audioIdx<0&&(n.playParam.audioNone=!0),!0!==p.IsSupport265Mse()||!1!==i||n.mediaExtFormat!==v.PLAYER_IN_TYPE_MP4&&n.mediaExtFormat!==v.PLAYER_IN_TYPE_FLV?n.onLoadFinish&&n.onLoadFinish():(a.abort(),n.player.release(),n.mediaExtFormat,v.PLAYER_IN_TYPE_MP4,n.player=null,n.mediaExtFormat===v.PLAYER_IN_TYPE_MP4?n._makeNativePlayer(n.playParam.durationMs,n.playParam.fps,n.playParam.sampleRate,n.playParam.size,!1,n.playParam.videoCodec):n.mediaExtFormat===v.PLAYER_IN_TYPE_FLV&&n._flvJsPlayer(n.playParam.durationMs,n.playParam.audioNone))):(n.playParam.videoCodec=v.CODEC_H264,a.abort(),n.player.release(),n.player=null,n.mediaExtFormat===v.PLAYER_IN_TYPE_MP4?n._makeNativePlayer(n.playParam.durationMs,n.playParam.fps,n.playParam.sampleRate,n.playParam.size,!1,n.playParam.videoCodec):n.mediaExtFormat===v.PLAYER_IN_TYPE_FLV?n._flvJsPlayer(n.playParam.durationMs,n.playParam.audioNone):n.onLoadFinish&&n.onLoadFinish())},this.player.onPlayingTime=function(e){n._durationText(e),n._durationText(n.player.duration),null!=n.onPlayTime&&n.onPlayTime(e)},this.player.onPlayingFinish=function(){n.pause(),null!=n.onPlayTime&&n.onPlayTime(0),n.onPlayFinish&&n.onPlayFinish(),n.player.reFull=!0,n.seek(0)},this.player.onCacheProcess=function(t){e.onCacheProcess&&e.onCacheProcess(t)},this.player.onLoadCache=function(){null!=e.onLoadCache&&e.onLoadCache()},this.player.onLoadCacheFinshed=function(){null!=e.onLoadCacheFinshed&&e.onLoadCacheFinshed()},this.player.onRender=function(e,t,i,r,a){n.snapshotYuvLastFrame.luma=null,n.snapshotYuvLastFrame.chromaB=null,n.snapshotYuvLastFrame.chromaR=null,n.snapshotYuvLastFrame.width=e,n.snapshotYuvLastFrame.height=t,n.snapshotYuvLastFrame.luma=new Uint8Array(i),n.snapshotYuvLastFrame.chromaB=new Uint8Array(r),n.snapshotYuvLastFrame.chromaR=new Uint8Array(a),null!=n.onRender&&n.onRender(e,t,i,r,a)},this.player.onSeekFinish=function(){null!=e.onSeekFinish&&e.onSeekFinish()};var l=!1,h=0,d=function e(i){setTimeout((function(){if(!1===l){if(a.abort(),a=null,s=null,i>=v.FETCH_FIRST_MAX_TIMES)return;a=new AbortController,s=a.signal,e(i+1)}}),v.FETCH_HTTP_FLV_TIMEOUT_MS),fetch(n.videoURL,{signal:s}).then((function(e){if(e.headers.get("Content-Length"),!e.ok)return console.error("error cdemuxdecoder prepare request media failed with http code:",e.status),!1;if(l=!0,e.headers.has("Content-Length"))h=e.headers.get("Content-Length"),n.configFormat.extInfo.coreProbePart<=0?n.player&&n.player.setProbeSize(n.configFormat.extInfo.probeSize):n.player&&n.player.setProbeSize(h*n.configFormat.extInfo.coreProbePart);else{if(n.mediaExtFormat===v.PLAYER_IN_TYPE_FLV)return a.abort(),n.player.release(),n.player=null,n._cLiveFLVDecoderEntry(u),!0;n.player&&n.player.setProbeSize(40960)}return e.headers.get("Content-Length"),n.configFormat.type,n.mediaExtFormat,function e(i){return i.read().then((function(a){if(a.done)return!0===r||(n.player.release(),n.player=null,t0&&void 0!==arguments[0]?arguments[0]:0;if(1===t)return i.player.release(),i.player=null,void i._cLiveG711DecoderEntry(e);if(i.playParam.fps=i.player.mediaInfo.fps,i.playParam.durationMs=-1,i.playMode=v.PLAYER_MODE_NOTIME_LIVE,i.playParam.sampleRate=i.player.mediaInfo.sampleRate,i.playParam.size={width:i.player.mediaInfo.width,height:i.player.mediaInfo.height},i.playParam.audioNone=i.player.mediaInfo.audioNone,i.player.mediaInfo,i.player.vCodecID===v.V_CODEC_NAME_HEVC)i.playParam.videoCodec=v.CODEC_H265,i.playParam.audioIdx<0&&(i.playParam.audioNone=!0),!0===p.IsSupport265Mse()&&i.mediaExtFormat===v.PLAYER_IN_TYPE_FLV?(i.player.release(),i.player=null,i.mediaExtFormat===v.PLAYER_IN_TYPE_FLV&&i._flvJsPlayer(i.playParam.durationMs,i.playParam.audioNone)):i.onLoadFinish&&i.onLoadFinish();else if(i.playParam.videoCodec=v.CODEC_H264,i.player.release(),i.player=null,i.mediaExtFormat===v.PLAYER_IN_TYPE_FLV)i._flvJsPlayer(i.playParam.durationMs,i.playParam.audioNone);else{if(i.mediaExtFormat!==v.PLAYER_IN_TYPE_TS&&i.mediaExtFormat!==v.PLAYER_IN_TYPE_MPEGTS)return-1;i._mpegTsNv3rdPlayer(i.playParam.durationMs,i.playParam.audioNone)}},this.player.onError=function(e){i.onError&&i.onError(e)},this.player.onReadyShowDone=function(){i.configFormat.extInfo.readyShow=!1,i.onReadyShowDone&&i.onReadyShowDone()},this.player.onLoadCache=function(){null!=t.onLoadCache&&t.onLoadCache()},this.player.onLoadCacheFinshed=function(){null!=t.onLoadCacheFinshed&&t.onLoadCacheFinshed()},this.player.onRender=function(e,t,n,r,a){i.snapshotYuvLastFrame.luma=null,i.snapshotYuvLastFrame.chromaB=null,i.snapshotYuvLastFrame.chromaR=null,i.snapshotYuvLastFrame.width=e,i.snapshotYuvLastFrame.height=t,i.snapshotYuvLastFrame.luma=new Uint8Array(n),i.snapshotYuvLastFrame.chromaB=new Uint8Array(r),i.snapshotYuvLastFrame.chromaR=new Uint8Array(a),null!=i.onRender&&i.onRender(e,t,n,r,a)},this.player.onPlayState=function(e){i.onPlayState&&i.onPlayState(e)},this.player.start(this.videoURL)}},{key:"_cWsFLVDecoderEntry",value:function(){var e=this,t=this,i={width:this.configFormat.playerW,height:this.configFormat.playerH,playerId:this.configFormat.playerId,token:this.configFormat.token,readyShow:this.configFormat.extInfo.readyShow,checkProbe:this.configFormat.extInfo.checkProbe,ignoreAudio:this.configFormat.extInfo.ignoreAudio,playMode:this.playMode,autoPlay:this.configFormat.extInfo.autoPlay};i.probeSize=this.configFormat.extInfo.probeSize,this.player=new h.CWsLiveCore(i),i.probeSize,window.g_players[this.player.corePtr]=this.player,this.player.onProbeFinish=function(){t.playParam.fps=t.player.mediaInfo.fps,t.playParam.durationMs=-1,t.playMode=v.PLAYER_MODE_NOTIME_LIVE,t.playParam.sampleRate=t.player.mediaInfo.sampleRate,t.playParam.size={width:t.player.mediaInfo.width,height:t.player.mediaInfo.height},t.playParam.audioNone=t.player.mediaInfo.audioNone,t.player.mediaInfo,t.player.vCodecID===v.V_CODEC_NAME_HEVC?(t.playParam.audioIdx<0&&(t.playParam.audioNone=!0),t.playParam.videoCodec=v.CODEC_H265,!0===p.IsSupport265Mse()&&t.mediaExtFormat===v.PLAYER_IN_TYPE_FLV?(t.player.release(),t.player=null,t._flvJsPlayer(t.playParam.durationMs,t.playParam.audioNone)):t.onLoadFinish&&t.onLoadFinish()):(t.playParam.videoCodec=v.CODEC_H264,t.player.release(),t.player=null,t._flvJsPlayer(t.playParam.durationMs,t.playParam.audioNone))},this.player.onError=function(e){t.onError&&t.onError(e)},this.player.onReadyShowDone=function(){t.configFormat.extInfo.readyShow=!1,t.onReadyShowDone&&t.onReadyShowDone()},this.player.onLoadCache=function(){null!=e.onLoadCache&&e.onLoadCache()},this.player.onLoadCacheFinshed=function(){null!=e.onLoadCacheFinshed&&e.onLoadCacheFinshed()},this.player.onRender=function(e,i,n,r,a){t.snapshotYuvLastFrame.luma=null,t.snapshotYuvLastFrame.chromaB=null,t.snapshotYuvLastFrame.chromaR=null,t.snapshotYuvLastFrame.width=e,t.snapshotYuvLastFrame.height=i,t.snapshotYuvLastFrame.luma=new Uint8Array(n),t.snapshotYuvLastFrame.chromaB=new Uint8Array(r),t.snapshotYuvLastFrame.chromaR=new Uint8Array(a),null!=t.onRender&&t.onRender(e,i,n,r,a)},this.player.start(this.videoURL)}},{key:"_mpegTsEntry",value:function(){var e=this,t=(Module.cwrap("AVPlayerInit","number",["string","string"])(this.configFormat.token,"0.0.0"),new AbortController),i=t.signal;this.timerFeed=null,this.mpegTsObj=new _.MpegTs,this.mpegTsObj.bindReady(e),this.mpegTsObj.onDemuxed=this._mpegTsEntryReady.bind(this),this.mpegTsObj.onReady=function(){var n=null;fetch(e.videoURL,{signal:i}).then((function(r){if(r.headers.has("Content-Length"))return function t(i){return i.read().then((function(r){if(!r.done){var a=r.value;if(null===n)n=a;else{var s=a,o=n.length+s.length,u=new Uint8Array(o);u.set(n),u.set(s,n.length),n=new Uint8Array(u),s=null,u=null}return t(i)}e.mpegTsObj.demux(n)}))}(r.body.getReader());t.abort(),i=null,t=null;var a={width:e.configFormat.playerW,height:e.configFormat.playerH,playerId:e.configFormat.playerId,token:e.configFormat.token,readyShow:e.configFormat.extInfo.readyShow,checkProbe:e.configFormat.extInfo.checkProbe,ignoreAudio:e.configFormat.extInfo.ignoreAudio,playMode:e.playMode,autoPlay:e.configFormat.extInfo.autoPlay};e._cLiveFLVDecoderEntry(a)})).catch((function(e){if(!e.toString().includes("user aborted")){var t=" mpegts request error:"+e;console.error(t)}}))},this.mpegTsObj.initMPEG()}},{key:"_mpegTsEntryReady",value:function(e){var t=e,i=(t.mpegTsObj.getVCodec(),t.mpegTsObj.getACodec()),n=t.mpegTsObj.getDurationMs(),r=t.mpegTsObj.getFPS(),a=t.mpegTsObj.getSampleRate(),s=t.mpegTsObj.getSize(),o=this.mpegTsObj.isHEVC();if(!o)return this.mpegTsObj.releaseTsDemuxer(),this.mpegTsObj=null,this.playParam.durationMs=n,this.playParam.fps=r,this.playParam.sampleRate=a,this.playParam.size=s,this.playParam.audioNone=""==i,this.playParam.videoCodec=o?0:1,this.playParam,void this._mpegTsNv3rdPlayer(this.playParam.durationMs,this.playParam.audioNone);t._makeMP4PlayerViewEvent(n,r,a,s,""==i),parseInt(n/1e3),t._avFeedMP4Data(0,0)}},{key:"_m3u8Entry",value:function(){var e=this,t=this;if(!1===this._isSupportWASM())return this._videoJsPlayer();Module.cwrap("AVPlayerInit","number",["string","string"])(this.configFormat.token,"0.0.0");var i=!1,n=0;this.hlsObj=new g.M3u8,this.hlsObj.bindReady(t),this.hlsObj.onFinished=function(e,r){0==i&&(n=t.hlsObj.getDurationMs(),t.hlsConf.hlsType=r.type,i=!0)},this.hlsObj.onCacheProcess=function(t){e.playMode!==v.PLAYER_MODE_NOTIME_LIVE&&e.onCacheProcess&&e.onCacheProcess(t)},this.hlsObj.onDemuxed=function(e){if(null==t.player){var i=t.hlsObj.isHevcParam,r=(t.hlsObj.getVCodec(),t.hlsObj.getACodec()),a=t.hlsObj.getFPS(),s=t.hlsObj.getSampleRate(),o=t.hlsObj.getSize(),u=!1;if(u=t.hlsObj.getSampleChannel()<=0||""===r,!i)return t.hlsObj.release(),t.hlsObj.mpegTsObj&&t.hlsObj.mpegTsObj.releaseTsDemuxer(),t.hlsObj=null,t.playParam.durationMs=n,t.playParam.fps=a,t.playParam.sampleRate=s,t.playParam.size=o,t.playParam.audioNone=""==r,t.playParam.videoCodec=i?0:1,t.playParam,void t._videoJsPlayer(n);t._makeMP4PlayerViewEvent(n,a,s,o,u)}},this.hlsObj.onSamples=this._hlsOnSamples.bind(this),this.hlsObj.demux(this.videoURL)}},{key:"_hlsOnSamples",value:function(e,t){1==t.video?this.player.appendHevcFrame(t):!1===this.hlsObj.audioNone&&this.player.appendAACFrame(t)}},{key:"_videoJsPlayer",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:-1,t=this,i={probeDurationMS:e,width:this.configFormat.playerW,height:this.configFormat.playerH,playerId:this.configFormat.playerId,ignoreAudio:this.configFormat.extInfo.ignoreAudio,autoPlay:this.configFormat.extInfo.autoPlay,playMode:this.playMode};this.player=new d.NvVideojsCore(i),this.player.onMakeItReady=function(){t.onMakeItReady&&t.onMakeItReady()},this.player.onLoadFinish=function(){t.playParam.size=t.player.getSize(),t.playParam.videoCodec=1,t.player.duration===1/0||t.player.duration<0?(t.playParam.durationMs=-1,t.playMode=v.PLAYER_MODE_NOTIME_LIVE):(t.playParam.durationMs=1e3*t.player.duration,t.playMode=v.PLAYER_MODE_VOD),t.playParam,t.player.duration,t.player.getSize(),t.onLoadFinish&&t.onLoadFinish()},this.player.onReadyShowDone=function(){t.onReadyShowDone&&t.onReadyShowDone()},this.player.onPlayingFinish=function(){t.pause(),t.seek(0),null!=t.onPlayFinish&&t.onPlayFinish()},this.player.onPlayingTime=function(e){t._durationText(e),t._durationText(t.player.duration),null!=t.onPlayTime&&t.onPlayTime(e)},this.player.onSeekFinish=function(){t.onSeekFinish&&t.onSeekFinish()},this.player.onPlayState=function(e){t.onPlayState&&t.onPlayState(e)},this.player.onCacheProcess=function(e){t.onCacheProcess&&t.onCacheProcess(e)},this.player.makeIt(this.videoURL)}},{key:"_flvJsPlayer",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:-1,t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],i=this,n={width:this.configFormat.playerW,height:this.configFormat.playerH,playerId:this.configFormat.playerId,ignoreAudio:this.configFormat.extInfo.ignoreAudio,duration:e,autoPlay:this.configFormat.extInfo.autoPlay,audioNone:t};this.player=new c.NvFlvjsCore(n),this.player.onLoadFinish=function(){i.playParam.size=i.player.getSize(),!i.player.duration||NaN===i.player.duration||i.player.duration===1/0||i.player.duration<0?(i.playParam.durationMs=-1,i.playMode=v.PLAYER_MODE_NOTIME_LIVE):(i.playParam.durationMs=1e3*i.player.duration,i.playMode=v.PLAYER_MODE_VOD),i.onLoadFinish&&i.onLoadFinish()},this.player.onReadyShowDone=function(){i.onReadyShowDone&&i.onReadyShowDone()},this.player.onPlayingTime=function(e){i._durationText(e),i._durationText(i.player.duration),null!=i.onPlayTime&&i.onPlayTime(e)},this.player.onPlayingFinish=function(){i.pause(),i.seek(0),null!=i.onPlayFinish&&i.onPlayFinish()},this.player.onPlayState=function(e){i.onPlayState&&i.onPlayState(e)},this.player.onCacheProcess=function(e){i.onCacheProcess&&i.onCacheProcess(e)},this.player.makeIt(this.videoURL)}},{key:"_mpegTsNv3rdPlayer",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:-1,t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],i=this,n={width:this.configFormat.playerW,height:this.configFormat.playerH,playerId:this.configFormat.playerId,ignoreAudio:this.configFormat.extInfo.ignoreAudio,duration:e,autoPlay:this.configFormat.extInfo.autoPlay,audioNone:t};this.player=new f.NvMpegTsCore(n),this.player.onLoadFinish=function(){i.playParam.size=i.player.getSize(),!i.player.duration||NaN===i.player.duration||i.player.duration===1/0||i.player.duration<0?(i.playParam.durationMs=-1,i.playMode=v.PLAYER_MODE_NOTIME_LIVE):(i.playParam.durationMs=1e3*i.player.duration,i.playMode=v.PLAYER_MODE_VOD),i.onLoadFinish&&i.onLoadFinish()},this.player.onReadyShowDone=function(){i.onReadyShowDone&&i.onReadyShowDone()},this.player.onPlayingTime=function(e){i._durationText(e),i._durationText(i.player.duration),null!=i.onPlayTime&&i.onPlayTime(e)},this.player.onPlayingFinish=function(){i.pause(),i.seek(0),null!=i.onPlayFinish&&i.onPlayFinish()},this.player.onPlayState=function(e){i.onPlayState&&i.onPlayState(e)},this.player.onCacheProcess=function(e){i.onCacheProcess&&i.onCacheProcess(e)},this.player.makeIt(this.videoURL)}},{key:"_raw265Entry",value:function(){var e=this;this.videoURL;var t=function t(){setTimeout((function(){e.workerParse.postMessage({cmd:"get-nalu",data:null,msg:"get-nalu"}),e.workerParse.parseEmpty,e.workerFetch.onMsgFetchFinished,!0===e.workerFetch.onMsgFetchFinished&&!0===e.workerParse.frameListEmpty&&!1===e.workerParse.streamEmpty&&e.workerParse.postMessage({cmd:"last-nalu",data:null,msg:"last-nalu"}),!0===e.workerParse.parseEmpty&&(e.workerParse.stopNaluInterval=!0),!0!==e.workerParse.stopNaluInterval&&t()}),1e3)};this._makeMP4PlayerViewEvent(-1,this.configFormat.extInfo.rawFps,-1,{width:this.configFormat.playerW,height:this.configFormat.playerH},!0,v.CODEC_H265),this.timerFeed&&(window.clearInterval(this.timerFeed),this.timerFeed=null),e.workerFetch=new Worker(p.GetScriptPath((function(){var e=new AbortController,t=e.signal,i=null;onmessage=function(n){var r=n.data;switch(void 0===r.cmd||null===r.cmd?"":r.cmd){case"start":var a=r.url;"http"===r.type?fetch(a,{signal:t}).then((function(e){return function e(t){return t.read().then((function(i){if(!i.done){var n=i.value;return postMessage({cmd:"fetch-chunk",data:n,msg:"fetch-chunk"}),e(t)}postMessage({cmd:"fetch-fin",data:null,msg:"fetch-fin"})}))}(e.body.getReader())})).catch((function(e){})):"websocket"===r.type&&function(e){(i=new WebSocket(e)).binaryType="arraybuffer",i.onopen=function(e){i.send("Hello WebSockets!")},i.onmessage=function(e){if(e.data instanceof ArrayBuffer){var t=e.data;t.byteLength>0&&postMessage({cmd:"fetch-chunk",data:new Uint8Array(t),msg:"fetch-chunk"})}},i.onclose=function(e){postMessage({cmd:"fetch-fin",data:null,msg:"fetch-fin"})}}(a),postMessage({cmd:"default",data:"WORKER STARTED",msg:"default"});break;case"stop":"http"===r.type?e.abort():"websocket"===r.type&&i&&i.close(),close()}}}))),e.workerFetch.onMsgFetchFinished=!1,e.workerFetch.onmessage=function(i){var n=i.data;switch(void 0===n.cmd||null===n.cmd?"":n.cmd){case"fetch-chunk":var r=n.data;e.workerParse.postMessage({cmd:"append-chunk",data:r,msg:"append-chunk"});break;case"fetch-fin":e.workerFetch.onMsgFetchFinished=!0,t()}},e.workerParse=new Worker(p.GetScriptPath((function(){var e,t=((e=new Object).frameList=[],e.stream=null,e.frameListEmpty=function(){return e.frameList.length<=0},e.streamEmpty=function(){return null===e.stream||e.stream.length<=0},e.checkEmpty=function(){return!0===e.streamEmpty()&&!0===e.frameListEmpty()||(e.stream,e.frameList,!1)},e.pushFrameRet=function(t){return!(!t||null==t||null==t||(e.frameList&&null!=e.frameList&&null!=e.frameList||(e.frameList=[]),e.frameList.push(t),0))},e.nextFrame=function(){return!e.frameList&&null==e.frameList||null==e.frameList&&e.frameList.length<1?null:e.frameList.shift()},e.clearFrameRet=function(){e.frameList=null},e.setStreamRet=function(t){e.stream=t},e.getStreamRet=function(){return e.stream},e.appendStreamRet=function(t){if(!t||void 0===t||null==t)return!1;if(!e.stream||void 0===e.stream||null==e.stream)return e.stream=t,!0;var i=e.stream.length,n=t.length,r=new Uint8Array(i+n);r.set(e.stream,0),r.set(t,i),e.stream=r;for(var a=0;a<9999;a++){var s=e.nextNalu();if(!1===s||null==s)break;e.frameList.push(s)}return!0},e.subBuf=function(t,i){var n=new Uint8Array(e.stream.subarray(t,i+1));return e.stream=new Uint8Array(e.stream.subarray(i+1)),n},e.lastNalu=function(){var t=e.subBuf(0,e.stream.length);e.frameList.push(t)},e.nextNalu=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;if(null==e.stream||e.stream.length<=4)return!1;for(var i=-1,n=0;n=e.stream.length)return!1;if(0==e.stream[n]&&0==e.stream[n+1]&&1==e.stream[n+2]||0==e.stream[n]&&0==e.stream[n+1]&&0==e.stream[n+2]&&1==e.stream[n+3]){var r=n;if(n+=3,-1==i)i=r;else{if(t<=1)return e.subBuf(i,r-1);t-=1}}}return!1},e.nextNalu2=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;if(null==e.stream||e.stream.length<=4)return!1;for(var i=-1,n=0;n=e.stream.length)return-1!=i&&e.subBuf(i,e.stream.length-1);var r="0 0 1"==e.stream.slice(n,n+3).join(" "),a="0 0 0 1"==e.stream.slice(n,n+4).join(" ");if(r||a){var s=n;if(n+=3,-1==i)i=s;else{if(t<=1)return e.subBuf(i,s-1);t-=1}}}return!1},e);onmessage=function(e){var i=e.data;switch(void 0===i.cmd||null===i.cmd?"":i.cmd){case"append-chunk":var n=i.data;t.appendStreamRet(n);var r=t.nextFrame();postMessage({cmd:"return-nalu",data:r,msg:"return-nalu",parseEmpty:t.checkEmpty(),streamEmpty:t.streamEmpty(),frameListEmpty:t.frameListEmpty()});break;case"get-nalu":var a=t.nextFrame();postMessage({cmd:"return-nalu",data:a,msg:"return-nalu",parseEmpty:t.checkEmpty(),streamEmpty:t.streamEmpty(),frameListEmpty:t.frameListEmpty()});break;case"last-nalu":var s=t.lastNalu();postMessage({cmd:"return-nalu",data:s,msg:"return-nalu",parseEmpty:t.checkEmpty(),streamEmpty:t.streamEmpty(),frameListEmpty:t.frameListEmpty()});break;case"stop":postMessage("parse - WORKER STOPPED: "+i),close()}}}))),e.workerParse.stopNaluInterval=!1,e.workerParse.parseEmpty=!1,e.workerParse.streamEmpty=!1,e.workerParse.frameListEmpty=!1,e.workerParse.onmessage=function(t){var i=t.data;switch(void 0===i.cmd||null===i.cmd?"":i.cmd){case"return-nalu":var n=i.data,r=i.parseEmpty,a=i.streamEmpty,s=i.frameListEmpty;e.workerParse.parseEmpty=r,e.workerParse.streamEmpty=a,e.workerParse.frameListEmpty=s,!1===n||null==n?!0===e.workerFetch.onMsgFetchFinished&&!0===r&&(e.workerParse.stopNaluInterval=!0):(e.append265NaluFrame(n),e.workerParse.postMessage({cmd:"get-nalu",data:null,msg:"get-nalu"}))}},p.ParseGetMediaURL(this.videoURL),this.workerFetch.postMessage({cmd:"start",url:p.ParseGetMediaURL(this.videoURL),type:this.mediaExtProtocol,msg:"start"}),function t(){setTimeout((function(){e.configFormat.extInfo.readyShow&&(e.player.cacheYuvBuf.getState()!=CACHE_APPEND_STATUS_CODE.NULL?(e.player.playFrameYUV(!0,!0),e.configFormat.extInfo.readyShow=!1,e.onReadyShowDone&&e.onReadyShowDone()):t())}),1e3)}()}},{key:"append265NaluFrame",value:function(e){var t={data:e,pts:this.rawModePts};this.player.appendHevcFrame(t),this.configFormat.extInfo.readyShow&&this.player.cacheYuvBuf.getState()!=CACHE_APPEND_STATUS_CODE.NULL&&(this.player.playFrameYUV(!0,!0),this.configFormat.extInfo.readyShow=!1,this.onReadyShowDone&&this.onReadyShowDone()),this.rawModePts+=1/this.configFormat.extInfo.rawFps}}])&&r(i.prototype,E),w&&r(i,w),e}();i.H265webjs=E,t.new265webjs=function(e,t){return new E(e,t)}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./consts":52,"./decoder/av-common":56,"./decoder/c-http-g711-core":57,"./decoder/c-httplive-core":58,"./decoder/c-native-core":59,"./decoder/c-wslive-core":60,"./decoder/cache":61,"./decoder/player-core":65,"./demuxer/m3u8":69,"./demuxer/mp4":71,"./demuxer/mpegts/mpeg.js":74,"./demuxer/ts":75,"./native/mp4-player":77,"./native/nv-flvjs-core":78,"./native/nv-mpegts-core":79,"./native/nv-videojs-core":80,"./render-engine/webgl-420p":81,"./utils/static-mem":82,"./utils/ui/ui":83}],77:[function(e,t,i){"use strict";function n(e,t){for(var i=0;i=t.duration-.04)return t.onCacheProcess&&t.onCacheProcess(t.duration),void window.clearInterval(t.bufferInterval);t.onCacheProcess&&t.onCacheProcess(e)}),200)},this.videoTag.src=e,this.videoTag.style.width="100%",this.videoTag.style.height="100%",i.appendChild(this.videoTag)}},{key:"setPlaybackRate",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;return!(e<=0||null==this.videoTag||null===this.videoTag||(this.videoTag.playbackRate=e,0))}},{key:"getPlaybackRate",value:function(){return null==this.videoTag||null===this.videoTag?0:this.videoTag.playbackRate}},{key:"getSize",value:function(){return{width:this.videoTag.videoWidth>0?this.videoTag.videoWidth:this.configFormat.width,height:this.videoTag.videoHeight>0?this.videoTag.videoHeight:this.configFormat.height}}},{key:"play",value:function(){this.videoTag.play()}},{key:"seek",value:function(e){this.videoTag.currentTime=e}},{key:"pause",value:function(){this.videoTag.pause()}},{key:"setVoice",value:function(e){this.videoTag.volume=e}},{key:"isPlayingState",value:function(){return!this.videoTag.paused}},{key:"release",value:function(){this.videoTag&&this.videoTag.remove(),this.videoTag=null,this.onLoadFinish=null,this.onPlayingTime=null,this.onPlayingFinish=null,this.onPlayState=null,null!==this.bufferInterval&&(window.clearInterval(this.bufferInterval),this.bufferInterval=null),window.onclick=document.body.onclick=null}},{key:"nativeNextFrame",value:function(){void 0!==this.videoTag&&null!==this.videoTag&&(this.videoTag.currentTime+=1/this.configFormat.fps)}}])&&n(t.prototype,i),a&&n(t,a),e}();i.Mp4Player=a},{"../consts":52}],78:[function(e,t,i){"use strict";function n(e,t){for(var i=0;i0&&s.GetMsTime()-t.lastDecodedFrameTime>1e4)return window.clearInterval(t.checkPicBlockInterval),t.checkPicBlockInterval=null,void t._reBuildFlvjs(e)}),1e3)}},{key:"_checkLoadState",value:function(e){var t=this;this.checkStartIntervalCount=0,this.checkStartInterval=window.setInterval((function(){return t.lastDecodedFrame,t.isInitDecodeFrames,t.checkStartIntervalCount,!1!==t.isInitDecodeFrames?(t.checkStartIntervalCount=0,window.clearInterval(t.checkStartInterval),void(t.checkStartInterval=null)):(t.checkStartIntervalCount+=1,t.checkStartIntervalCount>20?(window.clearInterval(t.checkStartInterval),t.checkStartIntervalCount=0,t.checkStartInterval=null,void(!1===t.isInitDecodeFrames&&t._reBuildFlvjs(e))):void 0)}),500)}},{key:"makeIt",value:function(e){var t=this;if(a.isSupported()){var i=document.querySelector("#"+this.configFormat.playerId);this.videoTag=document.createElement("video"),this.videoTag.id=this.myPlayerID,this.videoTag.style.width=this.configFormat.width+"px",this.videoTag.style.height=this.configFormat.height+"px",i.appendChild(this.videoTag),!0===this.configFormat.autoPlay&&(this.videoTag.muted="muted",this.videoTag.autoplay="autoplay",window.onclick=document.body.onclick=function(e){t.videoTag.muted=!1,t.isPlayingState(),window.onclick=document.body.onclick=null}),this.videoTag.onplay=function(){var e=t.isPlayingState();t.onPlayState&&t.onPlayState(e)},this.videoTag.onpause=function(){var e=t.isPlayingState();t.onPlayState&&t.onPlayState(e)};var n={hasVideo:!0,hasAudio:!(!0===this.configFormat.audioNone),type:"flv",url:e,isLive:this.configFormat.duration<=0,withCredentials:!1};this.myPlayer=a.createPlayer(n),this.myPlayer.attachMediaElement(this.videoTag),this.myPlayer.on(a.Events.MEDIA_INFO,(function(e){t.videoTag.videoWidth,!1===t.isInitDecodeFrames&&(t.isInitDecodeFrames=!0,t.width=Math.max(t.videoTag.videoWidth,e.width),t.height=Math.max(t.videoTag.videoHeight,e.height),t.duration=t.videoTag.duration,t.duration,t.onLoadFinish&&t.onLoadFinish(),t.onReadyShowDone&&t.onReadyShowDone(),t._loopBufferState(),t.isPlayingState(),t.videoTag.ontimeupdate=function(){t.onPlayingTime&&t.onPlayingTime(t.videoTag.currentTime)},t.duration!==1/0&&t.duration>0&&(t.videoTag.onended=function(){t.onPlayingFinish&&t.onPlayingFinish()}))})),this.myPlayer.on(a.Events.STATISTICS_INFO,(function(e){t.videoTag.videoWidth,t.videoTag.videoHeight,t.videoTag.duration,!1===t.isInitDecodeFrames&&t.videoTag.videoWidth>0&&t.videoTag.videoHeight>0&&(t.isInitDecodeFrames=!0,t.width=t.videoTag.videoWidth,t.height=t.videoTag.videoHeight,t.duration=t.videoTag.duration,t.duration,t.onLoadFinish&&t.onLoadFinish(),t.onReadyShowDone&&t.onReadyShowDone(),t._loopBufferState(),t.isPlayingState(),t.videoTag.ontimeupdate=function(){t.onPlayingTime&&t.onPlayingTime(t.videoTag.currentTime)},t.duration!==1/0&&(t.videoTag.onended=function(){t.onPlayingFinish&&t.onPlayingFinish()})),t.lastDecodedFrame=e.decodedFrames,t.lastDecodedFrameTime=s.GetMsTime()})),this.myPlayer.on(a.Events.SCRIPTDATA_ARRIVED,(function(e){})),this.myPlayer.on(a.Events.METADATA_ARRIVED,(function(e){!1===t.isInitDecodeFrames&&e.width&&e.width>0&&(t.isInitDecodeFrames=!0,t.duration=e.duration,t.width=e.width,t.height=e.height,t.duration,t.onLoadFinish&&t.onLoadFinish(),t.onReadyShowDone&&t.onReadyShowDone(),t._loopBufferState(),t.isPlayingState(),t.videoTag.ontimeupdate=function(){t.onPlayingTime&&t.onPlayingTime(t.videoTag.currentTime)},t.duration!==1/0&&(t.videoTag.onended=function(){t.onPlayingFinish&&t.onPlayingFinish()}))})),this.myPlayer.on(a.Events.ERROR,(function(i,n,r){t.myPlayer&&t._reBuildFlvjs(e)})),this.myPlayer.load(),this._checkLoadState(e),this._checkPicBlock(e)}else console.error("FLV is AVC/H.264, But your brower do not support mse!")}},{key:"setPlaybackRate",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;return!(e<=0||null==this.videoTag||null===this.videoTag||(this.videoTag.playbackRate=e,0))}},{key:"getPlaybackRate",value:function(){return null==this.videoTag||null===this.videoTag?0:this.videoTag.playbackRate}},{key:"getSize",value:function(){return{width:this.videoTag.videoWidth>0?this.videoTag.videoWidth:this.width,height:this.videoTag.videoHeight>0?this.videoTag.videoHeight:this.height}}},{key:"play",value:function(){this.myPlayer.play()}},{key:"seek",value:function(e){this.myPlayer.currentTime=e}},{key:"pause",value:function(){this.myPlayer.pause()}},{key:"setVoice",value:function(e){this.myPlayer.volume=e}},{key:"isPlayingState",value:function(){return!this.videoTag.paused}},{key:"_loopBufferState",value:function(){var e=this;e.duration<=0&&(e.duration=e.videoTag.duration),null!==e.bufferInterval&&(window.clearInterval(e.bufferInterval),e.bufferInterval=null),e.bufferInterval=window.setInterval((function(){if(!e.duration||e.duration<0)window.clearInterval(e.bufferInterval);else{var t=e.videoTag.buffered.end(0);if(t>=e.duration-.04)return e.onCacheProcess&&e.onCacheProcess(e.duration),void window.clearInterval(e.bufferInterval);e.onCacheProcess&&e.onCacheProcess(t)}}),200)}},{key:"_releaseFlvjs",value:function(){this.myPlayer,this.myPlayer.pause(),this.myPlayer.unload(),this.myPlayer.detachMediaElement(),this.myPlayer.destroy(),this.myPlayer=null,this.videoTag.remove(),this.videoTag=null,null!==this.checkStartInterval&&(this.checkStartIntervalCount=0,window.clearInterval(this.checkStartInterval),this.checkStartInterval=null),null!==this.checkPicBlockInterval&&(window.clearInterval(this.checkPicBlockInterval),this.checkPicBlockInterval=null),this.isInitDecodeFrames=!1,this.lastDecodedFrame=0,this.lastDecodedFrameTime=-1}},{key:"release",value:function(){null!==this.checkStartInterval&&(this.checkStartIntervalCount=0,window.clearInterval(this.checkStartInterval),this.checkStartInterval=null),null!==this.checkPicBlockInterval&&(window.clearInterval(this.checkPicBlockInterval),this.checkPicBlockInterval=null),null!==this.bufferInterval&&(window.clearInterval(this.bufferInterval),this.bufferInterval=null),this._releaseFlvjs(),this.myPlayerID=null,this.videoContaner=null,this.onLoadFinish=null,this.onPlayingTime=null,this.onPlayingFinish=null,this.onReadyShowDone=null,this.onPlayState=null,window.onclick=document.body.onclick=null}}])&&n(t.prototype,i),o&&n(t,o),e}();i.NvFlvjsCore=o},{"../consts":52,"../decoder/av-common":56,"../demuxer/flv-hevc/flv-hevc.js":68,"../version":84}],79:[function(e,t,i){"use strict";function n(e,t){for(var i=0;i0&&s.GetMsTime()-t.lastDecodedFrameTime>1e4)return window.clearInterval(t.checkPicBlockInterval),t.checkPicBlockInterval=null,void t._reBuildMpegTsjs(e)}),1e3)}},{key:"_checkLoadState",value:function(e){var t=this;this.checkStartIntervalCount=0,this.checkStartInterval=window.setInterval((function(){return t.lastDecodedFrame,t.isInitDecodeFrames,t.checkStartIntervalCount,!1!==t.isInitDecodeFrames?(t.checkStartIntervalCount=0,window.clearInterval(t.checkStartInterval),void(t.checkStartInterval=null)):(t.checkStartIntervalCount+=1,t.checkStartIntervalCount>20?(window.clearInterval(t.checkStartInterval),t.checkStartIntervalCount=0,t.checkStartInterval=null,void(!1===t.isInitDecodeFrames&&t._reBuildMpegTsjs(e))):void 0)}),500)}},{key:"makeIt",value:function(e){var t=this;if(a.isSupported()){var i=document.querySelector("#"+this.configFormat.playerId);this.videoTag=document.createElement("video"),this.videoTag.id=this.myPlayerID,this.videoTag.style.width=this.configFormat.width+"px",this.videoTag.style.height=this.configFormat.height+"px",i.appendChild(this.videoTag),!0===this.configFormat.autoPlay&&(this.videoTag.muted="muted",this.videoTag.autoplay="autoplay",window.onclick=document.body.onclick=function(e){t.videoTag.muted=!1,t.isPlayingState(),window.onclick=document.body.onclick=null}),this.videoTag.onplay=function(){var e=t.isPlayingState();t.onPlayState&&t.onPlayState(e)},this.videoTag.onpause=function(){var e=t.isPlayingState();t.onPlayState&&t.onPlayState(e)};var n={hasVideo:!0,hasAudio:!(!0===this.configFormat.audioNone),type:"mse",url:e,isLive:this.configFormat.duration<=0,withCredentials:!1};this.myPlayer=a.createPlayer(n),this.myPlayer.attachMediaElement(this.videoTag),this.myPlayer.on(a.Events.MEDIA_INFO,(function(e){t.videoTag.videoWidth,!1===t.isInitDecodeFrames&&(t.isInitDecodeFrames=!0,t.width=Math.max(t.videoTag.videoWidth,e.width),t.height=Math.max(t.videoTag.videoHeight,e.height),t.videoTag.duration&&e.duration?t.videoTag.duration?t.duration=t.videoTag.duration:e.duration&&(t.duration=e.duration):t.duration=t.configFormat.duration/1e3,t.duration,t.onLoadFinish&&t.onLoadFinish(),t.onReadyShowDone&&t.onReadyShowDone(),t._loopBufferState(),t.isPlayingState(),t.videoTag.ontimeupdate=function(){t.onPlayingTime&&t.onPlayingTime(t.videoTag.currentTime)},t.duration!==1/0&&t.duration>0&&(t.videoTag.onended=function(){t.onPlayingFinish&&t.onPlayingFinish()}))})),this.myPlayer.on(a.Events.SCRIPTDATA_ARRIVED,(function(e){})),this.myPlayer.on(a.Events.ERROR,(function(i,n,r){t.myPlayer&&t._reBuildMpegTsjs(e)})),this.myPlayer.load(),this._checkLoadState(e),this._checkPicBlock(e)}else console.error("FLV is AVC/H.264, But your brower do not support mse!")}},{key:"setPlaybackRate",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;return!(e<=0||null==this.videoTag||null===this.videoTag||(this.videoTag.playbackRate=e,0))}},{key:"getPlaybackRate",value:function(){return null==this.videoTag||null===this.videoTag?0:this.videoTag.playbackRate}},{key:"getSize",value:function(){return{width:this.videoTag.videoWidth>0?this.videoTag.videoWidth:this.width,height:this.videoTag.videoHeight>0?this.videoTag.videoHeight:this.height}}},{key:"play",value:function(){this.videoTag,this.videoTag.play()}},{key:"seek",value:function(e){this.videoTag.currentTime=e}},{key:"pause",value:function(){this.videoTag.pause()}},{key:"setVoice",value:function(e){this.videoTag.volume=e}},{key:"isPlayingState",value:function(){return!this.videoTag.paused}},{key:"_loopBufferState",value:function(){var e=this;e.duration<=0&&e.videoTag.duration&&(e.duration=e.videoTag.duration),null!==e.bufferInterval&&(window.clearInterval(e.bufferInterval),e.bufferInterval=null),e.bufferInterval=window.setInterval((function(){if(e.configFormat.duration<=0)window.clearInterval(e.bufferInterval);else{var t=e.videoTag.buffered.end(0);if(t>=e.duration-.04)return e.onCacheProcess&&e.onCacheProcess(e.duration),void window.clearInterval(e.bufferInterval);e.onCacheProcess&&e.onCacheProcess(t)}}),200)}},{key:"_releaseMpegTsjs",value:function(){this.myPlayer,this.myPlayer.pause(),this.myPlayer.unload(),this.myPlayer.detachMediaElement(),this.myPlayer.destroy(),this.myPlayer=null,this.videoTag.remove(),this.videoTag=null,null!==this.checkStartInterval&&(this.checkStartIntervalCount=0,window.clearInterval(this.checkStartInterval),this.checkStartInterval=null),null!==this.checkPicBlockInterval&&(window.clearInterval(this.checkPicBlockInterval),this.checkPicBlockInterval=null),this.isInitDecodeFrames=!1,this.lastDecodedFrame=0,this.lastDecodedFrameTime=-1}},{key:"release",value:function(){null!==this.checkStartInterval&&(this.checkStartIntervalCount=0,window.clearInterval(this.checkStartInterval),this.checkStartInterval=null),null!==this.checkPicBlockInterval&&(window.clearInterval(this.checkPicBlockInterval),this.checkPicBlockInterval=null),null!==this.bufferInterval&&(window.clearInterval(this.bufferInterval),this.bufferInterval=null),this._releaseMpegTsjs(),this.myPlayerID=null,this.videoContaner=null,this.onLoadFinish=null,this.onPlayingTime=null,this.onPlayingFinish=null,this.onReadyShowDone=null,this.onPlayState=null,window.onclick=document.body.onclick=null}}])&&n(t.prototype,i),o&&n(t,o),e}();i.NvMpegTsCore=o},{"../consts":52,"../decoder/av-common":56,"../version":84,"mpegts.js":41}],80:[function(e,t,i){"use strict";function n(e,t){for(var i=0;i0&&void 0!==arguments[0]?arguments[0]:1;return!(e<=0||null==this.videoTag||null===this.videoTag||(this.videoTag.playbackRate=e,0))}},{key:"getPlaybackRate",value:function(){return null==this.videoTag||null===this.videoTag?0:this.videoTag.playbackRate}},{key:"getSize",value:function(){return this.myPlayer.videoWidth()<=0?{width:this.videoTag.videoWidth,height:this.videoTag.videoHeight}:{width:this.myPlayer.videoWidth(),height:this.myPlayer.videoHeight()}}},{key:"play",value:function(){void 0===this.videoTag||null===this.videoTag?this.myPlayer.play():this.videoTag.play()}},{key:"seek",value:function(e){void 0===this.videoTag||null===this.videoTag?this.myPlayer.currentTime=e:this.videoTag.currentTime=e}},{key:"pause",value:function(){void 0===this.videoTag||null===this.videoTag?this.myPlayer.pause():this.videoTag.pause()}},{key:"setVoice",value:function(e){void 0===this.videoTag||null===this.videoTag?this.myPlayer.volume=e:this.videoTag.volume=e}},{key:"isPlayingState",value:function(){return!this.myPlayer.paused()}},{key:"_loopBufferState",value:function(){var e=this;e.duration<=0&&(e.duration=e.videoTag.duration),null!==e.bufferInterval&&(window.clearInterval(e.bufferInterval),e.bufferInterval=null),e.configFormat.probeDurationMS,e.configFormat.probeDurationMS<=0||e.duration<=0||(e.bufferInterval=window.setInterval((function(){var t=e.videoTag.buffered.end(0);if(t>=e.duration-.04)return e.onCacheProcess&&e.onCacheProcess(e.duration),void window.clearInterval(e.bufferInterval);e.onCacheProcess&&e.onCacheProcess(t)}),200))}},{key:"release",value:function(){this.loadSuccess=!1,void 0!==this.bootInterval&&null!==this.bootInterval&&(window.clearInterval(this.bootInterval),this.bootInterval=null),this.myPlayer.dispose(),this.myPlayerID=null,this.myPlayer=null,this.videoContaner=null,this.videoTag=null,this.onLoadFinish=null,this.onPlayingTime=null,this.onPlayingFinish=null,this.onSeekFinish=null,this.onReadyShowDone=null,this.onPlayState=null,null!==this.bufferInterval&&(window.clearInterval(this.bufferInterval),this.bufferInterval=null),window.onclick=document.body.onclick=null}}])&&n(t.prototype,i),s&&n(t,s),e}();i.NvVideojsCore=s},{"../consts":52,"../version":84,"video.js":47}],81:[function(e,t,i){"use strict";e("../decoder/av-common");function n(e){this.gl=e,this.texture=e.createTexture(),e.bindTexture(e.TEXTURE_2D,this.texture),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE)}n.prototype.bind=function(e,t,i){var n=this.gl;n.activeTexture([n.TEXTURE0,n.TEXTURE1,n.TEXTURE2][e]),n.bindTexture(n.TEXTURE_2D,this.texture),n.uniform1i(n.getUniformLocation(t,i),e)},n.prototype.fill=function(e,t,i){var n=this.gl;n.bindTexture(n.TEXTURE_2D,this.texture),n.texImage2D(n.TEXTURE_2D,0,n.LUMINANCE,e,t,0,n.LUMINANCE,n.UNSIGNED_BYTE,i)},t.exports={renderFrame:function(e,t,i,n,r,a){e.viewport(0,0,e.canvas.width,e.canvas.height),e.clearColor(0,0,0,0),e.clear(e.COLOR_BUFFER_BIT),e.y.fill(r,a,t),e.u.fill(r>>1,a>>1,i),e.v.fill(r>>1,a>>1,n),e.drawArrays(e.TRIANGLE_STRIP,0,4)},setupCanvas:function(e,t){var i=e.getContext("webgl")||e.getContext("experimental-webgl");if(!i)return i;var r=i.createProgram(),a=["attribute highp vec4 aVertexPosition;","attribute vec2 aTextureCoord;","varying highp vec2 vTextureCoord;","void main(void) {"," gl_Position = aVertexPosition;"," vTextureCoord = aTextureCoord;","}"].join("\n"),s=i.createShader(i.VERTEX_SHADER);i.shaderSource(s,a),i.compileShader(s);var o=["precision highp float;","varying lowp vec2 vTextureCoord;","uniform sampler2D YTexture;","uniform sampler2D UTexture;","uniform sampler2D VTexture;","const mat4 YUV2RGB = mat4","("," 1.1643828125, 0, 1.59602734375, -.87078515625,"," 1.1643828125, -.39176171875, -.81296875, .52959375,"," 1.1643828125, 2.017234375, 0, -1.081390625,"," 0, 0, 0, 1",");","void main(void) {"," gl_FragColor = vec4( texture2D(YTexture, vTextureCoord).x, texture2D(UTexture, vTextureCoord).x, texture2D(VTexture, vTextureCoord).x, 1) * YUV2RGB;","}"].join("\n"),u=i.createShader(i.FRAGMENT_SHADER);i.shaderSource(u,o),i.compileShader(u),i.attachShader(r,s),i.attachShader(r,u),i.linkProgram(r),i.useProgram(r),i.getProgramParameter(r,i.LINK_STATUS);var l=i.getAttribLocation(r,"aVertexPosition");i.enableVertexAttribArray(l);var h=i.getAttribLocation(r,"aTextureCoord");i.enableVertexAttribArray(h);var d=i.createBuffer();i.bindBuffer(i.ARRAY_BUFFER,d),i.bufferData(i.ARRAY_BUFFER,new Float32Array([1,1,0,-1,1,0,1,-1,0,-1,-1,0]),i.STATIC_DRAW),i.vertexAttribPointer(l,3,i.FLOAT,!1,0,0);var c=i.createBuffer();return i.bindBuffer(i.ARRAY_BUFFER,c),i.bufferData(i.ARRAY_BUFFER,new Float32Array([1,0,0,0,1,1,0,1]),i.STATIC_DRAW),i.vertexAttribPointer(h,2,i.FLOAT,!1,0,0),i.y=new n(i),i.u=new n(i),i.v=new n(i),i.y.bind(0,r,"YTexture"),i.u.bind(1,r,"UTexture"),i.v.bind(2,r,"VTexture"),i},releaseContext:function(e){e.deleteTexture(e.y.texture),e.deleteTexture(e.u.texture),e.deleteTexture(e.v.texture)}}},{"../decoder/av-common":56}],82:[function(e,t,i){(function(e){"use strict";e.STATIC_MEM_wasmDecoderState=-1,e.STATICE_MEM_playerCount=-1,e.STATICE_MEM_playerIndexPtr=0}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],83:[function(e,t,i){"use strict";function n(e,t){for(var i=0;i New265WebJs + +declare global { + interface Window { + new265webjs: new265webJsFn + } +} + +export default class H265webjsModule { + static createPlayer: (url: string, config: Web265JsConfig) => New265WebJs + static clear(): void +} diff --git a/web/public/static/js/h265web/index.js b/web/public/static/js/h265web/index.js new file mode 100644 index 0000000..c7ecdc9 --- /dev/null +++ b/web/public/static/js/h265web/index.js @@ -0,0 +1,32 @@ +/********************************************************* + * LICENSE: LICENSE-Free_CN.MD + * + * Author: Numberwolf - ChangYanlong + * QQ: 531365872 + * QQ Group:925466059 + * Wechat: numberwolf11 + * Discord: numberwolf#8694 + * E-Mail: porschegt23@foxmail.com + * Github: https://github.com/numberwolf/h265web.js + * + * 作者: 小老虎(Numberwolf)(常炎隆) + * QQ: 531365872 + * QQ群: 531365872 + * 微信: numberwolf11 + * Discord: numberwolf#8694 + * 邮箱: porschegt23@foxmail.com + * 博客: https://www.jianshu.com/u/9c09c1e00fd1 + * Github: https://github.com/numberwolf/h265web.js + * + **********************************************************/ +require('./h265webjs-v20221106'); +export default class h265webjs { + static createPlayer(videoURL, config) { + return window.new265webjs(videoURL, config); + } + + static clear() { + global.STATICE_MEM_playerCount = -1; + global.STATICE_MEM_playerIndexPtr = 0; + } +} diff --git a/web/public/static/js/h265web/missile-v20221120.wasm b/web/public/static/js/h265web/missile-v20221120.wasm new file mode 100644 index 0000000..629ce98 Binary files /dev/null and b/web/public/static/js/h265web/missile-v20221120.wasm differ diff --git a/web/public/static/js/h265web/missile.js b/web/public/static/js/h265web/missile.js new file mode 100644 index 0000000..c498b84 --- /dev/null +++ b/web/public/static/js/h265web/missile.js @@ -0,0 +1,7062 @@ +var ENVIRONMENT_IS_PTHREAD = true; +var Module = typeof Module !== "undefined" ? Module : {}; +var moduleOverrides = {}; +var key; +for (key in Module) { + if (Module.hasOwnProperty(key)) { + moduleOverrides[key] = Module[key] + } +} +var arguments_ = []; +var thisProgram = "./this.program"; +var quit_ = function(status, toThrow) { + throw toThrow +}; +var ENVIRONMENT_IS_WEB = false; +var ENVIRONMENT_IS_WORKER = false; +var ENVIRONMENT_IS_NODE = false; +var ENVIRONMENT_HAS_NODE = false; +var ENVIRONMENT_IS_SHELL = false; +ENVIRONMENT_IS_WEB = typeof window === "object"; +ENVIRONMENT_IS_WORKER = typeof importScripts === "function"; +ENVIRONMENT_HAS_NODE = typeof process === "object" && typeof process.versions === "object" && typeof process.versions.node === "string"; +ENVIRONMENT_IS_NODE = ENVIRONMENT_HAS_NODE && !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_WORKER; +ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; +if (Module["ENVIRONMENT"]) { + throw new Error("Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -s ENVIRONMENT=web or -s ENVIRONMENT=node)") +} +var scriptDirectory = ""; + +function locateFile(path) { + if (Module["locateFile"]) { + return Module["locateFile"](path, scriptDirectory) + } + return scriptDirectory + path +} +var read_, readAsync, readBinary, setWindowTitle; +if (ENVIRONMENT_IS_NODE) { + scriptDirectory = __dirname + "/"; + var nodeFS; + var nodePath; + read_ = function shell_read(filename, binary) { + var ret; + if (!nodeFS) nodeFS = require("fs"); + if (!nodePath) nodePath = require("path"); + filename = nodePath["normalize"](filename); + ret = nodeFS["readFileSync"](filename); + return binary ? ret : ret.toString() + }; + readBinary = function readBinary(filename) { + var ret = read_(filename, true); + if (!ret.buffer) { + ret = new Uint8Array(ret) + } + assert(ret.buffer); + return ret + }; + if (process["argv"].length > 1) { + thisProgram = process["argv"][1].replace(/\\/g, "/") + } + arguments_ = process["argv"].slice(2); + if (typeof module !== "undefined") { + module["exports"] = Module + } + process["on"]("uncaughtException", function(ex) { + if (!(ex instanceof ExitStatus)) { + throw ex + } + }); + process["on"]("unhandledRejection", abort); + quit_ = function(status) { + process["exit"](status) + }; + Module["inspect"] = function() { + return "[Emscripten Module object]" + } +} else if (ENVIRONMENT_IS_SHELL) { + if (typeof read != "undefined") { + read_ = function shell_read(f) { + return read(f) + } + } + readBinary = function readBinary(f) { + var data; + if (typeof readbuffer === "function") { + return new Uint8Array(readbuffer(f)) + } + data = read(f, "binary"); + assert(typeof data === "object"); + return data + }; + if (typeof scriptArgs != "undefined") { + arguments_ = scriptArgs + } else if (typeof arguments != "undefined") { + arguments_ = arguments + } + if (typeof quit === "function") { + quit_ = function(status) { + quit(status) + } + } + if (typeof print !== "undefined") { + if (typeof console === "undefined") console = {}; + console.log = print; + console.warn = console.error = typeof printErr !== "undefined" ? printErr : print + } +} else if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { + if (ENVIRONMENT_IS_WORKER) { + scriptDirectory = self.location.href + } else if (document.currentScript) { + scriptDirectory = document.currentScript.src + } + if (scriptDirectory.indexOf("blob:") !== 0) { + scriptDirectory = scriptDirectory.substr(0, scriptDirectory.lastIndexOf("/") + 1) + } else { + scriptDirectory = "" + } + read_ = function shell_read(url) { + var xhr = new XMLHttpRequest; + xhr.open("GET", url, false); + xhr.send(null); + return xhr.responseText + }; + if (ENVIRONMENT_IS_WORKER) { + readBinary = function readBinary(url) { + var xhr = new XMLHttpRequest; + xhr.open("GET", url, false); + xhr.responseType = "arraybuffer"; + xhr.send(null); + return new Uint8Array(xhr.response) + } + } + readAsync = function readAsync(url, onload, onerror) { + var xhr = new XMLHttpRequest; + xhr.open("GET", url, true); + xhr.responseType = "arraybuffer"; + xhr.onload = function xhr_onload() { + if (xhr.status == 200 || xhr.status == 0 && xhr.response) { + onload(xhr.response); + return + } + onerror() + }; + xhr.onerror = onerror; + xhr.send(null) + }; + setWindowTitle = function(title) { + document.title = title + } +} else { + throw new Error("environment detection error") +} +var out = Module["print"] || console.log.bind(console); +var err = Module["printErr"] || console.warn.bind(console); +for (key in moduleOverrides) { + if (moduleOverrides.hasOwnProperty(key)) { + Module[key] = moduleOverrides[key] + } +} +moduleOverrides = null; +if (Module["arguments"]) arguments_ = Module["arguments"]; +if (!Object.getOwnPropertyDescriptor(Module, "arguments")) Object.defineProperty(Module, "arguments", { + configurable: true, + get: function() { + abort("Module.arguments has been replaced with plain arguments_") + } +}); +if (Module["thisProgram"]) thisProgram = Module["thisProgram"]; +if (!Object.getOwnPropertyDescriptor(Module, "thisProgram")) Object.defineProperty(Module, "thisProgram", { + configurable: true, + get: function() { + abort("Module.thisProgram has been replaced with plain thisProgram") + } +}); +if (Module["quit"]) quit_ = Module["quit"]; +if (!Object.getOwnPropertyDescriptor(Module, "quit")) Object.defineProperty(Module, "quit", { + configurable: true, + get: function() { + abort("Module.quit has been replaced with plain quit_") + } +}); +assert(typeof Module["memoryInitializerPrefixURL"] === "undefined", "Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead"); +assert(typeof Module["pthreadMainPrefixURL"] === "undefined", "Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead"); +assert(typeof Module["cdInitializerPrefixURL"] === "undefined", "Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead"); +assert(typeof Module["filePackagePrefixURL"] === "undefined", "Module.filePackagePrefixURL option was removed, use Module.locateFile instead"); +assert(typeof Module["read"] === "undefined", "Module.read option was removed (modify read_ in JS)"); +assert(typeof Module["readAsync"] === "undefined", "Module.readAsync option was removed (modify readAsync in JS)"); +assert(typeof Module["readBinary"] === "undefined", "Module.readBinary option was removed (modify readBinary in JS)"); +assert(typeof Module["setWindowTitle"] === "undefined", "Module.setWindowTitle option was removed (modify setWindowTitle in JS)"); +if (!Object.getOwnPropertyDescriptor(Module, "read")) Object.defineProperty(Module, "read", { + configurable: true, + get: function() { + abort("Module.read has been replaced with plain read_") + } +}); +if (!Object.getOwnPropertyDescriptor(Module, "readAsync")) Object.defineProperty(Module, "readAsync", { + configurable: true, + get: function() { + abort("Module.readAsync has been replaced with plain readAsync") + } +}); +if (!Object.getOwnPropertyDescriptor(Module, "readBinary")) Object.defineProperty(Module, "readBinary", { + configurable: true, + get: function() { + abort("Module.readBinary has been replaced with plain readBinary") + } +}); +stackSave = stackRestore = stackAlloc = function() { + abort("cannot use the stack before compiled code is ready to run, and has provided stack access") +}; + +function dynamicAlloc(size) { + assert(DYNAMICTOP_PTR); + var ret = HEAP32[DYNAMICTOP_PTR >> 2]; + var end = ret + size + 15 & -16; + if (end > _emscripten_get_heap_size()) { + abort("failure to dynamicAlloc - memory growth etc. is not supported there, call malloc/sbrk directly") + } + HEAP32[DYNAMICTOP_PTR >> 2] = end; + return ret +} + +function getNativeTypeSize(type) { + switch (type) { + case "i1": + case "i8": + return 1; + case "i16": + return 2; + case "i32": + return 4; + case "i64": + return 8; + case "float": + return 4; + case "double": + return 8; + default: { + if (type[type.length - 1] === "*") { + return 4 + } else if (type[0] === "i") { + var bits = parseInt(type.substr(1)); + assert(bits % 8 === 0, "getNativeTypeSize invalid bits " + bits + ", type " + type); + return bits / 8 + } else { + return 0 + } + } + } +} + +function warnOnce(text) { + if (!warnOnce.shown) warnOnce.shown = {}; + if (!warnOnce.shown[text]) { + warnOnce.shown[text] = 1; + err(text) + } +} +var asm2wasmImports = { + "f64-rem": function(x, y) { + return x % y + }, + "debugger": function() { + debugger + } +}; +var jsCallStartIndex = 1; +var functionPointers = new Array(35); + +function addFunction(func, sig) { + assert(typeof func !== "undefined"); + var base = 0; + for (var i = base; i < base + 35; i++) { + if (!functionPointers[i]) { + functionPointers[i] = func; + return jsCallStartIndex + i + } + } + throw "Finished up all reserved function pointers. Use a higher value for RESERVED_FUNCTION_POINTERS." +} + +function removeFunction(index) { + functionPointers[index - jsCallStartIndex] = null +} +var tempRet0 = 0; +var getTempRet0 = function() { + return tempRet0 +}; +var wasmBinary; +if (Module["wasmBinary"]) wasmBinary = Module["wasmBinary"]; +if (!Object.getOwnPropertyDescriptor(Module, "wasmBinary")) Object.defineProperty(Module, "wasmBinary", { + configurable: true, + get: function() { + abort("Module.wasmBinary has been replaced with plain wasmBinary") + } +}); +var noExitRuntime; +if (Module["noExitRuntime"]) noExitRuntime = Module["noExitRuntime"]; +if (!Object.getOwnPropertyDescriptor(Module, "noExitRuntime")) Object.defineProperty(Module, "noExitRuntime", { + configurable: true, + get: function() { + abort("Module.noExitRuntime has been replaced with plain noExitRuntime") + } +}); +if (typeof WebAssembly !== "object") { + abort("No WebAssembly support found. Build with -s WASM=0 to target JavaScript instead.") +} + +function setValue(ptr, value, type, noSafe) { + type = type || "i8"; + if (type.charAt(type.length - 1) === "*") type = "i32"; + switch (type) { + case "i1": + HEAP8[ptr >> 0] = value; + break; + case "i8": + HEAP8[ptr >> 0] = value; + break; + case "i16": + HEAP16[ptr >> 1] = value; + break; + case "i32": + HEAP32[ptr >> 2] = value; + break; + case "i64": + tempI64 = [value >>> 0, (tempDouble = value, +Math_abs(tempDouble) >= 1 ? tempDouble > 0 ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 : ~~+Math_ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0)], HEAP32[ptr >> 2] = tempI64[0], HEAP32[ptr + 4 >> 2] = tempI64[1]; + break; + case "float": + HEAPF32[ptr >> 2] = value; + break; + case "double": + HEAPF64[ptr >> 3] = value; + break; + default: + abort("invalid type for setValue: " + type) + } +} +var wasmMemory; +var wasmTable = new WebAssembly.Table({ + "initial": 4928, + "element": "anyfunc" +}); +var ABORT = false; +var EXITSTATUS = 0; + +function assert(condition, text) { + if (!condition) { + abort("Assertion failed: " + text) + } +} + +function getCFunc(ident) { + var func = Module["_" + ident]; + assert(func, "Cannot call unknown function " + ident + ", make sure it is exported"); + return func +} + +function ccall(ident, returnType, argTypes, args, opts) { + var toC = { + "string": function(str) { + var ret = 0; + if (str !== null && str !== undefined && str !== 0) { + var len = (str.length << 2) + 1; + ret = stackAlloc(len); + stringToUTF8(str, ret, len) + } + return ret + }, + "array": function(arr) { + var ret = stackAlloc(arr.length); + writeArrayToMemory(arr, ret); + return ret + } + }; + + function convertReturnValue(ret) { + if (returnType === "string") return UTF8ToString(ret); + if (returnType === "boolean") return Boolean(ret); + return ret + } + var func = getCFunc(ident); + var cArgs = []; + var stack = 0; + assert(returnType !== "array", 'Return type should not be "array".'); + if (args) { + for (var i = 0; i < args.length; i++) { + var converter = toC[argTypes[i]]; + if (converter) { + if (stack === 0) stack = stackSave(); + cArgs[i] = converter(args[i]) + } else { + cArgs[i] = args[i] + } + } + } + var ret = func.apply(null, cArgs); + ret = convertReturnValue(ret); + if (stack !== 0) stackRestore(stack); + return ret +} + +function cwrap(ident, returnType, argTypes, opts) { + return function() { + return ccall(ident, returnType, argTypes, arguments, opts) + } +} +var ALLOC_NORMAL = 0; +var ALLOC_NONE = 3; + +function allocate(slab, types, allocator, ptr) { + var zeroinit, size; + if (typeof slab === "number") { + zeroinit = true; + size = slab + } else { + zeroinit = false; + size = slab.length + } + var singleType = typeof types === "string" ? types : null; + var ret; + if (allocator == ALLOC_NONE) { + ret = ptr + } else { + ret = [_malloc, stackAlloc, dynamicAlloc][allocator](Math.max(size, singleType ? 1 : types.length)) + } + if (zeroinit) { + var stop; + ptr = ret; + assert((ret & 3) == 0); + stop = ret + (size & ~3); + for (; ptr < stop; ptr += 4) { + HEAP32[ptr >> 2] = 0 + } + stop = ret + size; + while (ptr < stop) { + HEAP8[ptr++ >> 0] = 0 + } + return ret + } + if (singleType === "i8") { + if (slab.subarray || slab.slice) { + HEAPU8.set(slab, ret) + } else { + HEAPU8.set(new Uint8Array(slab), ret) + } + return ret + } + var i = 0, + type, typeSize, previousType; + while (i < size) { + var curr = slab[i]; + type = singleType || types[i]; + if (type === 0) { + i++; + continue + } + assert(type, "Must know what type to store in allocate!"); + if (type == "i64") type = "i32"; + setValue(ret + i, curr, type); + if (previousType !== type) { + typeSize = getNativeTypeSize(type); + previousType = type + } + i += typeSize + } + return ret +} + +function getMemory(size) { + if (!runtimeInitialized) return dynamicAlloc(size); + return _malloc(size) +} +var UTF8Decoder = typeof TextDecoder !== "undefined" ? new TextDecoder("utf8") : undefined; + +function UTF8ArrayToString(u8Array, idx, maxBytesToRead) { + var endIdx = idx + maxBytesToRead; + var endPtr = idx; + while (u8Array[endPtr] && !(endPtr >= endIdx)) ++endPtr; + if (endPtr - idx > 16 && u8Array.subarray && UTF8Decoder) { + return UTF8Decoder.decode(u8Array.subarray(idx, endPtr)) + } else { + var str = ""; + while (idx < endPtr) { + var u0 = u8Array[idx++]; + if (!(u0 & 128)) { + str += String.fromCharCode(u0); + continue + } + var u1 = u8Array[idx++] & 63; + if ((u0 & 224) == 192) { + str += String.fromCharCode((u0 & 31) << 6 | u1); + continue + } + var u2 = u8Array[idx++] & 63; + if ((u0 & 240) == 224) { + u0 = (u0 & 15) << 12 | u1 << 6 | u2 + } else { + if ((u0 & 248) != 240) warnOnce("Invalid UTF-8 leading byte 0x" + u0.toString(16) + " encountered when deserializing a UTF-8 string on the asm.js/wasm heap to a JS string!"); + u0 = (u0 & 7) << 18 | u1 << 12 | u2 << 6 | u8Array[idx++] & 63 + } + if (u0 < 65536) { + str += String.fromCharCode(u0) + } else { + var ch = u0 - 65536; + str += String.fromCharCode(55296 | ch >> 10, 56320 | ch & 1023) + } + } + } + return str +} + +function UTF8ToString(ptr, maxBytesToRead) { + return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : "" +} + +function stringToUTF8Array(str, outU8Array, outIdx, maxBytesToWrite) { + if (!(maxBytesToWrite > 0)) return 0; + var startIdx = outIdx; + var endIdx = outIdx + maxBytesToWrite - 1; + for (var i = 0; i < str.length; ++i) { + var u = str.charCodeAt(i); + if (u >= 55296 && u <= 57343) { + var u1 = str.charCodeAt(++i); + u = 65536 + ((u & 1023) << 10) | u1 & 1023 + } + if (u <= 127) { + if (outIdx >= endIdx) break; + outU8Array[outIdx++] = u + } else if (u <= 2047) { + if (outIdx + 1 >= endIdx) break; + outU8Array[outIdx++] = 192 | u >> 6; + outU8Array[outIdx++] = 128 | u & 63 + } else if (u <= 65535) { + if (outIdx + 2 >= endIdx) break; + outU8Array[outIdx++] = 224 | u >> 12; + outU8Array[outIdx++] = 128 | u >> 6 & 63; + outU8Array[outIdx++] = 128 | u & 63 + } else { + if (outIdx + 3 >= endIdx) break; + if (u >= 2097152) warnOnce("Invalid Unicode code point 0x" + u.toString(16) + " encountered when serializing a JS string to an UTF-8 string on the asm.js/wasm heap! (Valid unicode code points should be in range 0-0x1FFFFF)."); + outU8Array[outIdx++] = 240 | u >> 18; + outU8Array[outIdx++] = 128 | u >> 12 & 63; + outU8Array[outIdx++] = 128 | u >> 6 & 63; + outU8Array[outIdx++] = 128 | u & 63 + } + } + outU8Array[outIdx] = 0; + return outIdx - startIdx +} + +function stringToUTF8(str, outPtr, maxBytesToWrite) { + assert(typeof maxBytesToWrite == "number", "stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!"); + return stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite) +} + +function lengthBytesUTF8(str) { + var len = 0; + for (var i = 0; i < str.length; ++i) { + var u = str.charCodeAt(i); + if (u >= 55296 && u <= 57343) u = 65536 + ((u & 1023) << 10) | str.charCodeAt(++i) & 1023; + if (u <= 127) ++len; + else if (u <= 2047) len += 2; + else if (u <= 65535) len += 3; + else len += 4 + } + return len +} +var UTF16Decoder = typeof TextDecoder !== "undefined" ? new TextDecoder("utf-16le") : undefined; + +function allocateUTF8(str) { + var size = lengthBytesUTF8(str) + 1; + var ret = _malloc(size); + if (ret) stringToUTF8Array(str, HEAP8, ret, size); + return ret +} + +function allocateUTF8OnStack(str) { + var size = lengthBytesUTF8(str) + 1; + var ret = stackAlloc(size); + stringToUTF8Array(str, HEAP8, ret, size); + return ret +} + +function writeArrayToMemory(array, buffer) { + assert(array.length >= 0, "writeArrayToMemory array must have a length (should be an array or typed array)"); + HEAP8.set(array, buffer) +} + +function writeAsciiToMemory(str, buffer, dontAddNull) { + for (var i = 0; i < str.length; ++i) { + assert(str.charCodeAt(i) === str.charCodeAt(i) & 255); + HEAP8[buffer++ >> 0] = str.charCodeAt(i) + } + if (!dontAddNull) HEAP8[buffer >> 0] = 0 +} +var PAGE_SIZE = 16384; +var WASM_PAGE_SIZE = 65536; +var buffer, HEAP8, HEAPU8, HEAP16, HEAPU16, HEAP32, HEAPU32, HEAPF32, HEAPF64; + +function updateGlobalBufferAndViews(buf) { + buffer = buf; + Module["HEAP8"] = HEAP8 = new Int8Array(buf); + Module["HEAP16"] = HEAP16 = new Int16Array(buf); + Module["HEAP32"] = HEAP32 = new Int32Array(buf); + Module["HEAPU8"] = HEAPU8 = new Uint8Array(buf); + Module["HEAPU16"] = HEAPU16 = new Uint16Array(buf); + Module["HEAPU32"] = HEAPU32 = new Uint32Array(buf); + Module["HEAPF32"] = HEAPF32 = new Float32Array(buf); + Module["HEAPF64"] = HEAPF64 = new Float64Array(buf) +} +var STACK_BASE = 1398224, + STACK_MAX = 6641104, + DYNAMIC_BASE = 6641104, + DYNAMICTOP_PTR = 1398e3; +assert(STACK_BASE % 16 === 0, "stack must start aligned"); +assert(DYNAMIC_BASE % 16 === 0, "heap must start aligned"); +var TOTAL_STACK = 5242880; +if (Module["TOTAL_STACK"]) assert(TOTAL_STACK === Module["TOTAL_STACK"], "the stack size can no longer be determined at runtime"); +var INITIAL_TOTAL_MEMORY = Module["TOTAL_MEMORY"] || 2147483648; +if (!Object.getOwnPropertyDescriptor(Module, "TOTAL_MEMORY")) Object.defineProperty(Module, "TOTAL_MEMORY", { + configurable: true, + get: function() { + abort("Module.TOTAL_MEMORY has been replaced with plain INITIAL_TOTAL_MEMORY") + } +}); +assert(INITIAL_TOTAL_MEMORY >= TOTAL_STACK, "TOTAL_MEMORY should be larger than TOTAL_STACK, was " + INITIAL_TOTAL_MEMORY + "! (TOTAL_STACK=" + TOTAL_STACK + ")"); +assert(typeof Int32Array !== "undefined" && typeof Float64Array !== "undefined" && Int32Array.prototype.subarray !== undefined && Int32Array.prototype.set !== undefined, "JS engine does not provide full typed array support"); +if (Module["wasmMemory"]) { + wasmMemory = Module["wasmMemory"] +} else { + wasmMemory = new WebAssembly.Memory({ + "initial": INITIAL_TOTAL_MEMORY / WASM_PAGE_SIZE, + "maximum": INITIAL_TOTAL_MEMORY / WASM_PAGE_SIZE + }) +} +if (wasmMemory) { + buffer = wasmMemory.buffer +} +INITIAL_TOTAL_MEMORY = buffer.byteLength; +assert(INITIAL_TOTAL_MEMORY % WASM_PAGE_SIZE === 0); +updateGlobalBufferAndViews(buffer); +HEAP32[DYNAMICTOP_PTR >> 2] = DYNAMIC_BASE; + +function writeStackCookie() { + assert((STACK_MAX & 3) == 0); + HEAPU32[(STACK_MAX >> 2) - 1] = 34821223; + HEAPU32[(STACK_MAX >> 2) - 2] = 2310721022; + HEAP32[0] = 1668509029 +} + +function checkStackCookie() { + var cookie1 = HEAPU32[(STACK_MAX >> 2) - 1]; + var cookie2 = HEAPU32[(STACK_MAX >> 2) - 2]; + if (cookie1 != 34821223 || cookie2 != 2310721022) { + abort("Stack overflow! Stack cookie has been overwritten, expected hex dwords 0x89BACDFE and 0x02135467, but received 0x" + cookie2.toString(16) + " " + cookie1.toString(16)) + } + if (HEAP32[0] !== 1668509029) abort("Runtime error: The application has corrupted its heap memory area (address zero)!") +} + +function abortStackOverflow(allocSize) { + abort("Stack overflow! Attempted to allocate " + allocSize + " bytes on the stack, but stack has only " + (STACK_MAX - stackSave() + allocSize) + " bytes available!") +}(function() { + var h16 = new Int16Array(1); + var h8 = new Int8Array(h16.buffer); + h16[0] = 25459; + if (h8[0] !== 115 || h8[1] !== 99) throw "Runtime error: expected the system to be little-endian!" +})(); + +function abortFnPtrError(ptr, sig) { + var possibleSig = ""; + for (var x in debug_tables) { + var tbl = debug_tables[x]; + if (tbl[ptr]) { + possibleSig += 'as sig "' + x + '" pointing to function ' + tbl[ptr] + ", " + } + } + abort("Invalid function pointer " + ptr + " called with signature '" + sig + "'. Perhaps this is an invalid value (e.g. caused by calling a virtual method on a NULL pointer)? Or calling a function with an incorrect type, which will fail? (it is worth building your source files with -Werror (warnings are errors), as warnings can indicate undefined behavior which can cause this). This pointer might make sense in another type signature: " + possibleSig) +} + +function callRuntimeCallbacks(callbacks) { + while (callbacks.length > 0) { + var callback = callbacks.shift(); + if (typeof callback == "function") { + callback(); + continue + } + var func = callback.func; + if (typeof func === "number") { + if (callback.arg === undefined) { + Module["dynCall_v"](func) + } else { + Module["dynCall_vi"](func, callback.arg) + } + } else { + func(callback.arg === undefined ? null : callback.arg) + } + } +} +var __ATPRERUN__ = []; +var __ATINIT__ = []; +var __ATMAIN__ = []; +var __ATPOSTRUN__ = []; +var runtimeInitialized = false; +var runtimeExited = false; + +function preRun() { + if (Module["preRun"]) { + if (typeof Module["preRun"] == "function") Module["preRun"] = [Module["preRun"]]; + while (Module["preRun"].length) { + addOnPreRun(Module["preRun"].shift()) + } + } + callRuntimeCallbacks(__ATPRERUN__) +} + +function initRuntime() { + checkStackCookie(); + assert(!runtimeInitialized); + runtimeInitialized = true; + if (!Module["noFSInit"] && !FS.init.initialized) FS.init(); + TTY.init(); + callRuntimeCallbacks(__ATINIT__) +} + +function preMain() { + checkStackCookie(); + FS.ignorePermissions = false; + callRuntimeCallbacks(__ATMAIN__) +} + +function exitRuntime() { + checkStackCookie(); + runtimeExited = true +} + +function postRun() { + checkStackCookie(); + if (Module["postRun"]) { + if (typeof Module["postRun"] == "function") Module["postRun"] = [Module["postRun"]]; + while (Module["postRun"].length) { + addOnPostRun(Module["postRun"].shift()) + } + } + callRuntimeCallbacks(__ATPOSTRUN__) +} + +function addOnPreRun(cb) { + __ATPRERUN__.unshift(cb) +} + +function addOnPostRun(cb) { + __ATPOSTRUN__.unshift(cb) +} +assert(Math.imul, "This browser does not support Math.imul(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill"); +assert(Math.fround, "This browser does not support Math.fround(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill"); +assert(Math.clz32, "This browser does not support Math.clz32(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill"); +assert(Math.trunc, "This browser does not support Math.trunc(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill"); +var Math_abs = Math.abs; +var Math_ceil = Math.ceil; +var Math_floor = Math.floor; +var Math_min = Math.min; +var Math_trunc = Math.trunc; +var runDependencies = 0; +var runDependencyWatcher = null; +var dependenciesFulfilled = null; +var runDependencyTracking = {}; + +function getUniqueRunDependency(id) { + var orig = id; + while (1) { + if (!runDependencyTracking[id]) return id; + id = orig + Math.random() + } + return id +} + +function addRunDependency(id) { + runDependencies++; + if (Module["monitorRunDependencies"]) { + Module["monitorRunDependencies"](runDependencies) + } + if (id) { + assert(!runDependencyTracking[id]); + runDependencyTracking[id] = 1; + if (runDependencyWatcher === null && typeof setInterval !== "undefined") { + runDependencyWatcher = setInterval(function() { + if (ABORT) { + clearInterval(runDependencyWatcher); + runDependencyWatcher = null; + return + } + var shown = false; + for (var dep in runDependencyTracking) { + if (!shown) { + shown = true; + err("still waiting on run dependencies:") + } + err("dependency: " + dep) + } + if (shown) { + err("(end of list)") + } + }, 1e4) + } + } else { + err("warning: run dependency added without ID") + } +} + +function removeRunDependency(id) { + runDependencies--; + if (Module["monitorRunDependencies"]) { + Module["monitorRunDependencies"](runDependencies) + } + if (id) { + assert(runDependencyTracking[id]); + delete runDependencyTracking[id] + } else { + err("warning: run dependency removed without ID") + } + if (runDependencies == 0) { + if (runDependencyWatcher !== null) { + clearInterval(runDependencyWatcher); + runDependencyWatcher = null + } + if (dependenciesFulfilled) { + var callback = dependenciesFulfilled; + dependenciesFulfilled = null; + callback() + } + } +} +Module["preloadedImages"] = {}; +Module["preloadedAudios"] = {}; + +function abort(what) { + if (Module["onAbort"]) { + Module["onAbort"](what) + } + what += ""; + out(what); + err(what); + ABORT = true; + EXITSTATUS = 1; + var extra = ""; + var output = "abort(" + what + ") at " + stackTrace() + extra; + throw output +} +var dataURIPrefix = "data:application/octet-stream;base64,"; + +function isDataURI(filename) { + return String.prototype.startsWith ? filename.startsWith(dataURIPrefix) : filename.indexOf(dataURIPrefix) === 0 +} +var wasmBinaryFile = "missile-v20221120.wasm"; +if (!isDataURI(wasmBinaryFile)) { + wasmBinaryFile = locateFile(wasmBinaryFile) +} + +function getBinary() { + try { + if (wasmBinary) { + return new Uint8Array(wasmBinary) + } + if (readBinary) { + return readBinary(wasmBinaryFile) + } else { + throw "both async and sync fetching of the wasm failed" + } + } catch (err) { + abort(err) + } +} + +function getBinaryPromise() { + if (!wasmBinary && (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) && typeof fetch === "function") { + return fetch(wasmBinaryFile, { + credentials: "same-origin" + }).then(function(response) { + if (!response["ok"]) { + throw "failed to load wasm binary file at '" + wasmBinaryFile + "'" + } + return response["arrayBuffer"]() + }).catch(function() { + return getBinary() + }) + } + return new Promise(function(resolve, reject) { + resolve(getBinary()) + }) +} + +function createWasm() { + var info = { + "env": asmLibraryArg, + "wasi_unstable": asmLibraryArg, + "global": { + "NaN": NaN, + Infinity: Infinity + }, + "global.Math": Math, + "asm2wasm": asm2wasmImports + }; + + function receiveInstance(instance, module) { + var exports = instance.exports; + Module["asm"] = exports; + removeRunDependency("wasm-instantiate") + } + addRunDependency("wasm-instantiate"); + var trueModule = Module; + + function receiveInstantiatedSource(output) { + assert(Module === trueModule, "the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?"); + trueModule = null; + receiveInstance(output["instance"]) + } + + function instantiateArrayBuffer(receiver) { + return getBinaryPromise().then(function(binary) { + return WebAssembly.instantiate(binary, info) + }).then(receiver, function(reason) { + err("failed to asynchronously prepare wasm: " + reason); + abort(reason) + }) + } + + function instantiateAsync() { + if (!wasmBinary && typeof WebAssembly.instantiateStreaming === "function" && !isDataURI(wasmBinaryFile) && typeof fetch === "function") { + fetch(wasmBinaryFile, { + credentials: "same-origin" + }).then(function(response) { + var result = WebAssembly.instantiateStreaming(response, info); + return result.then(receiveInstantiatedSource, function(reason) { + err("wasm streaming compile failed: " + reason); + err("falling back to ArrayBuffer instantiation"); + instantiateArrayBuffer(receiveInstantiatedSource) + }) + }) + } else { + return instantiateArrayBuffer(receiveInstantiatedSource) + } + } + if (Module["instantiateWasm"]) { + try { + var exports = Module["instantiateWasm"](info, receiveInstance); + return exports + } catch (e) { + err("Module.instantiateWasm callback failed with error: " + e); + return false + } + } + instantiateAsync(); + return {} +} +Module["asm"] = createWasm; +var tempDouble; +var tempI64; +var ASM_CONSTS = [function() { + if (typeof window != "undefined") { + window.dispatchEvent(new CustomEvent("wasmLoaded")) + } else {} +}]; + +function _emscripten_asm_const_i(code) { + return ASM_CONSTS[code]() +} +__ATINIT__.push({ + func: function() { + ___emscripten_environ_constructor() + } +}); +var tempDoublePtr = 1398208; +assert(tempDoublePtr % 8 == 0); + +function demangle(func) { + warnOnce("warning: build with -s DEMANGLE_SUPPORT=1 to link in libcxxabi demangling"); + return func +} + +function demangleAll(text) { + var regex = /\b__Z[\w\d_]+/g; + return text.replace(regex, function(x) { + var y = demangle(x); + return x === y ? x : y + " [" + x + "]" + }) +} + +function jsStackTrace() { + var err = new Error; + if (!err.stack) { + try { + throw new Error(0) + } catch (e) { + err = e + } + if (!err.stack) { + return "(no stack trace available)" + } + } + return err.stack.toString() +} + +function stackTrace() { + var js = jsStackTrace(); + if (Module["extraStackTrace"]) js += "\n" + Module["extraStackTrace"](); + return demangleAll(js) +} +var ENV = {}; + +function ___buildEnvironment(environ) { + var MAX_ENV_VALUES = 64; + var TOTAL_ENV_SIZE = 1024; + var poolPtr; + var envPtr; + if (!___buildEnvironment.called) { + ___buildEnvironment.called = true; + ENV["USER"] = "web_user"; + ENV["LOGNAME"] = "web_user"; + ENV["PATH"] = "/"; + ENV["PWD"] = "/"; + ENV["HOME"] = "/home/web_user"; + ENV["LANG"] = (typeof navigator === "object" && navigator.languages && navigator.languages[0] || "C").replace("-", "_") + ".UTF-8"; + ENV["_"] = thisProgram; + poolPtr = getMemory(TOTAL_ENV_SIZE); + envPtr = getMemory(MAX_ENV_VALUES * 4); + HEAP32[envPtr >> 2] = poolPtr; + HEAP32[environ >> 2] = envPtr + } else { + envPtr = HEAP32[environ >> 2]; + poolPtr = HEAP32[envPtr >> 2] + } + var strings = []; + var totalSize = 0; + for (var key in ENV) { + if (typeof ENV[key] === "string") { + var line = key + "=" + ENV[key]; + strings.push(line); + totalSize += line.length + } + } + if (totalSize > TOTAL_ENV_SIZE) { + throw new Error("Environment size exceeded TOTAL_ENV_SIZE!") + } + var ptrSize = 4; + for (var i = 0; i < strings.length; i++) { + var line = strings[i]; + writeAsciiToMemory(line, poolPtr); + HEAP32[envPtr + i * ptrSize >> 2] = poolPtr; + poolPtr += line.length + 1 + } + HEAP32[envPtr + strings.length * ptrSize >> 2] = 0 +} + +function ___lock() {} + +function ___setErrNo(value) { + if (Module["___errno_location"]) HEAP32[Module["___errno_location"]() >> 2] = value; + else err("failed to set errno from JS"); + return value +} +var PATH = { + splitPath: function(filename) { + var splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; + return splitPathRe.exec(filename).slice(1) + }, + normalizeArray: function(parts, allowAboveRoot) { + var up = 0; + for (var i = parts.length - 1; i >= 0; i--) { + var last = parts[i]; + if (last === ".") { + parts.splice(i, 1) + } else if (last === "..") { + parts.splice(i, 1); + up++ + } else if (up) { + parts.splice(i, 1); + up-- + } + } + if (allowAboveRoot) { + for (; up; up--) { + parts.unshift("..") + } + } + return parts + }, + normalize: function(path) { + var isAbsolute = path.charAt(0) === "/", + trailingSlash = path.substr(-1) === "/"; + path = PATH.normalizeArray(path.split("/").filter(function(p) { + return !!p + }), !isAbsolute).join("/"); + if (!path && !isAbsolute) { + path = "." + } + if (path && trailingSlash) { + path += "/" + } + return (isAbsolute ? "/" : "") + path + }, + dirname: function(path) { + var result = PATH.splitPath(path), + root = result[0], + dir = result[1]; + if (!root && !dir) { + return "." + } + if (dir) { + dir = dir.substr(0, dir.length - 1) + } + return root + dir + }, + basename: function(path) { + if (path === "/") return "/"; + var lastSlash = path.lastIndexOf("/"); + if (lastSlash === -1) return path; + return path.substr(lastSlash + 1) + }, + extname: function(path) { + return PATH.splitPath(path)[3] + }, + join: function() { + var paths = Array.prototype.slice.call(arguments, 0); + return PATH.normalize(paths.join("/")) + }, + join2: function(l, r) { + return PATH.normalize(l + "/" + r) + } +}; +var PATH_FS = { + resolve: function() { + var resolvedPath = "", + resolvedAbsolute = false; + for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path = i >= 0 ? arguments[i] : FS.cwd(); + if (typeof path !== "string") { + throw new TypeError("Arguments to path.resolve must be strings") + } else if (!path) { + return "" + } + resolvedPath = path + "/" + resolvedPath; + resolvedAbsolute = path.charAt(0) === "/" + } + resolvedPath = PATH.normalizeArray(resolvedPath.split("/").filter(function(p) { + return !!p + }), !resolvedAbsolute).join("/"); + return (resolvedAbsolute ? "/" : "") + resolvedPath || "." + }, + relative: function(from, to) { + from = PATH_FS.resolve(from).substr(1); + to = PATH_FS.resolve(to).substr(1); + + function trim(arr) { + var start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== "") break + } + var end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== "") break + } + if (start > end) return []; + return arr.slice(start, end - start + 1) + } + var fromParts = trim(from.split("/")); + var toParts = trim(to.split("/")); + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break + } + } + var outputParts = []; + for (var i = samePartsLength; i < fromParts.length; i++) { + outputParts.push("..") + } + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + return outputParts.join("/") + } +}; +var TTY = { + ttys: [], + init: function() {}, + shutdown: function() {}, + register: function(dev, ops) { + TTY.ttys[dev] = { + input: [], + output: [], + ops: ops + }; + FS.registerDevice(dev, TTY.stream_ops) + }, + stream_ops: { + open: function(stream) { + var tty = TTY.ttys[stream.node.rdev]; + if (!tty) { + throw new FS.ErrnoError(43) + } + stream.tty = tty; + stream.seekable = false + }, + close: function(stream) { + stream.tty.ops.flush(stream.tty) + }, + flush: function(stream) { + stream.tty.ops.flush(stream.tty) + }, + read: function(stream, buffer, offset, length, pos) { + if (!stream.tty || !stream.tty.ops.get_char) { + throw new FS.ErrnoError(60) + } + var bytesRead = 0; + for (var i = 0; i < length; i++) { + var result; + try { + result = stream.tty.ops.get_char(stream.tty) + } catch (e) { + throw new FS.ErrnoError(29) + } + if (result === undefined && bytesRead === 0) { + throw new FS.ErrnoError(6) + } + if (result === null || result === undefined) break; + bytesRead++; + buffer[offset + i] = result + } + if (bytesRead) { + stream.node.timestamp = Date.now() + } + return bytesRead + }, + write: function(stream, buffer, offset, length, pos) { + if (!stream.tty || !stream.tty.ops.put_char) { + throw new FS.ErrnoError(60) + } + try { + for (var i = 0; i < length; i++) { + stream.tty.ops.put_char(stream.tty, buffer[offset + i]) + } + } catch (e) { + throw new FS.ErrnoError(29) + } + if (length) { + stream.node.timestamp = Date.now() + } + return i + } + }, + default_tty_ops: { + get_char: function(tty) { + if (!tty.input.length) { + var result = null; + if (ENVIRONMENT_IS_NODE) { + var BUFSIZE = 256; + var buf = Buffer.alloc ? Buffer.alloc(BUFSIZE) : new Buffer(BUFSIZE); + var bytesRead = 0; + try { + bytesRead = fs.readSync(process.stdin.fd, buf, 0, BUFSIZE, null) + } catch (e) { + if (e.toString().indexOf("EOF") != -1) bytesRead = 0; + else throw e + } + if (bytesRead > 0) { + result = buf.slice(0, bytesRead).toString("utf-8") + } else { + result = null + } + } else if (typeof window != "undefined" && typeof window.prompt == "function") { + result = window.prompt("Input: "); + if (result !== null) { + result += "\n" + } + } else if (typeof readline == "function") { + result = readline(); + if (result !== null) { + result += "\n" + } + } + if (!result) { + return null + } + tty.input = intArrayFromString(result, true) + } + return tty.input.shift() + }, + put_char: function(tty, val) { + if (val === null || val === 10) { + out(UTF8ArrayToString(tty.output, 0)); + tty.output = [] + } else { + if (val != 0) tty.output.push(val) + } + }, + flush: function(tty) { + if (tty.output && tty.output.length > 0) { + out(UTF8ArrayToString(tty.output, 0)); + tty.output = [] + } + } + }, + default_tty1_ops: { + put_char: function(tty, val) { + if (val === null || val === 10) { + err(UTF8ArrayToString(tty.output, 0)); + tty.output = [] + } else { + if (val != 0) tty.output.push(val) + } + }, + flush: function(tty) { + if (tty.output && tty.output.length > 0) { + err(UTF8ArrayToString(tty.output, 0)); + tty.output = [] + } + } + } +}; +var MEMFS = { + ops_table: null, + mount: function(mount) { + return MEMFS.createNode(null, "/", 16384 | 511, 0) + }, + createNode: function(parent, name, mode, dev) { + if (FS.isBlkdev(mode) || FS.isFIFO(mode)) { + throw new FS.ErrnoError(63) + } + if (!MEMFS.ops_table) { + MEMFS.ops_table = { + dir: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr, + lookup: MEMFS.node_ops.lookup, + mknod: MEMFS.node_ops.mknod, + rename: MEMFS.node_ops.rename, + unlink: MEMFS.node_ops.unlink, + rmdir: MEMFS.node_ops.rmdir, + readdir: MEMFS.node_ops.readdir, + symlink: MEMFS.node_ops.symlink + }, + stream: { + llseek: MEMFS.stream_ops.llseek + } + }, + file: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr + }, + stream: { + llseek: MEMFS.stream_ops.llseek, + read: MEMFS.stream_ops.read, + write: MEMFS.stream_ops.write, + allocate: MEMFS.stream_ops.allocate, + mmap: MEMFS.stream_ops.mmap, + msync: MEMFS.stream_ops.msync + } + }, + link: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr, + readlink: MEMFS.node_ops.readlink + }, + stream: {} + }, + chrdev: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr + }, + stream: FS.chrdev_stream_ops + } + } + } + var node = FS.createNode(parent, name, mode, dev); + if (FS.isDir(node.mode)) { + node.node_ops = MEMFS.ops_table.dir.node; + node.stream_ops = MEMFS.ops_table.dir.stream; + node.contents = {} + } else if (FS.isFile(node.mode)) { + node.node_ops = MEMFS.ops_table.file.node; + node.stream_ops = MEMFS.ops_table.file.stream; + node.usedBytes = 0; + node.contents = null + } else if (FS.isLink(node.mode)) { + node.node_ops = MEMFS.ops_table.link.node; + node.stream_ops = MEMFS.ops_table.link.stream + } else if (FS.isChrdev(node.mode)) { + node.node_ops = MEMFS.ops_table.chrdev.node; + node.stream_ops = MEMFS.ops_table.chrdev.stream + } + node.timestamp = Date.now(); + if (parent) { + parent.contents[name] = node + } + return node + }, + getFileDataAsRegularArray: function(node) { + if (node.contents && node.contents.subarray) { + var arr = []; + for (var i = 0; i < node.usedBytes; ++i) arr.push(node.contents[i]); + return arr + } + return node.contents + }, + getFileDataAsTypedArray: function(node) { + if (!node.contents) return new Uint8Array; + if (node.contents.subarray) return node.contents.subarray(0, node.usedBytes); + return new Uint8Array(node.contents) + }, + expandFileStorage: function(node, newCapacity) { + var prevCapacity = node.contents ? node.contents.length : 0; + if (prevCapacity >= newCapacity) return; + var CAPACITY_DOUBLING_MAX = 1024 * 1024; + newCapacity = Math.max(newCapacity, prevCapacity * (prevCapacity < CAPACITY_DOUBLING_MAX ? 2 : 1.125) | 0); + if (prevCapacity != 0) newCapacity = Math.max(newCapacity, 256); + var oldContents = node.contents; + node.contents = new Uint8Array(newCapacity); + if (node.usedBytes > 0) node.contents.set(oldContents.subarray(0, node.usedBytes), 0); + return + }, + resizeFileStorage: function(node, newSize) { + if (node.usedBytes == newSize) return; + if (newSize == 0) { + node.contents = null; + node.usedBytes = 0; + return + } + if (!node.contents || node.contents.subarray) { + var oldContents = node.contents; + node.contents = new Uint8Array(new ArrayBuffer(newSize)); + if (oldContents) { + node.contents.set(oldContents.subarray(0, Math.min(newSize, node.usedBytes))) + } + node.usedBytes = newSize; + return + } + if (!node.contents) node.contents = []; + if (node.contents.length > newSize) node.contents.length = newSize; + else + while (node.contents.length < newSize) node.contents.push(0); + node.usedBytes = newSize + }, + node_ops: { + getattr: function(node) { + var attr = {}; + attr.dev = FS.isChrdev(node.mode) ? node.id : 1; + attr.ino = node.id; + attr.mode = node.mode; + attr.nlink = 1; + attr.uid = 0; + attr.gid = 0; + attr.rdev = node.rdev; + if (FS.isDir(node.mode)) { + attr.size = 4096 + } else if (FS.isFile(node.mode)) { + attr.size = node.usedBytes + } else if (FS.isLink(node.mode)) { + attr.size = node.link.length + } else { + attr.size = 0 + } + attr.atime = new Date(node.timestamp); + attr.mtime = new Date(node.timestamp); + attr.ctime = new Date(node.timestamp); + attr.blksize = 4096; + attr.blocks = Math.ceil(attr.size / attr.blksize); + return attr + }, + setattr: function(node, attr) { + if (attr.mode !== undefined) { + node.mode = attr.mode + } + if (attr.timestamp !== undefined) { + node.timestamp = attr.timestamp + } + if (attr.size !== undefined) { + MEMFS.resizeFileStorage(node, attr.size) + } + }, + lookup: function(parent, name) { + throw FS.genericErrors[44] + }, + mknod: function(parent, name, mode, dev) { + return MEMFS.createNode(parent, name, mode, dev) + }, + rename: function(old_node, new_dir, new_name) { + if (FS.isDir(old_node.mode)) { + var new_node; + try { + new_node = FS.lookupNode(new_dir, new_name) + } catch (e) {} + if (new_node) { + for (var i in new_node.contents) { + throw new FS.ErrnoError(55) + } + } + } + delete old_node.parent.contents[old_node.name]; + old_node.name = new_name; + new_dir.contents[new_name] = old_node; + old_node.parent = new_dir + }, + unlink: function(parent, name) { + delete parent.contents[name] + }, + rmdir: function(parent, name) { + var node = FS.lookupNode(parent, name); + for (var i in node.contents) { + throw new FS.ErrnoError(55) + } + delete parent.contents[name] + }, + readdir: function(node) { + var entries = [".", ".."]; + for (var key in node.contents) { + if (!node.contents.hasOwnProperty(key)) { + continue + } + entries.push(key) + } + return entries + }, + symlink: function(parent, newname, oldpath) { + var node = MEMFS.createNode(parent, newname, 511 | 40960, 0); + node.link = oldpath; + return node + }, + readlink: function(node) { + if (!FS.isLink(node.mode)) { + throw new FS.ErrnoError(28) + } + return node.link + } + }, + stream_ops: { + read: function(stream, buffer, offset, length, position) { + var contents = stream.node.contents; + if (position >= stream.node.usedBytes) return 0; + var size = Math.min(stream.node.usedBytes - position, length); + assert(size >= 0); + if (size > 8 && contents.subarray) { + buffer.set(contents.subarray(position, position + size), offset) + } else { + for (var i = 0; i < size; i++) buffer[offset + i] = contents[position + i] + } + return size + }, + write: function(stream, buffer, offset, length, position, canOwn) { + if (!length) return 0; + var node = stream.node; + node.timestamp = Date.now(); + if (buffer.subarray && (!node.contents || node.contents.subarray)) { + if (canOwn) { + assert(position === 0, "canOwn must imply no weird position inside the file"); + node.contents = buffer.subarray(offset, offset + length); + node.usedBytes = length; + return length + } else if (node.usedBytes === 0 && position === 0) { + node.contents = new Uint8Array(buffer.subarray(offset, offset + length)); + node.usedBytes = length; + return length + } else if (position + length <= node.usedBytes) { + node.contents.set(buffer.subarray(offset, offset + length), position); + return length + } + } + MEMFS.expandFileStorage(node, position + length); + if (node.contents.subarray && buffer.subarray) node.contents.set(buffer.subarray(offset, offset + length), position); + else { + for (var i = 0; i < length; i++) { + node.contents[position + i] = buffer[offset + i] + } + } + node.usedBytes = Math.max(node.usedBytes, position + length); + return length + }, + llseek: function(stream, offset, whence) { + var position = offset; + if (whence === 1) { + position += stream.position + } else if (whence === 2) { + if (FS.isFile(stream.node.mode)) { + position += stream.node.usedBytes + } + } + if (position < 0) { + throw new FS.ErrnoError(28) + } + return position + }, + allocate: function(stream, offset, length) { + MEMFS.expandFileStorage(stream.node, offset + length); + stream.node.usedBytes = Math.max(stream.node.usedBytes, offset + length) + }, + mmap: function(stream, buffer, offset, length, position, prot, flags) { + if (!FS.isFile(stream.node.mode)) { + throw new FS.ErrnoError(43) + } + var ptr; + var allocated; + var contents = stream.node.contents; + if (!(flags & 2) && (contents.buffer === buffer || contents.buffer === buffer.buffer)) { + allocated = false; + ptr = contents.byteOffset + } else { + if (position > 0 || position + length < stream.node.usedBytes) { + if (contents.subarray) { + contents = contents.subarray(position, position + length) + } else { + contents = Array.prototype.slice.call(contents, position, position + length) + } + } + allocated = true; + var fromHeap = buffer.buffer == HEAP8.buffer; + ptr = _malloc(length); + if (!ptr) { + throw new FS.ErrnoError(48) + }(fromHeap ? HEAP8 : buffer).set(contents, ptr) + } + return { + ptr: ptr, + allocated: allocated + } + }, + msync: function(stream, buffer, offset, length, mmapFlags) { + if (!FS.isFile(stream.node.mode)) { + throw new FS.ErrnoError(43) + } + if (mmapFlags & 2) { + return 0 + } + var bytesWritten = MEMFS.stream_ops.write(stream, buffer, 0, length, offset, false); + return 0 + } + } +}; +var IDBFS = { + dbs: {}, + indexedDB: function() { + if (typeof indexedDB !== "undefined") return indexedDB; + var ret = null; + if (typeof window === "object") ret = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; + assert(ret, "IDBFS used, but indexedDB not supported"); + return ret + }, + DB_VERSION: 21, + DB_STORE_NAME: "FILE_DATA", + mount: function(mount) { + return MEMFS.mount.apply(null, arguments) + }, + syncfs: function(mount, populate, callback) { + IDBFS.getLocalSet(mount, function(err, local) { + if (err) return callback(err); + IDBFS.getRemoteSet(mount, function(err, remote) { + if (err) return callback(err); + var src = populate ? remote : local; + var dst = populate ? local : remote; + IDBFS.reconcile(src, dst, callback) + }) + }) + }, + getDB: function(name, callback) { + var db = IDBFS.dbs[name]; + if (db) { + return callback(null, db) + } + var req; + try { + req = IDBFS.indexedDB().open(name, IDBFS.DB_VERSION) + } catch (e) { + return callback(e) + } + if (!req) { + return callback("Unable to connect to IndexedDB") + } + req.onupgradeneeded = function(e) { + var db = e.target.result; + var transaction = e.target.transaction; + var fileStore; + if (db.objectStoreNames.contains(IDBFS.DB_STORE_NAME)) { + fileStore = transaction.objectStore(IDBFS.DB_STORE_NAME) + } else { + fileStore = db.createObjectStore(IDBFS.DB_STORE_NAME) + } + if (!fileStore.indexNames.contains("timestamp")) { + fileStore.createIndex("timestamp", "timestamp", { + unique: false + }) + } + }; + req.onsuccess = function() { + db = req.result; + IDBFS.dbs[name] = db; + callback(null, db) + }; + req.onerror = function(e) { + callback(this.error); + e.preventDefault() + } + }, + getLocalSet: function(mount, callback) { + var entries = {}; + + function isRealDir(p) { + return p !== "." && p !== ".." + } + + function toAbsolute(root) { + return function(p) { + return PATH.join2(root, p) + } + } + var check = FS.readdir(mount.mountpoint).filter(isRealDir).map(toAbsolute(mount.mountpoint)); + while (check.length) { + var path = check.pop(); + var stat; + try { + stat = FS.stat(path) + } catch (e) { + return callback(e) + } + if (FS.isDir(stat.mode)) { + check.push.apply(check, FS.readdir(path).filter(isRealDir).map(toAbsolute(path))) + } + entries[path] = { + timestamp: stat.mtime + } + } + return callback(null, { + type: "local", + entries: entries + }) + }, + getRemoteSet: function(mount, callback) { + var entries = {}; + IDBFS.getDB(mount.mountpoint, function(err, db) { + if (err) return callback(err); + try { + var transaction = db.transaction([IDBFS.DB_STORE_NAME], "readonly"); + transaction.onerror = function(e) { + callback(this.error); + e.preventDefault() + }; + var store = transaction.objectStore(IDBFS.DB_STORE_NAME); + var index = store.index("timestamp"); + index.openKeyCursor().onsuccess = function(event) { + var cursor = event.target.result; + if (!cursor) { + return callback(null, { + type: "remote", + db: db, + entries: entries + }) + } + entries[cursor.primaryKey] = { + timestamp: cursor.key + }; + cursor.continue() + } + } catch (e) { + return callback(e) + } + }) + }, + loadLocalEntry: function(path, callback) { + var stat, node; + try { + var lookup = FS.lookupPath(path); + node = lookup.node; + stat = FS.stat(path) + } catch (e) { + return callback(e) + } + if (FS.isDir(stat.mode)) { + return callback(null, { + timestamp: stat.mtime, + mode: stat.mode + }) + } else if (FS.isFile(stat.mode)) { + node.contents = MEMFS.getFileDataAsTypedArray(node); + return callback(null, { + timestamp: stat.mtime, + mode: stat.mode, + contents: node.contents + }) + } else { + return callback(new Error("node type not supported")) + } + }, + storeLocalEntry: function(path, entry, callback) { + try { + if (FS.isDir(entry.mode)) { + FS.mkdir(path, entry.mode) + } else if (FS.isFile(entry.mode)) { + FS.writeFile(path, entry.contents, { + canOwn: true + }) + } else { + return callback(new Error("node type not supported")) + } + FS.chmod(path, entry.mode); + FS.utime(path, entry.timestamp, entry.timestamp) + } catch (e) { + return callback(e) + } + callback(null) + }, + removeLocalEntry: function(path, callback) { + try { + var lookup = FS.lookupPath(path); + var stat = FS.stat(path); + if (FS.isDir(stat.mode)) { + FS.rmdir(path) + } else if (FS.isFile(stat.mode)) { + FS.unlink(path) + } + } catch (e) { + return callback(e) + } + callback(null) + }, + loadRemoteEntry: function(store, path, callback) { + var req = store.get(path); + req.onsuccess = function(event) { + callback(null, event.target.result) + }; + req.onerror = function(e) { + callback(this.error); + e.preventDefault() + } + }, + storeRemoteEntry: function(store, path, entry, callback) { + var req = store.put(entry, path); + req.onsuccess = function() { + callback(null) + }; + req.onerror = function(e) { + callback(this.error); + e.preventDefault() + } + }, + removeRemoteEntry: function(store, path, callback) { + var req = store.delete(path); + req.onsuccess = function() { + callback(null) + }; + req.onerror = function(e) { + callback(this.error); + e.preventDefault() + } + }, + reconcile: function(src, dst, callback) { + var total = 0; + var create = []; + Object.keys(src.entries).forEach(function(key) { + var e = src.entries[key]; + var e2 = dst.entries[key]; + if (!e2 || e.timestamp > e2.timestamp) { + create.push(key); + total++ + } + }); + var remove = []; + Object.keys(dst.entries).forEach(function(key) { + var e = dst.entries[key]; + var e2 = src.entries[key]; + if (!e2) { + remove.push(key); + total++ + } + }); + if (!total) { + return callback(null) + } + var errored = false; + var db = src.type === "remote" ? src.db : dst.db; + var transaction = db.transaction([IDBFS.DB_STORE_NAME], "readwrite"); + var store = transaction.objectStore(IDBFS.DB_STORE_NAME); + + function done(err) { + if (err && !errored) { + errored = true; + return callback(err) + } + } + transaction.onerror = function(e) { + done(this.error); + e.preventDefault() + }; + transaction.oncomplete = function(e) { + if (!errored) { + callback(null) + } + }; + create.sort().forEach(function(path) { + if (dst.type === "local") { + IDBFS.loadRemoteEntry(store, path, function(err, entry) { + if (err) return done(err); + IDBFS.storeLocalEntry(path, entry, done) + }) + } else { + IDBFS.loadLocalEntry(path, function(err, entry) { + if (err) return done(err); + IDBFS.storeRemoteEntry(store, path, entry, done) + }) + } + }); + remove.sort().reverse().forEach(function(path) { + if (dst.type === "local") { + IDBFS.removeLocalEntry(path, done) + } else { + IDBFS.removeRemoteEntry(store, path, done) + } + }) + } +}; +var ERRNO_CODES = { + EPERM: 63, + ENOENT: 44, + ESRCH: 71, + EINTR: 27, + EIO: 29, + ENXIO: 60, + E2BIG: 1, + ENOEXEC: 45, + EBADF: 8, + ECHILD: 12, + EAGAIN: 6, + EWOULDBLOCK: 6, + ENOMEM: 48, + EACCES: 2, + EFAULT: 21, + ENOTBLK: 105, + EBUSY: 10, + EEXIST: 20, + EXDEV: 75, + ENODEV: 43, + ENOTDIR: 54, + EISDIR: 31, + EINVAL: 28, + ENFILE: 41, + EMFILE: 33, + ENOTTY: 59, + ETXTBSY: 74, + EFBIG: 22, + ENOSPC: 51, + ESPIPE: 70, + EROFS: 69, + EMLINK: 34, + EPIPE: 64, + EDOM: 18, + ERANGE: 68, + ENOMSG: 49, + EIDRM: 24, + ECHRNG: 106, + EL2NSYNC: 156, + EL3HLT: 107, + EL3RST: 108, + ELNRNG: 109, + EUNATCH: 110, + ENOCSI: 111, + EL2HLT: 112, + EDEADLK: 16, + ENOLCK: 46, + EBADE: 113, + EBADR: 114, + EXFULL: 115, + ENOANO: 104, + EBADRQC: 103, + EBADSLT: 102, + EDEADLOCK: 16, + EBFONT: 101, + ENOSTR: 100, + ENODATA: 116, + ETIME: 117, + ENOSR: 118, + ENONET: 119, + ENOPKG: 120, + EREMOTE: 121, + ENOLINK: 47, + EADV: 122, + ESRMNT: 123, + ECOMM: 124, + EPROTO: 65, + EMULTIHOP: 36, + EDOTDOT: 125, + EBADMSG: 9, + ENOTUNIQ: 126, + EBADFD: 127, + EREMCHG: 128, + ELIBACC: 129, + ELIBBAD: 130, + ELIBSCN: 131, + ELIBMAX: 132, + ELIBEXEC: 133, + ENOSYS: 52, + ENOTEMPTY: 55, + ENAMETOOLONG: 37, + ELOOP: 32, + EOPNOTSUPP: 138, + EPFNOSUPPORT: 139, + ECONNRESET: 15, + ENOBUFS: 42, + EAFNOSUPPORT: 5, + EPROTOTYPE: 67, + ENOTSOCK: 57, + ENOPROTOOPT: 50, + ESHUTDOWN: 140, + ECONNREFUSED: 14, + EADDRINUSE: 3, + ECONNABORTED: 13, + ENETUNREACH: 40, + ENETDOWN: 38, + ETIMEDOUT: 73, + EHOSTDOWN: 142, + EHOSTUNREACH: 23, + EINPROGRESS: 26, + EALREADY: 7, + EDESTADDRREQ: 17, + EMSGSIZE: 35, + EPROTONOSUPPORT: 66, + ESOCKTNOSUPPORT: 137, + EADDRNOTAVAIL: 4, + ENETRESET: 39, + EISCONN: 30, + ENOTCONN: 53, + ETOOMANYREFS: 141, + EUSERS: 136, + EDQUOT: 19, + ESTALE: 72, + ENOTSUP: 138, + ENOMEDIUM: 148, + EILSEQ: 25, + EOVERFLOW: 61, + ECANCELED: 11, + ENOTRECOVERABLE: 56, + EOWNERDEAD: 62, + ESTRPIPE: 135 +}; +var NODEFS = { + isWindows: false, + staticInit: function() { + NODEFS.isWindows = !!process.platform.match(/^win/); + var flags = process["binding"]("constants"); + if (flags["fs"]) { + flags = flags["fs"] + } + NODEFS.flagsForNodeMap = { + 1024: flags["O_APPEND"], + 64: flags["O_CREAT"], + 128: flags["O_EXCL"], + 0: flags["O_RDONLY"], + 2: flags["O_RDWR"], + 4096: flags["O_SYNC"], + 512: flags["O_TRUNC"], + 1: flags["O_WRONLY"] + } + }, + bufferFrom: function(arrayBuffer) { + return Buffer["alloc"] ? Buffer.from(arrayBuffer) : new Buffer(arrayBuffer) + }, + convertNodeCode: function(e) { + var code = e.code; + assert(code in ERRNO_CODES); + return ERRNO_CODES[code] + }, + mount: function(mount) { + assert(ENVIRONMENT_HAS_NODE); + return NODEFS.createNode(null, "/", NODEFS.getMode(mount.opts.root), 0) + }, + createNode: function(parent, name, mode, dev) { + if (!FS.isDir(mode) && !FS.isFile(mode) && !FS.isLink(mode)) { + throw new FS.ErrnoError(28) + } + var node = FS.createNode(parent, name, mode); + node.node_ops = NODEFS.node_ops; + node.stream_ops = NODEFS.stream_ops; + return node + }, + getMode: function(path) { + var stat; + try { + stat = fs.lstatSync(path); + if (NODEFS.isWindows) { + stat.mode = stat.mode | (stat.mode & 292) >> 2 + } + } catch (e) { + if (!e.code) throw e; + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)) + } + return stat.mode + }, + realPath: function(node) { + var parts = []; + while (node.parent !== node) { + parts.push(node.name); + node = node.parent + } + parts.push(node.mount.opts.root); + parts.reverse(); + return PATH.join.apply(null, parts) + }, + flagsForNode: function(flags) { + flags &= ~2097152; + flags &= ~2048; + flags &= ~32768; + flags &= ~524288; + var newFlags = 0; + for (var k in NODEFS.flagsForNodeMap) { + if (flags & k) { + newFlags |= NODEFS.flagsForNodeMap[k]; + flags ^= k + } + } + if (!flags) { + return newFlags + } else { + throw new FS.ErrnoError(28) + } + }, + node_ops: { + getattr: function(node) { + var path = NODEFS.realPath(node); + var stat; + try { + stat = fs.lstatSync(path) + } catch (e) { + if (!e.code) throw e; + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)) + } + if (NODEFS.isWindows && !stat.blksize) { + stat.blksize = 4096 + } + if (NODEFS.isWindows && !stat.blocks) { + stat.blocks = (stat.size + stat.blksize - 1) / stat.blksize | 0 + } + return { + dev: stat.dev, + ino: stat.ino, + mode: stat.mode, + nlink: stat.nlink, + uid: stat.uid, + gid: stat.gid, + rdev: stat.rdev, + size: stat.size, + atime: stat.atime, + mtime: stat.mtime, + ctime: stat.ctime, + blksize: stat.blksize, + blocks: stat.blocks + } + }, + setattr: function(node, attr) { + var path = NODEFS.realPath(node); + try { + if (attr.mode !== undefined) { + fs.chmodSync(path, attr.mode); + node.mode = attr.mode + } + if (attr.timestamp !== undefined) { + var date = new Date(attr.timestamp); + fs.utimesSync(path, date, date) + } + if (attr.size !== undefined) { + fs.truncateSync(path, attr.size) + } + } catch (e) { + if (!e.code) throw e; + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)) + } + }, + lookup: function(parent, name) { + var path = PATH.join2(NODEFS.realPath(parent), name); + var mode = NODEFS.getMode(path); + return NODEFS.createNode(parent, name, mode) + }, + mknod: function(parent, name, mode, dev) { + var node = NODEFS.createNode(parent, name, mode, dev); + var path = NODEFS.realPath(node); + try { + if (FS.isDir(node.mode)) { + fs.mkdirSync(path, node.mode) + } else { + fs.writeFileSync(path, "", { + mode: node.mode + }) + } + } catch (e) { + if (!e.code) throw e; + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)) + } + return node + }, + rename: function(oldNode, newDir, newName) { + var oldPath = NODEFS.realPath(oldNode); + var newPath = PATH.join2(NODEFS.realPath(newDir), newName); + try { + fs.renameSync(oldPath, newPath) + } catch (e) { + if (!e.code) throw e; + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)) + } + }, + unlink: function(parent, name) { + var path = PATH.join2(NODEFS.realPath(parent), name); + try { + fs.unlinkSync(path) + } catch (e) { + if (!e.code) throw e; + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)) + } + }, + rmdir: function(parent, name) { + var path = PATH.join2(NODEFS.realPath(parent), name); + try { + fs.rmdirSync(path) + } catch (e) { + if (!e.code) throw e; + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)) + } + }, + readdir: function(node) { + var path = NODEFS.realPath(node); + try { + return fs.readdirSync(path) + } catch (e) { + if (!e.code) throw e; + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)) + } + }, + symlink: function(parent, newName, oldPath) { + var newPath = PATH.join2(NODEFS.realPath(parent), newName); + try { + fs.symlinkSync(oldPath, newPath) + } catch (e) { + if (!e.code) throw e; + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)) + } + }, + readlink: function(node) { + var path = NODEFS.realPath(node); + try { + path = fs.readlinkSync(path); + path = NODEJS_PATH.relative(NODEJS_PATH.resolve(node.mount.opts.root), path); + return path + } catch (e) { + if (!e.code) throw e; + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)) + } + } + }, + stream_ops: { + open: function(stream) { + var path = NODEFS.realPath(stream.node); + try { + if (FS.isFile(stream.node.mode)) { + stream.nfd = fs.openSync(path, NODEFS.flagsForNode(stream.flags)) + } + } catch (e) { + if (!e.code) throw e; + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)) + } + }, + close: function(stream) { + try { + if (FS.isFile(stream.node.mode) && stream.nfd) { + fs.closeSync(stream.nfd) + } + } catch (e) { + if (!e.code) throw e; + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)) + } + }, + read: function(stream, buffer, offset, length, position) { + if (length === 0) return 0; + try { + return fs.readSync(stream.nfd, NODEFS.bufferFrom(buffer.buffer), offset, length, position) + } catch (e) { + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)) + } + }, + write: function(stream, buffer, offset, length, position) { + try { + return fs.writeSync(stream.nfd, NODEFS.bufferFrom(buffer.buffer), offset, length, position) + } catch (e) { + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)) + } + }, + llseek: function(stream, offset, whence) { + var position = offset; + if (whence === 1) { + position += stream.position + } else if (whence === 2) { + if (FS.isFile(stream.node.mode)) { + try { + var stat = fs.fstatSync(stream.nfd); + position += stat.size + } catch (e) { + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)) + } + } + } + if (position < 0) { + throw new FS.ErrnoError(28) + } + return position + } + } +}; +var WORKERFS = { + DIR_MODE: 16895, + FILE_MODE: 33279, + reader: null, + mount: function(mount) { + assert(ENVIRONMENT_IS_WORKER); + if (!WORKERFS.reader) WORKERFS.reader = new FileReaderSync; + var root = WORKERFS.createNode(null, "/", WORKERFS.DIR_MODE, 0); + var createdParents = {}; + + function ensureParent(path) { + var parts = path.split("/"); + var parent = root; + for (var i = 0; i < parts.length - 1; i++) { + var curr = parts.slice(0, i + 1).join("/"); + if (!createdParents[curr]) { + createdParents[curr] = WORKERFS.createNode(parent, parts[i], WORKERFS.DIR_MODE, 0) + } + parent = createdParents[curr] + } + return parent + } + + function base(path) { + var parts = path.split("/"); + return parts[parts.length - 1] + } + Array.prototype.forEach.call(mount.opts["files"] || [], function(file) { + WORKERFS.createNode(ensureParent(file.name), base(file.name), WORKERFS.FILE_MODE, 0, file, file.lastModifiedDate) + }); + (mount.opts["blobs"] || []).forEach(function(obj) { + WORKERFS.createNode(ensureParent(obj["name"]), base(obj["name"]), WORKERFS.FILE_MODE, 0, obj["data"]) + }); + (mount.opts["packages"] || []).forEach(function(pack) { + pack["metadata"].files.forEach(function(file) { + var name = file.filename.substr(1); + WORKERFS.createNode(ensureParent(name), base(name), WORKERFS.FILE_MODE, 0, pack["blob"].slice(file.start, file.end)) + }) + }); + return root + }, + createNode: function(parent, name, mode, dev, contents, mtime) { + var node = FS.createNode(parent, name, mode); + node.mode = mode; + node.node_ops = WORKERFS.node_ops; + node.stream_ops = WORKERFS.stream_ops; + node.timestamp = (mtime || new Date).getTime(); + assert(WORKERFS.FILE_MODE !== WORKERFS.DIR_MODE); + if (mode === WORKERFS.FILE_MODE) { + node.size = contents.size; + node.contents = contents + } else { + node.size = 4096; + node.contents = {} + } + if (parent) { + parent.contents[name] = node + } + return node + }, + node_ops: { + getattr: function(node) { + return { + dev: 1, + ino: undefined, + mode: node.mode, + nlink: 1, + uid: 0, + gid: 0, + rdev: undefined, + size: node.size, + atime: new Date(node.timestamp), + mtime: new Date(node.timestamp), + ctime: new Date(node.timestamp), + blksize: 4096, + blocks: Math.ceil(node.size / 4096) + } + }, + setattr: function(node, attr) { + if (attr.mode !== undefined) { + node.mode = attr.mode + } + if (attr.timestamp !== undefined) { + node.timestamp = attr.timestamp + } + }, + lookup: function(parent, name) { + throw new FS.ErrnoError(44) + }, + mknod: function(parent, name, mode, dev) { + throw new FS.ErrnoError(63) + }, + rename: function(oldNode, newDir, newName) { + throw new FS.ErrnoError(63) + }, + unlink: function(parent, name) { + throw new FS.ErrnoError(63) + }, + rmdir: function(parent, name) { + throw new FS.ErrnoError(63) + }, + readdir: function(node) { + var entries = [".", ".."]; + for (var key in node.contents) { + if (!node.contents.hasOwnProperty(key)) { + continue + } + entries.push(key) + } + return entries + }, + symlink: function(parent, newName, oldPath) { + throw new FS.ErrnoError(63) + }, + readlink: function(node) { + throw new FS.ErrnoError(63) + } + }, + stream_ops: { + read: function(stream, buffer, offset, length, position) { + if (position >= stream.node.size) return 0; + var chunk = stream.node.contents.slice(position, position + length); + var ab = WORKERFS.reader.readAsArrayBuffer(chunk); + buffer.set(new Uint8Array(ab), offset); + return chunk.size + }, + write: function(stream, buffer, offset, length, position) { + throw new FS.ErrnoError(29) + }, + llseek: function(stream, offset, whence) { + var position = offset; + if (whence === 1) { + position += stream.position + } else if (whence === 2) { + if (FS.isFile(stream.node.mode)) { + position += stream.node.size + } + } + if (position < 0) { + throw new FS.ErrnoError(28) + } + return position + } + } +}; +var ERRNO_MESSAGES = { + 0: "Success", + 1: "Arg list too long", + 2: "Permission denied", + 3: "Address already in use", + 4: "Address not available", + 5: "Address family not supported by protocol family", + 6: "No more processes", + 7: "Socket already connected", + 8: "Bad file number", + 9: "Trying to read unreadable message", + 10: "Mount device busy", + 11: "Operation canceled", + 12: "No children", + 13: "Connection aborted", + 14: "Connection refused", + 15: "Connection reset by peer", + 16: "File locking deadlock error", + 17: "Destination address required", + 18: "Math arg out of domain of func", + 19: "Quota exceeded", + 20: "File exists", + 21: "Bad address", + 22: "File too large", + 23: "Host is unreachable", + 24: "Identifier removed", + 25: "Illegal byte sequence", + 26: "Connection already in progress", + 27: "Interrupted system call", + 28: "Invalid argument", + 29: "I/O error", + 30: "Socket is already connected", + 31: "Is a directory", + 32: "Too many symbolic links", + 33: "Too many open files", + 34: "Too many links", + 35: "Message too long", + 36: "Multihop attempted", + 37: "File or path name too long", + 38: "Network interface is not configured", + 39: "Connection reset by network", + 40: "Network is unreachable", + 41: "Too many open files in system", + 42: "No buffer space available", + 43: "No such device", + 44: "No such file or directory", + 45: "Exec format error", + 46: "No record locks available", + 47: "The link has been severed", + 48: "Not enough core", + 49: "No message of desired type", + 50: "Protocol not available", + 51: "No space left on device", + 52: "Function not implemented", + 53: "Socket is not connected", + 54: "Not a directory", + 55: "Directory not empty", + 56: "State not recoverable", + 57: "Socket operation on non-socket", + 59: "Not a typewriter", + 60: "No such device or address", + 61: "Value too large for defined data type", + 62: "Previous owner died", + 63: "Not super-user", + 64: "Broken pipe", + 65: "Protocol error", + 66: "Unknown protocol", + 67: "Protocol wrong type for socket", + 68: "Math result not representable", + 69: "Read only file system", + 70: "Illegal seek", + 71: "No such process", + 72: "Stale file handle", + 73: "Connection timed out", + 74: "Text file busy", + 75: "Cross-device link", + 100: "Device not a stream", + 101: "Bad font file fmt", + 102: "Invalid slot", + 103: "Invalid request code", + 104: "No anode", + 105: "Block device required", + 106: "Channel number out of range", + 107: "Level 3 halted", + 108: "Level 3 reset", + 109: "Link number out of range", + 110: "Protocol driver not attached", + 111: "No CSI structure available", + 112: "Level 2 halted", + 113: "Invalid exchange", + 114: "Invalid request descriptor", + 115: "Exchange full", + 116: "No data (for no delay io)", + 117: "Timer expired", + 118: "Out of streams resources", + 119: "Machine is not on the network", + 120: "Package not installed", + 121: "The object is remote", + 122: "Advertise error", + 123: "Srmount error", + 124: "Communication error on send", + 125: "Cross mount point (not really error)", + 126: "Given log. name not unique", + 127: "f.d. invalid for this operation", + 128: "Remote address changed", + 129: "Can access a needed shared lib", + 130: "Accessing a corrupted shared lib", + 131: ".lib section in a.out corrupted", + 132: "Attempting to link in too many libs", + 133: "Attempting to exec a shared library", + 135: "Streams pipe error", + 136: "Too many users", + 137: "Socket type not supported", + 138: "Not supported", + 139: "Protocol family not supported", + 140: "Can't send after socket shutdown", + 141: "Too many references", + 142: "Host is down", + 148: "No medium (in tape drive)", + 156: "Level 2 not synchronized" +}; +var FS = { + root: null, + mounts: [], + devices: {}, + streams: [], + nextInode: 1, + nameTable: null, + currentPath: "/", + initialized: false, + ignorePermissions: true, + trackingDelegate: {}, + tracking: { + openFlags: { + READ: 1, + WRITE: 2 + } + }, + ErrnoError: null, + genericErrors: {}, + filesystems: null, + syncFSRequests: 0, + handleFSError: function(e) { + if (!(e instanceof FS.ErrnoError)) throw e + " : " + stackTrace(); + return ___setErrNo(e.errno) + }, + lookupPath: function(path, opts) { + path = PATH_FS.resolve(FS.cwd(), path); + opts = opts || {}; + if (!path) return { + path: "", + node: null + }; + var defaults = { + follow_mount: true, + recurse_count: 0 + }; + for (var key in defaults) { + if (opts[key] === undefined) { + opts[key] = defaults[key] + } + } + if (opts.recurse_count > 8) { + throw new FS.ErrnoError(32) + } + var parts = PATH.normalizeArray(path.split("/").filter(function(p) { + return !!p + }), false); + var current = FS.root; + var current_path = "/"; + for (var i = 0; i < parts.length; i++) { + var islast = i === parts.length - 1; + if (islast && opts.parent) { + break + } + current = FS.lookupNode(current, parts[i]); + current_path = PATH.join2(current_path, parts[i]); + if (FS.isMountpoint(current)) { + if (!islast || islast && opts.follow_mount) { + current = current.mounted.root + } + } + if (!islast || opts.follow) { + var count = 0; + while (FS.isLink(current.mode)) { + var link = FS.readlink(current_path); + current_path = PATH_FS.resolve(PATH.dirname(current_path), link); + var lookup = FS.lookupPath(current_path, { + recurse_count: opts.recurse_count + }); + current = lookup.node; + if (count++ > 40) { + throw new FS.ErrnoError(32) + } + } + } + } + return { + path: current_path, + node: current + } + }, + getPath: function(node) { + var path; + while (true) { + if (FS.isRoot(node)) { + var mount = node.mount.mountpoint; + if (!path) return mount; + return mount[mount.length - 1] !== "/" ? mount + "/" + path : mount + path + } + path = path ? node.name + "/" + path : node.name; + node = node.parent + } + }, + hashName: function(parentid, name) { + var hash = 0; + for (var i = 0; i < name.length; i++) { + hash = (hash << 5) - hash + name.charCodeAt(i) | 0 + } + return (parentid + hash >>> 0) % FS.nameTable.length + }, + hashAddNode: function(node) { + var hash = FS.hashName(node.parent.id, node.name); + node.name_next = FS.nameTable[hash]; + FS.nameTable[hash] = node + }, + hashRemoveNode: function(node) { + var hash = FS.hashName(node.parent.id, node.name); + if (FS.nameTable[hash] === node) { + FS.nameTable[hash] = node.name_next + } else { + var current = FS.nameTable[hash]; + while (current) { + if (current.name_next === node) { + current.name_next = node.name_next; + break + } + current = current.name_next + } + } + }, + lookupNode: function(parent, name) { + var err = FS.mayLookup(parent); + if (err) { + throw new FS.ErrnoError(err, parent) + } + var hash = FS.hashName(parent.id, name); + for (var node = FS.nameTable[hash]; node; node = node.name_next) { + var nodeName = node.name; + if (node.parent.id === parent.id && nodeName === name) { + return node + } + } + return FS.lookup(parent, name) + }, + createNode: function(parent, name, mode, rdev) { + if (!FS.FSNode) { + FS.FSNode = function(parent, name, mode, rdev) { + if (!parent) { + parent = this + } + this.parent = parent; + this.mount = parent.mount; + this.mounted = null; + this.id = FS.nextInode++; + this.name = name; + this.mode = mode; + this.node_ops = {}; + this.stream_ops = {}; + this.rdev = rdev + }; + FS.FSNode.prototype = {}; + var readMode = 292 | 73; + var writeMode = 146; + Object.defineProperties(FS.FSNode.prototype, { + read: { + get: function() { + return (this.mode & readMode) === readMode + }, + set: function(val) { + val ? this.mode |= readMode : this.mode &= ~readMode + } + }, + write: { + get: function() { + return (this.mode & writeMode) === writeMode + }, + set: function(val) { + val ? this.mode |= writeMode : this.mode &= ~writeMode + } + }, + isFolder: { + get: function() { + return FS.isDir(this.mode) + } + }, + isDevice: { + get: function() { + return FS.isChrdev(this.mode) + } + } + }) + } + var node = new FS.FSNode(parent, name, mode, rdev); + FS.hashAddNode(node); + return node + }, + destroyNode: function(node) { + FS.hashRemoveNode(node) + }, + isRoot: function(node) { + return node === node.parent + }, + isMountpoint: function(node) { + return !!node.mounted + }, + isFile: function(mode) { + return (mode & 61440) === 32768 + }, + isDir: function(mode) { + return (mode & 61440) === 16384 + }, + isLink: function(mode) { + return (mode & 61440) === 40960 + }, + isChrdev: function(mode) { + return (mode & 61440) === 8192 + }, + isBlkdev: function(mode) { + return (mode & 61440) === 24576 + }, + isFIFO: function(mode) { + return (mode & 61440) === 4096 + }, + isSocket: function(mode) { + return (mode & 49152) === 49152 + }, + flagModes: { + "r": 0, + "rs": 1052672, + "r+": 2, + "w": 577, + "wx": 705, + "xw": 705, + "w+": 578, + "wx+": 706, + "xw+": 706, + "a": 1089, + "ax": 1217, + "xa": 1217, + "a+": 1090, + "ax+": 1218, + "xa+": 1218 + }, + modeStringToFlags: function(str) { + var flags = FS.flagModes[str]; + if (typeof flags === "undefined") { + throw new Error("Unknown file open mode: " + str) + } + return flags + }, + flagsToPermissionString: function(flag) { + var perms = ["r", "w", "rw"][flag & 3]; + if (flag & 512) { + perms += "w" + } + return perms + }, + nodePermissions: function(node, perms) { + if (FS.ignorePermissions) { + return 0 + } + if (perms.indexOf("r") !== -1 && !(node.mode & 292)) { + return 2 + } else if (perms.indexOf("w") !== -1 && !(node.mode & 146)) { + return 2 + } else if (perms.indexOf("x") !== -1 && !(node.mode & 73)) { + return 2 + } + return 0 + }, + mayLookup: function(dir) { + var err = FS.nodePermissions(dir, "x"); + if (err) return err; + if (!dir.node_ops.lookup) return 2; + return 0 + }, + mayCreate: function(dir, name) { + try { + var node = FS.lookupNode(dir, name); + return 20 + } catch (e) {} + return FS.nodePermissions(dir, "wx") + }, + mayDelete: function(dir, name, isdir) { + var node; + try { + node = FS.lookupNode(dir, name) + } catch (e) { + return e.errno + } + var err = FS.nodePermissions(dir, "wx"); + if (err) { + return err + } + if (isdir) { + if (!FS.isDir(node.mode)) { + return 54 + } + if (FS.isRoot(node) || FS.getPath(node) === FS.cwd()) { + return 10 + } + } else { + if (FS.isDir(node.mode)) { + return 31 + } + } + return 0 + }, + mayOpen: function(node, flags) { + if (!node) { + return 44 + } + if (FS.isLink(node.mode)) { + return 32 + } else if (FS.isDir(node.mode)) { + if (FS.flagsToPermissionString(flags) !== "r" || flags & 512) { + return 31 + } + } + return FS.nodePermissions(node, FS.flagsToPermissionString(flags)) + }, + MAX_OPEN_FDS: 4096, + nextfd: function(fd_start, fd_end) { + fd_start = fd_start || 0; + fd_end = fd_end || FS.MAX_OPEN_FDS; + for (var fd = fd_start; fd <= fd_end; fd++) { + if (!FS.streams[fd]) { + return fd + } + } + throw new FS.ErrnoError(33) + }, + getStream: function(fd) { + return FS.streams[fd] + }, + createStream: function(stream, fd_start, fd_end) { + if (!FS.FSStream) { + FS.FSStream = function() {}; + FS.FSStream.prototype = {}; + Object.defineProperties(FS.FSStream.prototype, { + object: { + get: function() { + return this.node + }, + set: function(val) { + this.node = val + } + }, + isRead: { + get: function() { + return (this.flags & 2097155) !== 1 + } + }, + isWrite: { + get: function() { + return (this.flags & 2097155) !== 0 + } + }, + isAppend: { + get: function() { + return this.flags & 1024 + } + } + }) + } + var newStream = new FS.FSStream; + for (var p in stream) { + newStream[p] = stream[p] + } + stream = newStream; + var fd = FS.nextfd(fd_start, fd_end); + stream.fd = fd; + FS.streams[fd] = stream; + return stream + }, + closeStream: function(fd) { + FS.streams[fd] = null + }, + chrdev_stream_ops: { + open: function(stream) { + var device = FS.getDevice(stream.node.rdev); + stream.stream_ops = device.stream_ops; + if (stream.stream_ops.open) { + stream.stream_ops.open(stream) + } + }, + llseek: function() { + throw new FS.ErrnoError(70) + } + }, + major: function(dev) { + return dev >> 8 + }, + minor: function(dev) { + return dev & 255 + }, + makedev: function(ma, mi) { + return ma << 8 | mi + }, + registerDevice: function(dev, ops) { + FS.devices[dev] = { + stream_ops: ops + } + }, + getDevice: function(dev) { + return FS.devices[dev] + }, + getMounts: function(mount) { + var mounts = []; + var check = [mount]; + while (check.length) { + var m = check.pop(); + mounts.push(m); + check.push.apply(check, m.mounts) + } + return mounts + }, + syncfs: function(populate, callback) { + if (typeof populate === "function") { + callback = populate; + populate = false + } + FS.syncFSRequests++; + if (FS.syncFSRequests > 1) { + console.log("warning: " + FS.syncFSRequests + " FS.syncfs operations in flight at once, probably just doing extra work") + } + var mounts = FS.getMounts(FS.root.mount); + var completed = 0; + + function doCallback(err) { + assert(FS.syncFSRequests > 0); + FS.syncFSRequests--; + return callback(err) + } + + function done(err) { + if (err) { + if (!done.errored) { + done.errored = true; + return doCallback(err) + } + return + } + if (++completed >= mounts.length) { + doCallback(null) + } + } + mounts.forEach(function(mount) { + if (!mount.type.syncfs) { + return done(null) + } + mount.type.syncfs(mount, populate, done) + }) + }, + mount: function(type, opts, mountpoint) { + var root = mountpoint === "/"; + var pseudo = !mountpoint; + var node; + if (root && FS.root) { + throw new FS.ErrnoError(10) + } else if (!root && !pseudo) { + var lookup = FS.lookupPath(mountpoint, { + follow_mount: false + }); + mountpoint = lookup.path; + node = lookup.node; + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10) + } + if (!FS.isDir(node.mode)) { + throw new FS.ErrnoError(54) + } + } + var mount = { + type: type, + opts: opts, + mountpoint: mountpoint, + mounts: [] + }; + var mountRoot = type.mount(mount); + mountRoot.mount = mount; + mount.root = mountRoot; + if (root) { + FS.root = mountRoot + } else if (node) { + node.mounted = mount; + if (node.mount) { + node.mount.mounts.push(mount) + } + } + return mountRoot + }, + unmount: function(mountpoint) { + var lookup = FS.lookupPath(mountpoint, { + follow_mount: false + }); + if (!FS.isMountpoint(lookup.node)) { + throw new FS.ErrnoError(28) + } + var node = lookup.node; + var mount = node.mounted; + var mounts = FS.getMounts(mount); + Object.keys(FS.nameTable).forEach(function(hash) { + var current = FS.nameTable[hash]; + while (current) { + var next = current.name_next; + if (mounts.indexOf(current.mount) !== -1) { + FS.destroyNode(current) + } + current = next + } + }); + node.mounted = null; + var idx = node.mount.mounts.indexOf(mount); + assert(idx !== -1); + node.mount.mounts.splice(idx, 1) + }, + lookup: function(parent, name) { + return parent.node_ops.lookup(parent, name) + }, + mknod: function(path, mode, dev) { + var lookup = FS.lookupPath(path, { + parent: true + }); + var parent = lookup.node; + var name = PATH.basename(path); + if (!name || name === "." || name === "..") { + throw new FS.ErrnoError(28) + } + var err = FS.mayCreate(parent, name); + if (err) { + throw new FS.ErrnoError(err) + } + if (!parent.node_ops.mknod) { + throw new FS.ErrnoError(63) + } + return parent.node_ops.mknod(parent, name, mode, dev) + }, + create: function(path, mode) { + mode = mode !== undefined ? mode : 438; + mode &= 4095; + mode |= 32768; + return FS.mknod(path, mode, 0) + }, + mkdir: function(path, mode) { + mode = mode !== undefined ? mode : 511; + mode &= 511 | 512; + mode |= 16384; + return FS.mknod(path, mode, 0) + }, + mkdirTree: function(path, mode) { + var dirs = path.split("/"); + var d = ""; + for (var i = 0; i < dirs.length; ++i) { + if (!dirs[i]) continue; + d += "/" + dirs[i]; + try { + FS.mkdir(d, mode) + } catch (e) { + if (e.errno != 20) throw e + } + } + }, + mkdev: function(path, mode, dev) { + if (typeof dev === "undefined") { + dev = mode; + mode = 438 + } + mode |= 8192; + return FS.mknod(path, mode, dev) + }, + symlink: function(oldpath, newpath) { + if (!PATH_FS.resolve(oldpath)) { + throw new FS.ErrnoError(44) + } + var lookup = FS.lookupPath(newpath, { + parent: true + }); + var parent = lookup.node; + if (!parent) { + throw new FS.ErrnoError(44) + } + var newname = PATH.basename(newpath); + var err = FS.mayCreate(parent, newname); + if (err) { + throw new FS.ErrnoError(err) + } + if (!parent.node_ops.symlink) { + throw new FS.ErrnoError(63) + } + return parent.node_ops.symlink(parent, newname, oldpath) + }, + rename: function(old_path, new_path) { + var old_dirname = PATH.dirname(old_path); + var new_dirname = PATH.dirname(new_path); + var old_name = PATH.basename(old_path); + var new_name = PATH.basename(new_path); + var lookup, old_dir, new_dir; + try { + lookup = FS.lookupPath(old_path, { + parent: true + }); + old_dir = lookup.node; + lookup = FS.lookupPath(new_path, { + parent: true + }); + new_dir = lookup.node + } catch (e) { + throw new FS.ErrnoError(10) + } + if (!old_dir || !new_dir) throw new FS.ErrnoError(44); + if (old_dir.mount !== new_dir.mount) { + throw new FS.ErrnoError(75) + } + var old_node = FS.lookupNode(old_dir, old_name); + var relative = PATH_FS.relative(old_path, new_dirname); + if (relative.charAt(0) !== ".") { + throw new FS.ErrnoError(28) + } + relative = PATH_FS.relative(new_path, old_dirname); + if (relative.charAt(0) !== ".") { + throw new FS.ErrnoError(55) + } + var new_node; + try { + new_node = FS.lookupNode(new_dir, new_name) + } catch (e) {} + if (old_node === new_node) { + return + } + var isdir = FS.isDir(old_node.mode); + var err = FS.mayDelete(old_dir, old_name, isdir); + if (err) { + throw new FS.ErrnoError(err) + } + err = new_node ? FS.mayDelete(new_dir, new_name, isdir) : FS.mayCreate(new_dir, new_name); + if (err) { + throw new FS.ErrnoError(err) + } + if (!old_dir.node_ops.rename) { + throw new FS.ErrnoError(63) + } + if (FS.isMountpoint(old_node) || new_node && FS.isMountpoint(new_node)) { + throw new FS.ErrnoError(10) + } + if (new_dir !== old_dir) { + err = FS.nodePermissions(old_dir, "w"); + if (err) { + throw new FS.ErrnoError(err) + } + } + try { + if (FS.trackingDelegate["willMovePath"]) { + FS.trackingDelegate["willMovePath"](old_path, new_path) + } + } catch (e) { + console.log("FS.trackingDelegate['willMovePath']('" + old_path + "', '" + new_path + "') threw an exception: " + e.message) + } + FS.hashRemoveNode(old_node); + try { + old_dir.node_ops.rename(old_node, new_dir, new_name) + } catch (e) { + throw e + } finally { + FS.hashAddNode(old_node) + } + try { + if (FS.trackingDelegate["onMovePath"]) FS.trackingDelegate["onMovePath"](old_path, new_path) + } catch (e) { + console.log("FS.trackingDelegate['onMovePath']('" + old_path + "', '" + new_path + "') threw an exception: " + e.message) + } + }, + rmdir: function(path) { + var lookup = FS.lookupPath(path, { + parent: true + }); + var parent = lookup.node; + var name = PATH.basename(path); + var node = FS.lookupNode(parent, name); + var err = FS.mayDelete(parent, name, true); + if (err) { + throw new FS.ErrnoError(err) + } + if (!parent.node_ops.rmdir) { + throw new FS.ErrnoError(63) + } + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10) + } + try { + if (FS.trackingDelegate["willDeletePath"]) { + FS.trackingDelegate["willDeletePath"](path) + } + } catch (e) { + console.log("FS.trackingDelegate['willDeletePath']('" + path + "') threw an exception: " + e.message) + } + parent.node_ops.rmdir(parent, name); + FS.destroyNode(node); + try { + if (FS.trackingDelegate["onDeletePath"]) FS.trackingDelegate["onDeletePath"](path) + } catch (e) { + console.log("FS.trackingDelegate['onDeletePath']('" + path + "') threw an exception: " + e.message) + } + }, + readdir: function(path) { + var lookup = FS.lookupPath(path, { + follow: true + }); + var node = lookup.node; + if (!node.node_ops.readdir) { + throw new FS.ErrnoError(54) + } + return node.node_ops.readdir(node) + }, + unlink: function(path) { + var lookup = FS.lookupPath(path, { + parent: true + }); + var parent = lookup.node; + var name = PATH.basename(path); + var node = FS.lookupNode(parent, name); + var err = FS.mayDelete(parent, name, false); + if (err) { + throw new FS.ErrnoError(err) + } + if (!parent.node_ops.unlink) { + throw new FS.ErrnoError(63) + } + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10) + } + try { + if (FS.trackingDelegate["willDeletePath"]) { + FS.trackingDelegate["willDeletePath"](path) + } + } catch (e) { + console.log("FS.trackingDelegate['willDeletePath']('" + path + "') threw an exception: " + e.message) + } + parent.node_ops.unlink(parent, name); + FS.destroyNode(node); + try { + if (FS.trackingDelegate["onDeletePath"]) FS.trackingDelegate["onDeletePath"](path) + } catch (e) { + console.log("FS.trackingDelegate['onDeletePath']('" + path + "') threw an exception: " + e.message) + } + }, + readlink: function(path) { + var lookup = FS.lookupPath(path); + var link = lookup.node; + if (!link) { + throw new FS.ErrnoError(44) + } + if (!link.node_ops.readlink) { + throw new FS.ErrnoError(28) + } + return PATH_FS.resolve(FS.getPath(link.parent), link.node_ops.readlink(link)) + }, + stat: function(path, dontFollow) { + var lookup = FS.lookupPath(path, { + follow: !dontFollow + }); + var node = lookup.node; + if (!node) { + throw new FS.ErrnoError(44) + } + if (!node.node_ops.getattr) { + throw new FS.ErrnoError(63) + } + return node.node_ops.getattr(node) + }, + lstat: function(path) { + return FS.stat(path, true) + }, + chmod: function(path, mode, dontFollow) { + var node; + if (typeof path === "string") { + var lookup = FS.lookupPath(path, { + follow: !dontFollow + }); + node = lookup.node + } else { + node = path + } + if (!node.node_ops.setattr) { + throw new FS.ErrnoError(63) + } + node.node_ops.setattr(node, { + mode: mode & 4095 | node.mode & ~4095, + timestamp: Date.now() + }) + }, + lchmod: function(path, mode) { + FS.chmod(path, mode, true) + }, + fchmod: function(fd, mode) { + var stream = FS.getStream(fd); + if (!stream) { + throw new FS.ErrnoError(8) + } + FS.chmod(stream.node, mode) + }, + chown: function(path, uid, gid, dontFollow) { + var node; + if (typeof path === "string") { + var lookup = FS.lookupPath(path, { + follow: !dontFollow + }); + node = lookup.node + } else { + node = path + } + if (!node.node_ops.setattr) { + throw new FS.ErrnoError(63) + } + node.node_ops.setattr(node, { + timestamp: Date.now() + }) + }, + lchown: function(path, uid, gid) { + FS.chown(path, uid, gid, true) + }, + fchown: function(fd, uid, gid) { + var stream = FS.getStream(fd); + if (!stream) { + throw new FS.ErrnoError(8) + } + FS.chown(stream.node, uid, gid) + }, + truncate: function(path, len) { + if (len < 0) { + throw new FS.ErrnoError(28) + } + var node; + if (typeof path === "string") { + var lookup = FS.lookupPath(path, { + follow: true + }); + node = lookup.node + } else { + node = path + } + if (!node.node_ops.setattr) { + throw new FS.ErrnoError(63) + } + if (FS.isDir(node.mode)) { + throw new FS.ErrnoError(31) + } + if (!FS.isFile(node.mode)) { + throw new FS.ErrnoError(28) + } + var err = FS.nodePermissions(node, "w"); + if (err) { + throw new FS.ErrnoError(err) + } + node.node_ops.setattr(node, { + size: len, + timestamp: Date.now() + }) + }, + ftruncate: function(fd, len) { + var stream = FS.getStream(fd); + if (!stream) { + throw new FS.ErrnoError(8) + } + if ((stream.flags & 2097155) === 0) { + throw new FS.ErrnoError(28) + } + FS.truncate(stream.node, len) + }, + utime: function(path, atime, mtime) { + var lookup = FS.lookupPath(path, { + follow: true + }); + var node = lookup.node; + node.node_ops.setattr(node, { + timestamp: Math.max(atime, mtime) + }) + }, + open: function(path, flags, mode, fd_start, fd_end) { + if (path === "") { + throw new FS.ErrnoError(44) + } + flags = typeof flags === "string" ? FS.modeStringToFlags(flags) : flags; + mode = typeof mode === "undefined" ? 438 : mode; + if (flags & 64) { + mode = mode & 4095 | 32768 + } else { + mode = 0 + } + var node; + if (typeof path === "object") { + node = path + } else { + path = PATH.normalize(path); + try { + var lookup = FS.lookupPath(path, { + follow: !(flags & 131072) + }); + node = lookup.node + } catch (e) {} + } + var created = false; + if (flags & 64) { + if (node) { + if (flags & 128) { + throw new FS.ErrnoError(20) + } + } else { + node = FS.mknod(path, mode, 0); + created = true + } + } + if (!node) { + throw new FS.ErrnoError(44) + } + if (FS.isChrdev(node.mode)) { + flags &= ~512 + } + if (flags & 65536 && !FS.isDir(node.mode)) { + throw new FS.ErrnoError(54) + } + if (!created) { + var err = FS.mayOpen(node, flags); + if (err) { + throw new FS.ErrnoError(err) + } + } + if (flags & 512) { + FS.truncate(node, 0) + } + flags &= ~(128 | 512); + var stream = FS.createStream({ + node: node, + path: FS.getPath(node), + flags: flags, + seekable: true, + position: 0, + stream_ops: node.stream_ops, + ungotten: [], + error: false + }, fd_start, fd_end); + if (stream.stream_ops.open) { + stream.stream_ops.open(stream) + } + if (Module["logReadFiles"] && !(flags & 1)) { + if (!FS.readFiles) FS.readFiles = {}; + if (!(path in FS.readFiles)) { + FS.readFiles[path] = 1; + console.log("FS.trackingDelegate error on read file: " + path) + } + } + try { + if (FS.trackingDelegate["onOpenFile"]) { + var trackingFlags = 0; + if ((flags & 2097155) !== 1) { + trackingFlags |= FS.tracking.openFlags.READ + } + if ((flags & 2097155) !== 0) { + trackingFlags |= FS.tracking.openFlags.WRITE + } + FS.trackingDelegate["onOpenFile"](path, trackingFlags) + } + } catch (e) { + console.log("FS.trackingDelegate['onOpenFile']('" + path + "', flags) threw an exception: " + e.message) + } + return stream + }, + close: function(stream) { + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8) + } + if (stream.getdents) stream.getdents = null; + try { + if (stream.stream_ops.close) { + stream.stream_ops.close(stream) + } + } catch (e) { + throw e + } finally { + FS.closeStream(stream.fd) + } + stream.fd = null + }, + isClosed: function(stream) { + return stream.fd === null + }, + llseek: function(stream, offset, whence) { + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8) + } + if (!stream.seekable || !stream.stream_ops.llseek) { + throw new FS.ErrnoError(70) + } + if (whence != 0 && whence != 1 && whence != 2) { + throw new FS.ErrnoError(28) + } + stream.position = stream.stream_ops.llseek(stream, offset, whence); + stream.ungotten = []; + return stream.position + }, + read: function(stream, buffer, offset, length, position) { + if (length < 0 || position < 0) { + throw new FS.ErrnoError(28) + } + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8) + } + if ((stream.flags & 2097155) === 1) { + throw new FS.ErrnoError(8) + } + if (FS.isDir(stream.node.mode)) { + throw new FS.ErrnoError(31) + } + if (!stream.stream_ops.read) { + throw new FS.ErrnoError(28) + } + var seeking = typeof position !== "undefined"; + if (!seeking) { + position = stream.position + } else if (!stream.seekable) { + throw new FS.ErrnoError(70) + } + var bytesRead = stream.stream_ops.read(stream, buffer, offset, length, position); + if (!seeking) stream.position += bytesRead; + return bytesRead + }, + write: function(stream, buffer, offset, length, position, canOwn) { + if (length < 0 || position < 0) { + throw new FS.ErrnoError(28) + } + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8) + } + if ((stream.flags & 2097155) === 0) { + throw new FS.ErrnoError(8) + } + if (FS.isDir(stream.node.mode)) { + throw new FS.ErrnoError(31) + } + if (!stream.stream_ops.write) { + throw new FS.ErrnoError(28) + } + if (stream.flags & 1024) { + FS.llseek(stream, 0, 2) + } + var seeking = typeof position !== "undefined"; + if (!seeking) { + position = stream.position + } else if (!stream.seekable) { + throw new FS.ErrnoError(70) + } + var bytesWritten = stream.stream_ops.write(stream, buffer, offset, length, position, canOwn); + if (!seeking) stream.position += bytesWritten; + try { + if (stream.path && FS.trackingDelegate["onWriteToFile"]) FS.trackingDelegate["onWriteToFile"](stream.path) + } catch (e) { + console.log("FS.trackingDelegate['onWriteToFile']('" + stream.path + "') threw an exception: " + e.message) + } + return bytesWritten + }, + allocate: function(stream, offset, length) { + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8) + } + if (offset < 0 || length <= 0) { + throw new FS.ErrnoError(28) + } + if ((stream.flags & 2097155) === 0) { + throw new FS.ErrnoError(8) + } + if (!FS.isFile(stream.node.mode) && !FS.isDir(stream.node.mode)) { + throw new FS.ErrnoError(43) + } + if (!stream.stream_ops.allocate) { + throw new FS.ErrnoError(138) + } + stream.stream_ops.allocate(stream, offset, length) + }, + mmap: function(stream, buffer, offset, length, position, prot, flags) { + if ((prot & 2) !== 0 && (flags & 2) === 0 && (stream.flags & 2097155) !== 2) { + throw new FS.ErrnoError(2) + } + if ((stream.flags & 2097155) === 1) { + throw new FS.ErrnoError(2) + } + if (!stream.stream_ops.mmap) { + throw new FS.ErrnoError(43) + } + return stream.stream_ops.mmap(stream, buffer, offset, length, position, prot, flags) + }, + msync: function(stream, buffer, offset, length, mmapFlags) { + if (!stream || !stream.stream_ops.msync) { + return 0 + } + return stream.stream_ops.msync(stream, buffer, offset, length, mmapFlags) + }, + munmap: function(stream) { + return 0 + }, + ioctl: function(stream, cmd, arg) { + if (!stream.stream_ops.ioctl) { + throw new FS.ErrnoError(59) + } + return stream.stream_ops.ioctl(stream, cmd, arg) + }, + readFile: function(path, opts) { + opts = opts || {}; + opts.flags = opts.flags || "r"; + opts.encoding = opts.encoding || "binary"; + if (opts.encoding !== "utf8" && opts.encoding !== "binary") { + throw new Error('Invalid encoding type "' + opts.encoding + '"') + } + var ret; + var stream = FS.open(path, opts.flags); + var stat = FS.stat(path); + var length = stat.size; + var buf = new Uint8Array(length); + FS.read(stream, buf, 0, length, 0); + if (opts.encoding === "utf8") { + ret = UTF8ArrayToString(buf, 0) + } else if (opts.encoding === "binary") { + ret = buf + } + FS.close(stream); + return ret + }, + writeFile: function(path, data, opts) { + opts = opts || {}; + opts.flags = opts.flags || "w"; + var stream = FS.open(path, opts.flags, opts.mode); + if (typeof data === "string") { + var buf = new Uint8Array(lengthBytesUTF8(data) + 1); + var actualNumBytes = stringToUTF8Array(data, buf, 0, buf.length); + FS.write(stream, buf, 0, actualNumBytes, undefined, opts.canOwn) + } else if (ArrayBuffer.isView(data)) { + FS.write(stream, data, 0, data.byteLength, undefined, opts.canOwn) + } else { + throw new Error("Unsupported data type") + } + FS.close(stream) + }, + cwd: function() { + return FS.currentPath + }, + chdir: function(path) { + var lookup = FS.lookupPath(path, { + follow: true + }); + if (lookup.node === null) { + throw new FS.ErrnoError(44) + } + if (!FS.isDir(lookup.node.mode)) { + throw new FS.ErrnoError(54) + } + var err = FS.nodePermissions(lookup.node, "x"); + if (err) { + throw new FS.ErrnoError(err) + } + FS.currentPath = lookup.path + }, + createDefaultDirectories: function() { + FS.mkdir("/tmp"); + FS.mkdir("/home"); + FS.mkdir("/home/web_user") + }, + createDefaultDevices: function() { + FS.mkdir("/dev"); + FS.registerDevice(FS.makedev(1, 3), { + read: function() { + return 0 + }, + write: function(stream, buffer, offset, length, pos) { + return length + } + }); + FS.mkdev("/dev/null", FS.makedev(1, 3)); + TTY.register(FS.makedev(5, 0), TTY.default_tty_ops); + TTY.register(FS.makedev(6, 0), TTY.default_tty1_ops); + FS.mkdev("/dev/tty", FS.makedev(5, 0)); + FS.mkdev("/dev/tty1", FS.makedev(6, 0)); + var random_device; + if (typeof crypto === "object" && typeof crypto["getRandomValues"] === "function") { + var randomBuffer = new Uint8Array(1); + random_device = function() { + crypto.getRandomValues(randomBuffer); + return randomBuffer[0] + } + } else if (ENVIRONMENT_IS_NODE) { + try { + var crypto_module = require("crypto"); + random_device = function() { + return crypto_module["randomBytes"](1)[0] + } + } catch (e) {} + } else {} + if (!random_device) { + random_device = function() { + abort("no cryptographic support found for random_device. consider polyfilling it if you want to use something insecure like Math.random(), e.g. put this in a --pre-js: var crypto = { getRandomValues: function(array) { for (var i = 0; i < array.length; i++) array[i] = (Math.random()*256)|0 } };") + } + } + FS.createDevice("/dev", "random", random_device); + FS.createDevice("/dev", "urandom", random_device); + FS.mkdir("/dev/shm"); + FS.mkdir("/dev/shm/tmp") + }, + createSpecialDirectories: function() { + FS.mkdir("/proc"); + FS.mkdir("/proc/self"); + FS.mkdir("/proc/self/fd"); + FS.mount({ + mount: function() { + var node = FS.createNode("/proc/self", "fd", 16384 | 511, 73); + node.node_ops = { + lookup: function(parent, name) { + var fd = +name; + var stream = FS.getStream(fd); + if (!stream) throw new FS.ErrnoError(8); + var ret = { + parent: null, + mount: { + mountpoint: "fake" + }, + node_ops: { + readlink: function() { + return stream.path + } + } + }; + ret.parent = ret; + return ret + } + }; + return node + } + }, {}, "/proc/self/fd") + }, + createStandardStreams: function() { + if (Module["stdin"]) { + FS.createDevice("/dev", "stdin", Module["stdin"]) + } else { + FS.symlink("/dev/tty", "/dev/stdin") + } + if (Module["stdout"]) { + FS.createDevice("/dev", "stdout", null, Module["stdout"]) + } else { + FS.symlink("/dev/tty", "/dev/stdout") + } + if (Module["stderr"]) { + FS.createDevice("/dev", "stderr", null, Module["stderr"]) + } else { + FS.symlink("/dev/tty1", "/dev/stderr") + } + var stdin = FS.open("/dev/stdin", "r"); + var stdout = FS.open("/dev/stdout", "w"); + var stderr = FS.open("/dev/stderr", "w"); + assert(stdin.fd === 0, "invalid handle for stdin (" + stdin.fd + ")"); + assert(stdout.fd === 1, "invalid handle for stdout (" + stdout.fd + ")"); + assert(stderr.fd === 2, "invalid handle for stderr (" + stderr.fd + ")") + }, + ensureErrnoError: function() { + if (FS.ErrnoError) return; + FS.ErrnoError = function ErrnoError(errno, node) { + this.node = node; + this.setErrno = function(errno) { + this.errno = errno; + for (var key in ERRNO_CODES) { + if (ERRNO_CODES[key] === errno) { + this.code = key; + break + } + } + }; + this.setErrno(errno); + this.message = ERRNO_MESSAGES[errno]; + if (this.stack) { + Object.defineProperty(this, "stack", { + value: (new Error).stack, + writable: true + }); + this.stack = demangleAll(this.stack) + } + }; + FS.ErrnoError.prototype = new Error; + FS.ErrnoError.prototype.constructor = FS.ErrnoError; + [44].forEach(function(code) { + FS.genericErrors[code] = new FS.ErrnoError(code); + FS.genericErrors[code].stack = "" + }) + }, + staticInit: function() { + FS.ensureErrnoError(); + FS.nameTable = new Array(4096); + FS.mount(MEMFS, {}, "/"); + FS.createDefaultDirectories(); + FS.createDefaultDevices(); + FS.createSpecialDirectories(); + FS.filesystems = { + "MEMFS": MEMFS, + "IDBFS": IDBFS, + "NODEFS": NODEFS, + "WORKERFS": WORKERFS + } + }, + init: function(input, output, error) { + assert(!FS.init.initialized, "FS.init was previously called. If you want to initialize later with custom parameters, remove any earlier calls (note that one is automatically added to the generated code)"); + FS.init.initialized = true; + FS.ensureErrnoError(); + Module["stdin"] = input || Module["stdin"]; + Module["stdout"] = output || Module["stdout"]; + Module["stderr"] = error || Module["stderr"]; + FS.createStandardStreams() + }, + quit: function() { + FS.init.initialized = false; + var fflush = Module["_fflush"]; + if (fflush) fflush(0); + for (var i = 0; i < FS.streams.length; i++) { + var stream = FS.streams[i]; + if (!stream) { + continue + } + FS.close(stream) + } + }, + getMode: function(canRead, canWrite) { + var mode = 0; + if (canRead) mode |= 292 | 73; + if (canWrite) mode |= 146; + return mode + }, + joinPath: function(parts, forceRelative) { + var path = PATH.join.apply(null, parts); + if (forceRelative && path[0] == "/") path = path.substr(1); + return path + }, + absolutePath: function(relative, base) { + return PATH_FS.resolve(base, relative) + }, + standardizePath: function(path) { + return PATH.normalize(path) + }, + findObject: function(path, dontResolveLastLink) { + var ret = FS.analyzePath(path, dontResolveLastLink); + if (ret.exists) { + return ret.object + } else { + ___setErrNo(ret.error); + return null + } + }, + analyzePath: function(path, dontResolveLastLink) { + try { + var lookup = FS.lookupPath(path, { + follow: !dontResolveLastLink + }); + path = lookup.path + } catch (e) {} + var ret = { + isRoot: false, + exists: false, + error: 0, + name: null, + path: null, + object: null, + parentExists: false, + parentPath: null, + parentObject: null + }; + try { + var lookup = FS.lookupPath(path, { + parent: true + }); + ret.parentExists = true; + ret.parentPath = lookup.path; + ret.parentObject = lookup.node; + ret.name = PATH.basename(path); + lookup = FS.lookupPath(path, { + follow: !dontResolveLastLink + }); + ret.exists = true; + ret.path = lookup.path; + ret.object = lookup.node; + ret.name = lookup.node.name; + ret.isRoot = lookup.path === "/" + } catch (e) { + ret.error = e.errno + } + return ret + }, + createFolder: function(parent, name, canRead, canWrite) { + var path = PATH.join2(typeof parent === "string" ? parent : FS.getPath(parent), name); + var mode = FS.getMode(canRead, canWrite); + return FS.mkdir(path, mode) + }, + createPath: function(parent, path, canRead, canWrite) { + parent = typeof parent === "string" ? parent : FS.getPath(parent); + var parts = path.split("/").reverse(); + while (parts.length) { + var part = parts.pop(); + if (!part) continue; + var current = PATH.join2(parent, part); + try { + FS.mkdir(current) + } catch (e) {} + parent = current + } + return current + }, + createFile: function(parent, name, properties, canRead, canWrite) { + var path = PATH.join2(typeof parent === "string" ? parent : FS.getPath(parent), name); + var mode = FS.getMode(canRead, canWrite); + return FS.create(path, mode) + }, + createDataFile: function(parent, name, data, canRead, canWrite, canOwn) { + var path = name ? PATH.join2(typeof parent === "string" ? parent : FS.getPath(parent), name) : parent; + var mode = FS.getMode(canRead, canWrite); + var node = FS.create(path, mode); + if (data) { + if (typeof data === "string") { + var arr = new Array(data.length); + for (var i = 0, len = data.length; i < len; ++i) arr[i] = data.charCodeAt(i); + data = arr + } + FS.chmod(node, mode | 146); + var stream = FS.open(node, "w"); + FS.write(stream, data, 0, data.length, 0, canOwn); + FS.close(stream); + FS.chmod(node, mode) + } + return node + }, + createDevice: function(parent, name, input, output) { + var path = PATH.join2(typeof parent === "string" ? parent : FS.getPath(parent), name); + var mode = FS.getMode(!!input, !!output); + if (!FS.createDevice.major) FS.createDevice.major = 64; + var dev = FS.makedev(FS.createDevice.major++, 0); + FS.registerDevice(dev, { + open: function(stream) { + stream.seekable = false + }, + close: function(stream) { + if (output && output.buffer && output.buffer.length) { + output(10) + } + }, + read: function(stream, buffer, offset, length, pos) { + var bytesRead = 0; + for (var i = 0; i < length; i++) { + var result; + try { + result = input() + } catch (e) { + throw new FS.ErrnoError(29) + } + if (result === undefined && bytesRead === 0) { + throw new FS.ErrnoError(6) + } + if (result === null || result === undefined) break; + bytesRead++; + buffer[offset + i] = result + } + if (bytesRead) { + stream.node.timestamp = Date.now() + } + return bytesRead + }, + write: function(stream, buffer, offset, length, pos) { + for (var i = 0; i < length; i++) { + try { + output(buffer[offset + i]) + } catch (e) { + throw new FS.ErrnoError(29) + } + } + if (length) { + stream.node.timestamp = Date.now() + } + return i + } + }); + return FS.mkdev(path, mode, dev) + }, + createLink: function(parent, name, target, canRead, canWrite) { + var path = PATH.join2(typeof parent === "string" ? parent : FS.getPath(parent), name); + return FS.symlink(target, path) + }, + forceLoadFile: function(obj) { + if (obj.isDevice || obj.isFolder || obj.link || obj.contents) return true; + var success = true; + if (typeof XMLHttpRequest !== "undefined") { + throw new Error("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread.") + } else if (read_) { + try { + obj.contents = intArrayFromString(read_(obj.url), true); + obj.usedBytes = obj.contents.length + } catch (e) { + success = false + } + } else { + throw new Error("Cannot load without read() or XMLHttpRequest.") + } + if (!success) ___setErrNo(29); + return success + }, + createLazyFile: function(parent, name, url, canRead, canWrite) { + function LazyUint8Array() { + this.lengthKnown = false; + this.chunks = [] + } + LazyUint8Array.prototype.get = function LazyUint8Array_get(idx) { + if (idx > this.length - 1 || idx < 0) { + return undefined + } + var chunkOffset = idx % this.chunkSize; + var chunkNum = idx / this.chunkSize | 0; + return this.getter(chunkNum)[chunkOffset] + }; + LazyUint8Array.prototype.setDataGetter = function LazyUint8Array_setDataGetter(getter) { + this.getter = getter + }; + LazyUint8Array.prototype.cacheLength = function LazyUint8Array_cacheLength() { + var xhr = new XMLHttpRequest; + xhr.open("HEAD", url, false); + xhr.send(null); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status); + var datalength = Number(xhr.getResponseHeader("Content-length")); + var header; + var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes"; + var usesGzip = (header = xhr.getResponseHeader("Content-Encoding")) && header === "gzip"; + var chunkSize = 1024 * 1024; + if (!hasByteServing) chunkSize = datalength; + var doXHR = function(from, to) { + if (from > to) throw new Error("invalid range (" + from + ", " + to + ") or no bytes requested!"); + if (to > datalength - 1) throw new Error("only " + datalength + " bytes available! programmer error!"); + var xhr = new XMLHttpRequest; + xhr.open("GET", url, false); + if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to); + if (typeof Uint8Array != "undefined") xhr.responseType = "arraybuffer"; + if (xhr.overrideMimeType) { + xhr.overrideMimeType("text/plain; charset=x-user-defined") + } + xhr.send(null); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status); + if (xhr.response !== undefined) { + return new Uint8Array(xhr.response || []) + } else { + return intArrayFromString(xhr.responseText || "", true) + } + }; + var lazyArray = this; + lazyArray.setDataGetter(function(chunkNum) { + var start = chunkNum * chunkSize; + var end = (chunkNum + 1) * chunkSize - 1; + end = Math.min(end, datalength - 1); + if (typeof lazyArray.chunks[chunkNum] === "undefined") { + lazyArray.chunks[chunkNum] = doXHR(start, end) + } + if (typeof lazyArray.chunks[chunkNum] === "undefined") throw new Error("doXHR failed!"); + return lazyArray.chunks[chunkNum] + }); + if (usesGzip || !datalength) { + chunkSize = datalength = 1; + datalength = this.getter(0).length; + chunkSize = datalength; + console.log("LazyFiles on gzip forces download of the whole file when length is accessed") + } + this._length = datalength; + this._chunkSize = chunkSize; + this.lengthKnown = true + }; + if (typeof XMLHttpRequest !== "undefined") { + if (!ENVIRONMENT_IS_WORKER) throw "Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc"; + var lazyArray = new LazyUint8Array; + Object.defineProperties(lazyArray, { + length: { + get: function() { + if (!this.lengthKnown) { + this.cacheLength() + } + return this._length + } + }, + chunkSize: { + get: function() { + if (!this.lengthKnown) { + this.cacheLength() + } + return this._chunkSize + } + } + }); + var properties = { + isDevice: false, + contents: lazyArray + } + } else { + var properties = { + isDevice: false, + url: url + } + } + var node = FS.createFile(parent, name, properties, canRead, canWrite); + if (properties.contents) { + node.contents = properties.contents + } else if (properties.url) { + node.contents = null; + node.url = properties.url + } + Object.defineProperties(node, { + usedBytes: { + get: function() { + return this.contents.length + } + } + }); + var stream_ops = {}; + var keys = Object.keys(node.stream_ops); + keys.forEach(function(key) { + var fn = node.stream_ops[key]; + stream_ops[key] = function forceLoadLazyFile() { + if (!FS.forceLoadFile(node)) { + throw new FS.ErrnoError(29) + } + return fn.apply(null, arguments) + } + }); + stream_ops.read = function stream_ops_read(stream, buffer, offset, length, position) { + if (!FS.forceLoadFile(node)) { + throw new FS.ErrnoError(29) + } + var contents = stream.node.contents; + if (position >= contents.length) return 0; + var size = Math.min(contents.length - position, length); + assert(size >= 0); + if (contents.slice) { + for (var i = 0; i < size; i++) { + buffer[offset + i] = contents[position + i] + } + } else { + for (var i = 0; i < size; i++) { + buffer[offset + i] = contents.get(position + i) + } + } + return size + }; + node.stream_ops = stream_ops; + return node + }, + createPreloadedFile: function(parent, name, url, canRead, canWrite, onload, onerror, dontCreateFile, canOwn, preFinish) { + Browser.init(); + var fullname = name ? PATH_FS.resolve(PATH.join2(parent, name)) : parent; + var dep = getUniqueRunDependency("cp " + fullname); + + function processData(byteArray) { + function finish(byteArray) { + if (preFinish) preFinish(); + if (!dontCreateFile) { + FS.createDataFile(parent, name, byteArray, canRead, canWrite, canOwn) + } + if (onload) onload(); + removeRunDependency(dep) + } + var handled = false; + Module["preloadPlugins"].forEach(function(plugin) { + if (handled) return; + if (plugin["canHandle"](fullname)) { + plugin["handle"](byteArray, fullname, finish, function() { + if (onerror) onerror(); + removeRunDependency(dep) + }); + handled = true + } + }); + if (!handled) finish(byteArray) + } + addRunDependency(dep); + if (typeof url == "string") { + Browser.asyncLoad(url, function(byteArray) { + processData(byteArray) + }, onerror) + } else { + processData(url) + } + }, + indexedDB: function() { + return window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB + }, + DB_NAME: function() { + return "EM_FS_" + window.location.pathname + }, + DB_VERSION: 20, + DB_STORE_NAME: "FILE_DATA", + saveFilesToDB: function(paths, onload, onerror) { + onload = onload || function() {}; + onerror = onerror || function() {}; + var indexedDB = FS.indexedDB(); + try { + var openRequest = indexedDB.open(FS.DB_NAME(), FS.DB_VERSION) + } catch (e) { + return onerror(e) + } + openRequest.onupgradeneeded = function openRequest_onupgradeneeded() { + console.log("creating db"); + var db = openRequest.result; + db.createObjectStore(FS.DB_STORE_NAME) + }; + openRequest.onsuccess = function openRequest_onsuccess() { + var db = openRequest.result; + var transaction = db.transaction([FS.DB_STORE_NAME], "readwrite"); + var files = transaction.objectStore(FS.DB_STORE_NAME); + var ok = 0, + fail = 0, + total = paths.length; + + function finish() { + if (fail == 0) onload(); + else onerror() + } + paths.forEach(function(path) { + var putRequest = files.put(FS.analyzePath(path).object.contents, path); + putRequest.onsuccess = function putRequest_onsuccess() { + ok++; + if (ok + fail == total) finish() + }; + putRequest.onerror = function putRequest_onerror() { + fail++; + if (ok + fail == total) finish() + } + }); + transaction.onerror = onerror + }; + openRequest.onerror = onerror + }, + loadFilesFromDB: function(paths, onload, onerror) { + onload = onload || function() {}; + onerror = onerror || function() {}; + var indexedDB = FS.indexedDB(); + try { + var openRequest = indexedDB.open(FS.DB_NAME(), FS.DB_VERSION) + } catch (e) { + return onerror(e) + } + openRequest.onupgradeneeded = onerror; + openRequest.onsuccess = function openRequest_onsuccess() { + var db = openRequest.result; + try { + var transaction = db.transaction([FS.DB_STORE_NAME], "readonly") + } catch (e) { + onerror(e); + return + } + var files = transaction.objectStore(FS.DB_STORE_NAME); + var ok = 0, + fail = 0, + total = paths.length; + + function finish() { + if (fail == 0) onload(); + else onerror() + } + paths.forEach(function(path) { + var getRequest = files.get(path); + getRequest.onsuccess = function getRequest_onsuccess() { + if (FS.analyzePath(path).exists) { + FS.unlink(path) + } + FS.createDataFile(PATH.dirname(path), PATH.basename(path), getRequest.result, true, true, true); + ok++; + if (ok + fail == total) finish() + }; + getRequest.onerror = function getRequest_onerror() { + fail++; + if (ok + fail == total) finish() + } + }); + transaction.onerror = onerror + }; + openRequest.onerror = onerror + } +}; +var SYSCALLS = { + DEFAULT_POLLMASK: 5, + mappings: {}, + umask: 511, + calculateAt: function(dirfd, path) { + if (path[0] !== "/") { + var dir; + if (dirfd === -100) { + dir = FS.cwd() + } else { + var dirstream = FS.getStream(dirfd); + if (!dirstream) throw new FS.ErrnoError(8); + dir = dirstream.path + } + path = PATH.join2(dir, path) + } + return path + }, + doStat: function(func, path, buf) { + try { + var stat = func(path) + } catch (e) { + if (e && e.node && PATH.normalize(path) !== PATH.normalize(FS.getPath(e.node))) { + return -54 + } + throw e + } + HEAP32[buf >> 2] = stat.dev; + HEAP32[buf + 4 >> 2] = 0; + HEAP32[buf + 8 >> 2] = stat.ino; + HEAP32[buf + 12 >> 2] = stat.mode; + HEAP32[buf + 16 >> 2] = stat.nlink; + HEAP32[buf + 20 >> 2] = stat.uid; + HEAP32[buf + 24 >> 2] = stat.gid; + HEAP32[buf + 28 >> 2] = stat.rdev; + HEAP32[buf + 32 >> 2] = 0; + tempI64 = [stat.size >>> 0, (tempDouble = stat.size, +Math_abs(tempDouble) >= 1 ? tempDouble > 0 ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 : ~~+Math_ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0)], HEAP32[buf + 40 >> 2] = tempI64[0], HEAP32[buf + 44 >> 2] = tempI64[1]; + HEAP32[buf + 48 >> 2] = 4096; + HEAP32[buf + 52 >> 2] = stat.blocks; + HEAP32[buf + 56 >> 2] = stat.atime.getTime() / 1e3 | 0; + HEAP32[buf + 60 >> 2] = 0; + HEAP32[buf + 64 >> 2] = stat.mtime.getTime() / 1e3 | 0; + HEAP32[buf + 68 >> 2] = 0; + HEAP32[buf + 72 >> 2] = stat.ctime.getTime() / 1e3 | 0; + HEAP32[buf + 76 >> 2] = 0; + tempI64 = [stat.ino >>> 0, (tempDouble = stat.ino, +Math_abs(tempDouble) >= 1 ? tempDouble > 0 ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 : ~~+Math_ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0)], HEAP32[buf + 80 >> 2] = tempI64[0], HEAP32[buf + 84 >> 2] = tempI64[1]; + return 0 + }, + doMsync: function(addr, stream, len, flags) { + var buffer = new Uint8Array(HEAPU8.subarray(addr, addr + len)); + FS.msync(stream, buffer, 0, len, flags) + }, + doMkdir: function(path, mode) { + path = PATH.normalize(path); + if (path[path.length - 1] === "/") path = path.substr(0, path.length - 1); + FS.mkdir(path, mode, 0); + return 0 + }, + doMknod: function(path, mode, dev) { + switch (mode & 61440) { + case 32768: + case 8192: + case 24576: + case 4096: + case 49152: + break; + default: + return -28 + } + FS.mknod(path, mode, dev); + return 0 + }, + doReadlink: function(path, buf, bufsize) { + if (bufsize <= 0) return -28; + var ret = FS.readlink(path); + var len = Math.min(bufsize, lengthBytesUTF8(ret)); + var endChar = HEAP8[buf + len]; + stringToUTF8(ret, buf, bufsize + 1); + HEAP8[buf + len] = endChar; + return len + }, + doAccess: function(path, amode) { + if (amode & ~7) { + return -28 + } + var node; + var lookup = FS.lookupPath(path, { + follow: true + }); + node = lookup.node; + if (!node) { + return -44 + } + var perms = ""; + if (amode & 4) perms += "r"; + if (amode & 2) perms += "w"; + if (amode & 1) perms += "x"; + if (perms && FS.nodePermissions(node, perms)) { + return -2 + } + return 0 + }, + doDup: function(path, flags, suggestFD) { + var suggest = FS.getStream(suggestFD); + if (suggest) FS.close(suggest); + return FS.open(path, flags, 0, suggestFD, suggestFD).fd + }, + doReadv: function(stream, iov, iovcnt, offset) { + var ret = 0; + for (var i = 0; i < iovcnt; i++) { + var ptr = HEAP32[iov + i * 8 >> 2]; + var len = HEAP32[iov + (i * 8 + 4) >> 2]; + var curr = FS.read(stream, HEAP8, ptr, len, offset); + if (curr < 0) return -1; + ret += curr; + if (curr < len) break + } + return ret + }, + doWritev: function(stream, iov, iovcnt, offset) { + var ret = 0; + for (var i = 0; i < iovcnt; i++) { + var ptr = HEAP32[iov + i * 8 >> 2]; + var len = HEAP32[iov + (i * 8 + 4) >> 2]; + var curr = FS.write(stream, HEAP8, ptr, len, offset); + if (curr < 0) return -1; + ret += curr + } + return ret + }, + varargs: 0, + get: function(varargs) { + SYSCALLS.varargs += 4; + var ret = HEAP32[SYSCALLS.varargs - 4 >> 2]; + return ret + }, + getStr: function() { + var ret = UTF8ToString(SYSCALLS.get()); + return ret + }, + getStreamFromFD: function(fd) { + if (fd === undefined) fd = SYSCALLS.get(); + var stream = FS.getStream(fd); + if (!stream) throw new FS.ErrnoError(8); + return stream + }, + get64: function() { + var low = SYSCALLS.get(), + high = SYSCALLS.get(); + if (low >= 0) assert(high === 0); + else assert(high === -1); + return low + }, + getZero: function() { + assert(SYSCALLS.get() === 0) + } +}; + +function ___syscall221(which, varargs) { + SYSCALLS.varargs = varargs; + try { + var stream = SYSCALLS.getStreamFromFD(), + cmd = SYSCALLS.get(); + switch (cmd) { + case 0: { + var arg = SYSCALLS.get(); + if (arg < 0) { + return -28 + } + var newStream; + newStream = FS.open(stream.path, stream.flags, 0, arg); + return newStream.fd + } + case 1: + case 2: + return 0; + case 3: + return stream.flags; + case 4: { + var arg = SYSCALLS.get(); + stream.flags |= arg; + return 0 + } + case 12: { + var arg = SYSCALLS.get(); + var offset = 0; + HEAP16[arg + offset >> 1] = 2; + return 0 + } + case 13: + case 14: + return 0; + case 16: + case 8: + return -28; + case 9: + ___setErrNo(28); + return -1; + default: { + return -28 + } + } + } catch (e) { + if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); + return -e.errno + } +} + +function ___syscall3(which, varargs) { + SYSCALLS.varargs = varargs; + try { + var stream = SYSCALLS.getStreamFromFD(), + buf = SYSCALLS.get(), + count = SYSCALLS.get(); + return FS.read(stream, HEAP8, buf, count) + } catch (e) { + if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); + return -e.errno + } +} + +function ___syscall5(which, varargs) { + SYSCALLS.varargs = varargs; + try { + var pathname = SYSCALLS.getStr(), + flags = SYSCALLS.get(), + mode = SYSCALLS.get(); + var stream = FS.open(pathname, flags, mode); + return stream.fd + } catch (e) { + if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); + return -e.errno + } +} + +function ___unlock() {} + +function _fd_close(fd) { + try { + var stream = SYSCALLS.getStreamFromFD(fd); + FS.close(stream); + return 0 + } catch (e) { + if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); + return e.errno + } +} + +function ___wasi_fd_close() { + return _fd_close.apply(null, arguments) +} + +function _fd_fdstat_get(fd, pbuf) { + try { + var stream = SYSCALLS.getStreamFromFD(fd); + var type = stream.tty ? 2 : FS.isDir(stream.mode) ? 3 : FS.isLink(stream.mode) ? 7 : 4; + HEAP8[pbuf >> 0] = type; + return 0 + } catch (e) { + if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); + return e.errno + } +} + +function ___wasi_fd_fdstat_get() { + return _fd_fdstat_get.apply(null, arguments) +} + +function _fd_seek(fd, offset_low, offset_high, whence, newOffset) { + try { + var stream = SYSCALLS.getStreamFromFD(fd); + var HIGH_OFFSET = 4294967296; + var offset = offset_high * HIGH_OFFSET + (offset_low >>> 0); + var DOUBLE_LIMIT = 9007199254740992; + if (offset <= -DOUBLE_LIMIT || offset >= DOUBLE_LIMIT) { + return -61 + } + FS.llseek(stream, offset, whence); + tempI64 = [stream.position >>> 0, (tempDouble = stream.position, +Math_abs(tempDouble) >= 1 ? tempDouble > 0 ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 : ~~+Math_ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0)], HEAP32[newOffset >> 2] = tempI64[0], HEAP32[newOffset + 4 >> 2] = tempI64[1]; + if (stream.getdents && offset === 0 && whence === 0) stream.getdents = null; + return 0 + } catch (e) { + if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); + return e.errno + } +} + +function ___wasi_fd_seek() { + return _fd_seek.apply(null, arguments) +} + +function _fd_write(fd, iov, iovcnt, pnum) { + try { + var stream = SYSCALLS.getStreamFromFD(fd); + var num = SYSCALLS.doWritev(stream, iov, iovcnt); + HEAP32[pnum >> 2] = num; + return 0 + } catch (e) { + if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); + return e.errno + } +} + +function ___wasi_fd_write() { + return _fd_write.apply(null, arguments) +} + +function __emscripten_fetch_free(id) { + delete Fetch.xhrs[id - 1] +} + +function _abort() { + abort() +} + +function _clock() { + if (_clock.start === undefined) _clock.start = Date.now(); + return (Date.now() - _clock.start) * (1e6 / 1e3) | 0 +} + +function _emscripten_get_now() { + abort() +} + +function _emscripten_get_now_is_monotonic() { + return 0 || ENVIRONMENT_IS_NODE || typeof dateNow !== "undefined" || typeof performance === "object" && performance && typeof performance["now"] === "function" +} + +function _clock_gettime(clk_id, tp) { + var now; + if (clk_id === 0) { + now = Date.now() + } else if (clk_id === 1 && _emscripten_get_now_is_monotonic()) { + now = _emscripten_get_now() + } else { + ___setErrNo(28); + return -1 + } + HEAP32[tp >> 2] = now / 1e3 | 0; + HEAP32[tp + 4 >> 2] = now % 1e3 * 1e3 * 1e3 | 0; + return 0 +} + +function _emscripten_get_heap_size() { + return HEAP8.length +} + +function _emscripten_is_main_browser_thread() { + return !ENVIRONMENT_IS_WORKER +} + +function abortOnCannotGrowMemory(requestedSize) { + abort("Cannot enlarge memory arrays to size " + requestedSize + " bytes (OOM). Either (1) compile with -s TOTAL_MEMORY=X with X higher than the current value " + HEAP8.length + ", (2) compile with -s ALLOW_MEMORY_GROWTH=1 which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0 ") +} + +function _emscripten_resize_heap(requestedSize) { + abortOnCannotGrowMemory(requestedSize) +} +var Fetch = { + xhrs: [], + setu64: function(addr, val) { + HEAPU32[addr >> 2] = val; + HEAPU32[addr + 4 >> 2] = val / 4294967296 | 0 + }, + openDatabase: function(dbname, dbversion, onsuccess, onerror) { + try { + var openRequest = indexedDB.open(dbname, dbversion) + } catch (e) { + return onerror(e) + } + openRequest.onupgradeneeded = function(event) { + var db = event.target.result; + if (db.objectStoreNames.contains("FILES")) { + db.deleteObjectStore("FILES") + } + db.createObjectStore("FILES") + }; + openRequest.onsuccess = function(event) { + onsuccess(event.target.result) + }; + openRequest.onerror = function(error) { + onerror(error) + } + }, + staticInit: function() { + var isMainThread = typeof ENVIRONMENT_IS_FETCH_WORKER === "undefined"; + var onsuccess = function(db) { + Fetch.dbInstance = db; + if (isMainThread) { + removeRunDependency("library_fetch_init") + } + }; + var onerror = function() { + Fetch.dbInstance = false; + if (isMainThread) { + removeRunDependency("library_fetch_init") + } + }; + Fetch.openDatabase("emscripten_filesystem", 1, onsuccess, onerror); + if (typeof ENVIRONMENT_IS_FETCH_WORKER === "undefined" || !ENVIRONMENT_IS_FETCH_WORKER) addRunDependency("library_fetch_init") + } +}; + +function __emscripten_fetch_xhr(fetch, onsuccess, onerror, onprogress, onreadystatechange) { + var url = HEAPU32[fetch + 8 >> 2]; + if (!url) { + onerror(fetch, 0, "no url specified!"); + return + } + var url_ = UTF8ToString(url); + var fetch_attr = fetch + 112; + var requestMethod = UTF8ToString(fetch_attr); + if (!requestMethod) requestMethod = "GET"; + var userData = HEAPU32[fetch_attr + 32 >> 2]; + var fetchAttributes = HEAPU32[fetch_attr + 52 >> 2]; + var timeoutMsecs = HEAPU32[fetch_attr + 56 >> 2]; + var withCredentials = !!HEAPU32[fetch_attr + 60 >> 2]; + var destinationPath = HEAPU32[fetch_attr + 64 >> 2]; + var userName = HEAPU32[fetch_attr + 68 >> 2]; + var password = HEAPU32[fetch_attr + 72 >> 2]; + var requestHeaders = HEAPU32[fetch_attr + 76 >> 2]; + var overriddenMimeType = HEAPU32[fetch_attr + 80 >> 2]; + var dataPtr = HEAPU32[fetch_attr + 84 >> 2]; + var dataLength = HEAPU32[fetch_attr + 88 >> 2]; + var fetchAttrLoadToMemory = !!(fetchAttributes & 1); + var fetchAttrStreamData = !!(fetchAttributes & 2); + var fetchAttrPersistFile = !!(fetchAttributes & 4); + var fetchAttrAppend = !!(fetchAttributes & 8); + var fetchAttrReplace = !!(fetchAttributes & 16); + var fetchAttrSynchronous = !!(fetchAttributes & 64); + var fetchAttrWaitable = !!(fetchAttributes & 128); + var userNameStr = userName ? UTF8ToString(userName) : undefined; + var passwordStr = password ? UTF8ToString(password) : undefined; + var overriddenMimeTypeStr = overriddenMimeType ? UTF8ToString(overriddenMimeType) : undefined; + var xhr = new XMLHttpRequest; + xhr.withCredentials = withCredentials; + xhr.open(requestMethod, url_, !fetchAttrSynchronous, userNameStr, passwordStr); + if (!fetchAttrSynchronous) xhr.timeout = timeoutMsecs; + xhr.url_ = url_; + assert(!fetchAttrStreamData, "streaming uses moz-chunked-arraybuffer which is no longer supported; TODO: rewrite using fetch()"); + xhr.responseType = "arraybuffer"; + if (overriddenMimeType) { + xhr.overrideMimeType(overriddenMimeTypeStr) + } + if (requestHeaders) { + for (;;) { + var key = HEAPU32[requestHeaders >> 2]; + if (!key) break; + var value = HEAPU32[requestHeaders + 4 >> 2]; + if (!value) break; + requestHeaders += 8; + var keyStr = UTF8ToString(key); + var valueStr = UTF8ToString(value); + xhr.setRequestHeader(keyStr, valueStr) + } + } + Fetch.xhrs.push(xhr); + var id = Fetch.xhrs.length; + HEAPU32[fetch + 0 >> 2] = id; + var data = dataPtr && dataLength ? HEAPU8.slice(dataPtr, dataPtr + dataLength) : null; + xhr.onload = function(e) { + var len = xhr.response ? xhr.response.byteLength : 0; + var ptr = 0; + var ptrLen = 0; + if (fetchAttrLoadToMemory && !fetchAttrStreamData) { + ptrLen = len; + ptr = _malloc(ptrLen); + HEAPU8.set(new Uint8Array(xhr.response), ptr) + } + HEAPU32[fetch + 12 >> 2] = ptr; + Fetch.setu64(fetch + 16, ptrLen); + Fetch.setu64(fetch + 24, 0); + if (len) { + Fetch.setu64(fetch + 32, len) + } + HEAPU16[fetch + 40 >> 1] = xhr.readyState; + if (xhr.readyState === 4 && xhr.status === 0) { + if (len > 0) xhr.status = 200; + else xhr.status = 404 + } + HEAPU16[fetch + 42 >> 1] = xhr.status; + if (xhr.statusText) stringToUTF8(xhr.statusText, fetch + 44, 64); + if (xhr.status >= 200 && xhr.status < 300) { + if (onsuccess) onsuccess(fetch, xhr, e) + } else { + if (onerror) onerror(fetch, xhr, e) + } + }; + xhr.onerror = function(e) { + var status = xhr.status; + if (xhr.readyState === 4 && status === 0) status = 404; + HEAPU32[fetch + 12 >> 2] = 0; + Fetch.setu64(fetch + 16, 0); + Fetch.setu64(fetch + 24, 0); + Fetch.setu64(fetch + 32, 0); + HEAPU16[fetch + 40 >> 1] = xhr.readyState; + HEAPU16[fetch + 42 >> 1] = status; + if (onerror) onerror(fetch, xhr, e) + }; + xhr.ontimeout = function(e) { + if (onerror) onerror(fetch, xhr, e) + }; + xhr.onprogress = function(e) { + var ptrLen = fetchAttrLoadToMemory && fetchAttrStreamData && xhr.response ? xhr.response.byteLength : 0; + var ptr = 0; + if (fetchAttrLoadToMemory && fetchAttrStreamData) { + ptr = _malloc(ptrLen); + HEAPU8.set(new Uint8Array(xhr.response), ptr) + } + HEAPU32[fetch + 12 >> 2] = ptr; + Fetch.setu64(fetch + 16, ptrLen); + Fetch.setu64(fetch + 24, e.loaded - ptrLen); + Fetch.setu64(fetch + 32, e.total); + HEAPU16[fetch + 40 >> 1] = xhr.readyState; + if (xhr.readyState >= 3 && xhr.status === 0 && e.loaded > 0) xhr.status = 200; + HEAPU16[fetch + 42 >> 1] = xhr.status; + if (xhr.statusText) stringToUTF8(xhr.statusText, fetch + 44, 64); + if (onprogress) onprogress(fetch, xhr, e) + }; + xhr.onreadystatechange = function(e) { + HEAPU16[fetch + 40 >> 1] = xhr.readyState; + if (xhr.readyState >= 2) { + HEAPU16[fetch + 42 >> 1] = xhr.status + } + if (onreadystatechange) onreadystatechange(fetch, xhr, e) + }; + try { + xhr.send(data) + } catch (e) { + if (onerror) onerror(fetch, xhr, e) + } +} + +function __emscripten_fetch_cache_data(db, fetch, data, onsuccess, onerror) { + if (!db) { + onerror(fetch, 0, "IndexedDB not available!"); + return + } + var fetch_attr = fetch + 112; + var destinationPath = HEAPU32[fetch_attr + 64 >> 2]; + if (!destinationPath) destinationPath = HEAPU32[fetch + 8 >> 2]; + var destinationPathStr = UTF8ToString(destinationPath); + try { + var transaction = db.transaction(["FILES"], "readwrite"); + var packages = transaction.objectStore("FILES"); + var putRequest = packages.put(data, destinationPathStr); + putRequest.onsuccess = function(event) { + HEAPU16[fetch + 40 >> 1] = 4; + HEAPU16[fetch + 42 >> 1] = 200; + stringToUTF8("OK", fetch + 44, 64); + onsuccess(fetch, 0, destinationPathStr) + }; + putRequest.onerror = function(error) { + HEAPU16[fetch + 40 >> 1] = 4; + HEAPU16[fetch + 42 >> 1] = 413; + stringToUTF8("Payload Too Large", fetch + 44, 64); + onerror(fetch, 0, error) + } + } catch (e) { + onerror(fetch, 0, e) + } +} + +function __emscripten_fetch_load_cached_data(db, fetch, onsuccess, onerror) { + if (!db) { + onerror(fetch, 0, "IndexedDB not available!"); + return + } + var fetch_attr = fetch + 112; + var path = HEAPU32[fetch_attr + 64 >> 2]; + if (!path) path = HEAPU32[fetch + 8 >> 2]; + var pathStr = UTF8ToString(path); + try { + var transaction = db.transaction(["FILES"], "readonly"); + var packages = transaction.objectStore("FILES"); + var getRequest = packages.get(pathStr); + getRequest.onsuccess = function(event) { + if (event.target.result) { + var value = event.target.result; + var len = value.byteLength || value.length; + var ptr = _malloc(len); + HEAPU8.set(new Uint8Array(value), ptr); + HEAPU32[fetch + 12 >> 2] = ptr; + Fetch.setu64(fetch + 16, len); + Fetch.setu64(fetch + 24, 0); + Fetch.setu64(fetch + 32, len); + HEAPU16[fetch + 40 >> 1] = 4; + HEAPU16[fetch + 42 >> 1] = 200; + stringToUTF8("OK", fetch + 44, 64); + onsuccess(fetch, 0, value) + } else { + HEAPU16[fetch + 40 >> 1] = 4; + HEAPU16[fetch + 42 >> 1] = 404; + stringToUTF8("Not Found", fetch + 44, 64); + onerror(fetch, 0, "no data") + } + }; + getRequest.onerror = function(error) { + HEAPU16[fetch + 40 >> 1] = 4; + HEAPU16[fetch + 42 >> 1] = 404; + stringToUTF8("Not Found", fetch + 44, 64); + onerror(fetch, 0, error) + } + } catch (e) { + onerror(fetch, 0, e) + } +} + +function __emscripten_fetch_delete_cached_data(db, fetch, onsuccess, onerror) { + if (!db) { + onerror(fetch, 0, "IndexedDB not available!"); + return + } + var fetch_attr = fetch + 112; + var path = HEAPU32[fetch_attr + 64 >> 2]; + if (!path) path = HEAPU32[fetch + 8 >> 2]; + var pathStr = UTF8ToString(path); + try { + var transaction = db.transaction(["FILES"], "readwrite"); + var packages = transaction.objectStore("FILES"); + var request = packages.delete(pathStr); + request.onsuccess = function(event) { + var value = event.target.result; + HEAPU32[fetch + 12 >> 2] = 0; + Fetch.setu64(fetch + 16, 0); + Fetch.setu64(fetch + 24, 0); + Fetch.setu64(fetch + 32, 0); + HEAPU16[fetch + 40 >> 1] = 4; + HEAPU16[fetch + 42 >> 1] = 200; + stringToUTF8("OK", fetch + 44, 64); + onsuccess(fetch, 0, value) + }; + request.onerror = function(error) { + HEAPU16[fetch + 40 >> 1] = 4; + HEAPU16[fetch + 42 >> 1] = 404; + stringToUTF8("Not Found", fetch + 44, 64); + onerror(fetch, 0, error) + } + } catch (e) { + onerror(fetch, 0, e) + } +} + +function _emscripten_start_fetch(fetch, successcb, errorcb, progresscb, readystatechangecb) { + if (typeof noExitRuntime !== "undefined") noExitRuntime = true; + var fetch_attr = fetch + 112; + var requestMethod = UTF8ToString(fetch_attr); + var onsuccess = HEAPU32[fetch_attr + 36 >> 2]; + var onerror = HEAPU32[fetch_attr + 40 >> 2]; + var onprogress = HEAPU32[fetch_attr + 44 >> 2]; + var onreadystatechange = HEAPU32[fetch_attr + 48 >> 2]; + var fetchAttributes = HEAPU32[fetch_attr + 52 >> 2]; + var fetchAttrLoadToMemory = !!(fetchAttributes & 1); + var fetchAttrStreamData = !!(fetchAttributes & 2); + var fetchAttrPersistFile = !!(fetchAttributes & 4); + var fetchAttrNoDownload = !!(fetchAttributes & 32); + var fetchAttrAppend = !!(fetchAttributes & 8); + var fetchAttrReplace = !!(fetchAttributes & 16); + var reportSuccess = function(fetch, xhr, e) { + if (onsuccess) dynCall_vi(onsuccess, fetch); + else if (successcb) successcb(fetch) + }; + var reportProgress = function(fetch, xhr, e) { + if (onprogress) dynCall_vi(onprogress, fetch); + else if (progresscb) progresscb(fetch) + }; + var reportError = function(fetch, xhr, e) { + if (onerror) dynCall_vi(onerror, fetch); + else if (errorcb) errorcb(fetch) + }; + var reportReadyStateChange = function(fetch, xhr, e) { + if (onreadystatechange) dynCall_vi(onreadystatechange, fetch); + else if (readystatechangecb) readystatechangecb(fetch) + }; + var performUncachedXhr = function(fetch, xhr, e) { + __emscripten_fetch_xhr(fetch, reportSuccess, reportError, reportProgress, reportReadyStateChange) + }; + var cacheResultAndReportSuccess = function(fetch, xhr, e) { + var storeSuccess = function(fetch, xhr, e) { + if (onsuccess) dynCall_vi(onsuccess, fetch); + else if (successcb) successcb(fetch) + }; + var storeError = function(fetch, xhr, e) { + if (onsuccess) dynCall_vi(onsuccess, fetch); + else if (successcb) successcb(fetch) + }; + __emscripten_fetch_cache_data(Fetch.dbInstance, fetch, xhr.response, storeSuccess, storeError) + }; + var performCachedXhr = function(fetch, xhr, e) { + __emscripten_fetch_xhr(fetch, cacheResultAndReportSuccess, reportError, reportProgress, reportReadyStateChange) + }; + if (requestMethod === "EM_IDB_STORE") { + var ptr = HEAPU32[fetch_attr + 84 >> 2]; + __emscripten_fetch_cache_data(Fetch.dbInstance, fetch, HEAPU8.slice(ptr, ptr + HEAPU32[fetch_attr + 88 >> 2]), reportSuccess, reportError) + } else if (requestMethod === "EM_IDB_DELETE") { + __emscripten_fetch_delete_cached_data(Fetch.dbInstance, fetch, reportSuccess, reportError) + } else if (!fetchAttrReplace) { + __emscripten_fetch_load_cached_data(Fetch.dbInstance, fetch, reportSuccess, fetchAttrNoDownload ? reportError : fetchAttrPersistFile ? performCachedXhr : performUncachedXhr) + } else if (!fetchAttrNoDownload) { + __emscripten_fetch_xhr(fetch, fetchAttrPersistFile ? cacheResultAndReportSuccess : reportSuccess, reportError, reportProgress, reportReadyStateChange) + } else { + return 0 + } + return fetch +} +var _fabs = Math_abs; + +function _getenv(name) { + if (name === 0) return 0; + name = UTF8ToString(name); + if (!ENV.hasOwnProperty(name)) return 0; + if (_getenv.ret) _free(_getenv.ret); + _getenv.ret = allocateUTF8(ENV[name]); + return _getenv.ret +} + +function _gettimeofday(ptr) { + var now = Date.now(); + HEAP32[ptr >> 2] = now / 1e3 | 0; + HEAP32[ptr + 4 >> 2] = now % 1e3 * 1e3 | 0; + return 0 +} +var ___tm_timezone = (stringToUTF8("GMT", 1398096, 4), 1398096); + +function _gmtime_r(time, tmPtr) { + var date = new Date(HEAP32[time >> 2] * 1e3); + HEAP32[tmPtr >> 2] = date.getUTCSeconds(); + HEAP32[tmPtr + 4 >> 2] = date.getUTCMinutes(); + HEAP32[tmPtr + 8 >> 2] = date.getUTCHours(); + HEAP32[tmPtr + 12 >> 2] = date.getUTCDate(); + HEAP32[tmPtr + 16 >> 2] = date.getUTCMonth(); + HEAP32[tmPtr + 20 >> 2] = date.getUTCFullYear() - 1900; + HEAP32[tmPtr + 24 >> 2] = date.getUTCDay(); + HEAP32[tmPtr + 36 >> 2] = 0; + HEAP32[tmPtr + 32 >> 2] = 0; + var start = Date.UTC(date.getUTCFullYear(), 0, 1, 0, 0, 0, 0); + var yday = (date.getTime() - start) / (1e3 * 60 * 60 * 24) | 0; + HEAP32[tmPtr + 28 >> 2] = yday; + HEAP32[tmPtr + 40 >> 2] = ___tm_timezone; + return tmPtr +} + +function _llvm_exp2_f32(x) { + return Math.pow(2, x) +} + +function _llvm_exp2_f64(a0) { + return _llvm_exp2_f32(a0) +} + +function _llvm_log2_f32(x) { + return Math.log(x) / Math.LN2 +} + +function _llvm_stackrestore(p) { + var self = _llvm_stacksave; + var ret = self.LLVM_SAVEDSTACKS[p]; + self.LLVM_SAVEDSTACKS.splice(p, 1); + stackRestore(ret) +} + +function _llvm_stacksave() { + var self = _llvm_stacksave; + if (!self.LLVM_SAVEDSTACKS) { + self.LLVM_SAVEDSTACKS = [] + } + self.LLVM_SAVEDSTACKS.push(stackSave()); + return self.LLVM_SAVEDSTACKS.length - 1 +} +var _llvm_trunc_f64 = Math_trunc; + +function _tzset() { + if (_tzset.called) return; + _tzset.called = true; + HEAP32[__get_timezone() >> 2] = (new Date).getTimezoneOffset() * 60; + var currentYear = (new Date).getFullYear(); + var winter = new Date(currentYear, 0, 1); + var summer = new Date(currentYear, 6, 1); + HEAP32[__get_daylight() >> 2] = Number(winter.getTimezoneOffset() != summer.getTimezoneOffset()); + + function extractZone(date) { + var match = date.toTimeString().match(/\(([A-Za-z ]+)\)$/); + return match ? match[1] : "GMT" + } + var winterName = extractZone(winter); + var summerName = extractZone(summer); + var winterNamePtr = allocate(intArrayFromString(winterName), "i8", ALLOC_NORMAL); + var summerNamePtr = allocate(intArrayFromString(summerName), "i8", ALLOC_NORMAL); + if (summer.getTimezoneOffset() < winter.getTimezoneOffset()) { + HEAP32[__get_tzname() >> 2] = winterNamePtr; + HEAP32[__get_tzname() + 4 >> 2] = summerNamePtr + } else { + HEAP32[__get_tzname() >> 2] = summerNamePtr; + HEAP32[__get_tzname() + 4 >> 2] = winterNamePtr + } +} + +function _localtime_r(time, tmPtr) { + _tzset(); + var date = new Date(HEAP32[time >> 2] * 1e3); + HEAP32[tmPtr >> 2] = date.getSeconds(); + HEAP32[tmPtr + 4 >> 2] = date.getMinutes(); + HEAP32[tmPtr + 8 >> 2] = date.getHours(); + HEAP32[tmPtr + 12 >> 2] = date.getDate(); + HEAP32[tmPtr + 16 >> 2] = date.getMonth(); + HEAP32[tmPtr + 20 >> 2] = date.getFullYear() - 1900; + HEAP32[tmPtr + 24 >> 2] = date.getDay(); + var start = new Date(date.getFullYear(), 0, 1); + var yday = (date.getTime() - start.getTime()) / (1e3 * 60 * 60 * 24) | 0; + HEAP32[tmPtr + 28 >> 2] = yday; + HEAP32[tmPtr + 36 >> 2] = -(date.getTimezoneOffset() * 60); + var summerOffset = new Date(date.getFullYear(), 6, 1).getTimezoneOffset(); + var winterOffset = start.getTimezoneOffset(); + var dst = (summerOffset != winterOffset && date.getTimezoneOffset() == Math.min(winterOffset, summerOffset)) | 0; + HEAP32[tmPtr + 32 >> 2] = dst; + var zonePtr = HEAP32[__get_tzname() + (dst ? 4 : 0) >> 2]; + HEAP32[tmPtr + 40 >> 2] = zonePtr; + return tmPtr +} + +function _emscripten_memcpy_big(dest, src, num) { + HEAPU8.set(HEAPU8.subarray(src, src + num), dest) +} + +function _usleep(useconds) { + var msec = useconds / 1e3; + if ((ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) && self["performance"] && self["performance"]["now"]) { + var start = self["performance"]["now"](); + while (self["performance"]["now"]() - start < msec) {} + } else { + var start = Date.now(); + while (Date.now() - start < msec) {} + } + return 0 +} +Module["_usleep"] = _usleep; + +function _nanosleep(rqtp, rmtp) { + if (rqtp === 0) { + ___setErrNo(28); + return -1 + } + var seconds = HEAP32[rqtp >> 2]; + var nanoseconds = HEAP32[rqtp + 4 >> 2]; + if (nanoseconds < 0 || nanoseconds > 999999999 || seconds < 0) { + ___setErrNo(28); + return -1 + } + if (rmtp !== 0) { + HEAP32[rmtp >> 2] = 0; + HEAP32[rmtp + 4 >> 2] = 0 + } + return _usleep(seconds * 1e6 + nanoseconds / 1e3) +} + +function _pthread_cond_destroy() { + return 0 +} + +function _pthread_cond_init() { + return 0 +} + +function _pthread_create() { + return 6 +} + +function _pthread_join() {} + +function __isLeapYear(year) { + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) +} + +function __arraySum(array, index) { + var sum = 0; + for (var i = 0; i <= index; sum += array[i++]); + return sum +} +var __MONTH_DAYS_LEAP = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; +var __MONTH_DAYS_REGULAR = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + +function __addDays(date, days) { + var newDate = new Date(date.getTime()); + while (days > 0) { + var leap = __isLeapYear(newDate.getFullYear()); + var currentMonth = newDate.getMonth(); + var daysInCurrentMonth = (leap ? __MONTH_DAYS_LEAP : __MONTH_DAYS_REGULAR)[currentMonth]; + if (days > daysInCurrentMonth - newDate.getDate()) { + days -= daysInCurrentMonth - newDate.getDate() + 1; + newDate.setDate(1); + if (currentMonth < 11) { + newDate.setMonth(currentMonth + 1) + } else { + newDate.setMonth(0); + newDate.setFullYear(newDate.getFullYear() + 1) + } + } else { + newDate.setDate(newDate.getDate() + days); + return newDate + } + } + return newDate +} + +function _strftime(s, maxsize, format, tm) { + var tm_zone = HEAP32[tm + 40 >> 2]; + var date = { + tm_sec: HEAP32[tm >> 2], + tm_min: HEAP32[tm + 4 >> 2], + tm_hour: HEAP32[tm + 8 >> 2], + tm_mday: HEAP32[tm + 12 >> 2], + tm_mon: HEAP32[tm + 16 >> 2], + tm_year: HEAP32[tm + 20 >> 2], + tm_wday: HEAP32[tm + 24 >> 2], + tm_yday: HEAP32[tm + 28 >> 2], + tm_isdst: HEAP32[tm + 32 >> 2], + tm_gmtoff: HEAP32[tm + 36 >> 2], + tm_zone: tm_zone ? UTF8ToString(tm_zone) : "" + }; + var pattern = UTF8ToString(format); + var EXPANSION_RULES_1 = { + "%c": "%a %b %d %H:%M:%S %Y", + "%D": "%m/%d/%y", + "%F": "%Y-%m-%d", + "%h": "%b", + "%r": "%I:%M:%S %p", + "%R": "%H:%M", + "%T": "%H:%M:%S", + "%x": "%m/%d/%y", + "%X": "%H:%M:%S", + "%Ec": "%c", + "%EC": "%C", + "%Ex": "%m/%d/%y", + "%EX": "%H:%M:%S", + "%Ey": "%y", + "%EY": "%Y", + "%Od": "%d", + "%Oe": "%e", + "%OH": "%H", + "%OI": "%I", + "%Om": "%m", + "%OM": "%M", + "%OS": "%S", + "%Ou": "%u", + "%OU": "%U", + "%OV": "%V", + "%Ow": "%w", + "%OW": "%W", + "%Oy": "%y" + }; + for (var rule in EXPANSION_RULES_1) { + pattern = pattern.replace(new RegExp(rule, "g"), EXPANSION_RULES_1[rule]) + } + var WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + var MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + + function leadingSomething(value, digits, character) { + var str = typeof value === "number" ? value.toString() : value || ""; + while (str.length < digits) { + str = character[0] + str + } + return str + } + + function leadingNulls(value, digits) { + return leadingSomething(value, digits, "0") + } + + function compareByDay(date1, date2) { + function sgn(value) { + return value < 0 ? -1 : value > 0 ? 1 : 0 + } + var compare; + if ((compare = sgn(date1.getFullYear() - date2.getFullYear())) === 0) { + if ((compare = sgn(date1.getMonth() - date2.getMonth())) === 0) { + compare = sgn(date1.getDate() - date2.getDate()) + } + } + return compare + } + + function getFirstWeekStartDate(janFourth) { + switch (janFourth.getDay()) { + case 0: + return new Date(janFourth.getFullYear() - 1, 11, 29); + case 1: + return janFourth; + case 2: + return new Date(janFourth.getFullYear(), 0, 3); + case 3: + return new Date(janFourth.getFullYear(), 0, 2); + case 4: + return new Date(janFourth.getFullYear(), 0, 1); + case 5: + return new Date(janFourth.getFullYear() - 1, 11, 31); + case 6: + return new Date(janFourth.getFullYear() - 1, 11, 30) + } + } + + function getWeekBasedYear(date) { + var thisDate = __addDays(new Date(date.tm_year + 1900, 0, 1), date.tm_yday); + var janFourthThisYear = new Date(thisDate.getFullYear(), 0, 4); + var janFourthNextYear = new Date(thisDate.getFullYear() + 1, 0, 4); + var firstWeekStartThisYear = getFirstWeekStartDate(janFourthThisYear); + var firstWeekStartNextYear = getFirstWeekStartDate(janFourthNextYear); + if (compareByDay(firstWeekStartThisYear, thisDate) <= 0) { + if (compareByDay(firstWeekStartNextYear, thisDate) <= 0) { + return thisDate.getFullYear() + 1 + } else { + return thisDate.getFullYear() + } + } else { + return thisDate.getFullYear() - 1 + } + } + var EXPANSION_RULES_2 = { + "%a": function(date) { + return WEEKDAYS[date.tm_wday].substring(0, 3) + }, + "%A": function(date) { + return WEEKDAYS[date.tm_wday] + }, + "%b": function(date) { + return MONTHS[date.tm_mon].substring(0, 3) + }, + "%B": function(date) { + return MONTHS[date.tm_mon] + }, + "%C": function(date) { + var year = date.tm_year + 1900; + return leadingNulls(year / 100 | 0, 2) + }, + "%d": function(date) { + return leadingNulls(date.tm_mday, 2) + }, + "%e": function(date) { + return leadingSomething(date.tm_mday, 2, " ") + }, + "%g": function(date) { + return getWeekBasedYear(date).toString().substring(2) + }, + "%G": function(date) { + return getWeekBasedYear(date) + }, + "%H": function(date) { + return leadingNulls(date.tm_hour, 2) + }, + "%I": function(date) { + var twelveHour = date.tm_hour; + if (twelveHour == 0) twelveHour = 12; + else if (twelveHour > 12) twelveHour -= 12; + return leadingNulls(twelveHour, 2) + }, + "%j": function(date) { + return leadingNulls(date.tm_mday + __arraySum(__isLeapYear(date.tm_year + 1900) ? __MONTH_DAYS_LEAP : __MONTH_DAYS_REGULAR, date.tm_mon - 1), 3) + }, + "%m": function(date) { + return leadingNulls(date.tm_mon + 1, 2) + }, + "%M": function(date) { + return leadingNulls(date.tm_min, 2) + }, + "%n": function() { + return "\n" + }, + "%p": function(date) { + if (date.tm_hour >= 0 && date.tm_hour < 12) { + return "AM" + } else { + return "PM" + } + }, + "%S": function(date) { + return leadingNulls(date.tm_sec, 2) + }, + "%t": function() { + return "\t" + }, + "%u": function(date) { + return date.tm_wday || 7 + }, + "%U": function(date) { + var janFirst = new Date(date.tm_year + 1900, 0, 1); + var firstSunday = janFirst.getDay() === 0 ? janFirst : __addDays(janFirst, 7 - janFirst.getDay()); + var endDate = new Date(date.tm_year + 1900, date.tm_mon, date.tm_mday); + if (compareByDay(firstSunday, endDate) < 0) { + var februaryFirstUntilEndMonth = __arraySum(__isLeapYear(endDate.getFullYear()) ? __MONTH_DAYS_LEAP : __MONTH_DAYS_REGULAR, endDate.getMonth() - 1) - 31; + var firstSundayUntilEndJanuary = 31 - firstSunday.getDate(); + var days = firstSundayUntilEndJanuary + februaryFirstUntilEndMonth + endDate.getDate(); + return leadingNulls(Math.ceil(days / 7), 2) + } + return compareByDay(firstSunday, janFirst) === 0 ? "01" : "00" + }, + "%V": function(date) { + var janFourthThisYear = new Date(date.tm_year + 1900, 0, 4); + var janFourthNextYear = new Date(date.tm_year + 1901, 0, 4); + var firstWeekStartThisYear = getFirstWeekStartDate(janFourthThisYear); + var firstWeekStartNextYear = getFirstWeekStartDate(janFourthNextYear); + var endDate = __addDays(new Date(date.tm_year + 1900, 0, 1), date.tm_yday); + if (compareByDay(endDate, firstWeekStartThisYear) < 0) { + return "53" + } + if (compareByDay(firstWeekStartNextYear, endDate) <= 0) { + return "01" + } + var daysDifference; + if (firstWeekStartThisYear.getFullYear() < date.tm_year + 1900) { + daysDifference = date.tm_yday + 32 - firstWeekStartThisYear.getDate() + } else { + daysDifference = date.tm_yday + 1 - firstWeekStartThisYear.getDate() + } + return leadingNulls(Math.ceil(daysDifference / 7), 2) + }, + "%w": function(date) { + return date.tm_wday + }, + "%W": function(date) { + var janFirst = new Date(date.tm_year, 0, 1); + var firstMonday = janFirst.getDay() === 1 ? janFirst : __addDays(janFirst, janFirst.getDay() === 0 ? 1 : 7 - janFirst.getDay() + 1); + var endDate = new Date(date.tm_year + 1900, date.tm_mon, date.tm_mday); + if (compareByDay(firstMonday, endDate) < 0) { + var februaryFirstUntilEndMonth = __arraySum(__isLeapYear(endDate.getFullYear()) ? __MONTH_DAYS_LEAP : __MONTH_DAYS_REGULAR, endDate.getMonth() - 1) - 31; + var firstMondayUntilEndJanuary = 31 - firstMonday.getDate(); + var days = firstMondayUntilEndJanuary + februaryFirstUntilEndMonth + endDate.getDate(); + return leadingNulls(Math.ceil(days / 7), 2) + } + return compareByDay(firstMonday, janFirst) === 0 ? "01" : "00" + }, + "%y": function(date) { + return (date.tm_year + 1900).toString().substring(2) + }, + "%Y": function(date) { + return date.tm_year + 1900 + }, + "%z": function(date) { + var off = date.tm_gmtoff; + var ahead = off >= 0; + off = Math.abs(off) / 60; + off = off / 60 * 100 + off % 60; + return (ahead ? "+" : "-") + String("0000" + off).slice(-4) + }, + "%Z": function(date) { + return date.tm_zone + }, + "%%": function() { + return "%" + } + }; + for (var rule in EXPANSION_RULES_2) { + if (pattern.indexOf(rule) >= 0) { + pattern = pattern.replace(new RegExp(rule, "g"), EXPANSION_RULES_2[rule](date)) + } + } + var bytes = intArrayFromString(pattern, false); + if (bytes.length > maxsize) { + return 0 + } + writeArrayToMemory(bytes, s); + return bytes.length - 1 +} + +function _sysconf(name) { + switch (name) { + case 30: + return PAGE_SIZE; + case 85: + var maxHeapSize = 2 * 1024 * 1024 * 1024 - 65536; + maxHeapSize = HEAPU8.length; + return maxHeapSize / PAGE_SIZE; + case 132: + case 133: + case 12: + case 137: + case 138: + case 15: + case 235: + case 16: + case 17: + case 18: + case 19: + case 20: + case 149: + case 13: + case 10: + case 236: + case 153: + case 9: + case 21: + case 22: + case 159: + case 154: + case 14: + case 77: + case 78: + case 139: + case 80: + case 81: + case 82: + case 68: + case 67: + case 164: + case 11: + case 29: + case 47: + case 48: + case 95: + case 52: + case 51: + case 46: + return 200809; + case 79: + return 0; + case 27: + case 246: + case 127: + case 128: + case 23: + case 24: + case 160: + case 161: + case 181: + case 182: + case 242: + case 183: + case 184: + case 243: + case 244: + case 245: + case 165: + case 178: + case 179: + case 49: + case 50: + case 168: + case 169: + case 175: + case 170: + case 171: + case 172: + case 97: + case 76: + case 32: + case 173: + case 35: + return -1; + case 176: + case 177: + case 7: + case 155: + case 8: + case 157: + case 125: + case 126: + case 92: + case 93: + case 129: + case 130: + case 131: + case 94: + case 91: + return 1; + case 74: + case 60: + case 69: + case 70: + case 4: + return 1024; + case 31: + case 42: + case 72: + return 32; + case 87: + case 26: + case 33: + return 2147483647; + case 34: + case 1: + return 47839; + case 38: + case 36: + return 99; + case 43: + case 37: + return 2048; + case 0: + return 2097152; + case 3: + return 65536; + case 28: + return 32768; + case 44: + return 32767; + case 75: + return 16384; + case 39: + return 1e3; + case 89: + return 700; + case 71: + return 256; + case 40: + return 255; + case 2: + return 100; + case 180: + return 64; + case 25: + return 20; + case 5: + return 16; + case 6: + return 6; + case 73: + return 4; + case 84: { + if (typeof navigator === "object") return navigator["hardwareConcurrency"] || 1; + return 1 + } + } + ___setErrNo(28); + return -1 +} + +function _time(ptr) { + var ret = Date.now() / 1e3 | 0; + if (ptr) { + HEAP32[ptr >> 2] = ret + } + return ret +} +FS.staticInit(); +if (ENVIRONMENT_HAS_NODE) { + var fs = require("fs"); + var NODEJS_PATH = require("path"); + NODEFS.staticInit() +} +if (ENVIRONMENT_IS_NODE) { + _emscripten_get_now = function _emscripten_get_now_actual() { + var t = process["hrtime"](); + return t[0] * 1e3 + t[1] / 1e6 + } +} else if (typeof dateNow !== "undefined") { + _emscripten_get_now = dateNow +} else if (typeof performance === "object" && performance && typeof performance["now"] === "function") { + _emscripten_get_now = function() { + return performance["now"]() + } +} else { + _emscripten_get_now = Date.now +} +Fetch.staticInit(); + +function intArrayFromString(stringy, dontAddNull, length) { + var len = length > 0 ? length : lengthBytesUTF8(stringy) + 1; + var u8array = new Array(len); + var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length); + if (dontAddNull) u8array.length = numBytesWritten; + return u8array +} +var debug_table_dd = [0, "jsCall_dd_0", "jsCall_dd_1", "jsCall_dd_2", "jsCall_dd_3", "jsCall_dd_4", "jsCall_dd_5", "jsCall_dd_6", "jsCall_dd_7", "jsCall_dd_8", "jsCall_dd_9", "jsCall_dd_10", "jsCall_dd_11", "jsCall_dd_12", "jsCall_dd_13", "jsCall_dd_14", "jsCall_dd_15", "jsCall_dd_16", "jsCall_dd_17", "jsCall_dd_18", "jsCall_dd_19", "jsCall_dd_20", "jsCall_dd_21", "jsCall_dd_22", "jsCall_dd_23", "jsCall_dd_24", "jsCall_dd_25", "jsCall_dd_26", "jsCall_dd_27", "jsCall_dd_28", "jsCall_dd_29", "jsCall_dd_30", "jsCall_dd_31", "jsCall_dd_32", "jsCall_dd_33", "jsCall_dd_34", "_sinh", "_cosh", "_tanh", "_sin", "_cos", "_tan", "_atan", "_asin", "_acos", "_exp", "_log", "_fabs", "_etime", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_did = [0, "jsCall_did_0", "jsCall_did_1", "jsCall_did_2", "jsCall_did_3", "jsCall_did_4", "jsCall_did_5", "jsCall_did_6", "jsCall_did_7", "jsCall_did_8", "jsCall_did_9", "jsCall_did_10", "jsCall_did_11", "jsCall_did_12", "jsCall_did_13", "jsCall_did_14", "jsCall_did_15", "jsCall_did_16", "jsCall_did_17", "jsCall_did_18", "jsCall_did_19", "jsCall_did_20", "jsCall_did_21", "jsCall_did_22", "jsCall_did_23", "jsCall_did_24", "jsCall_did_25", "jsCall_did_26", "jsCall_did_27", "jsCall_did_28", "jsCall_did_29", "jsCall_did_30", "jsCall_did_31", "jsCall_did_32", "jsCall_did_33", "jsCall_did_34", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_didd = [0, "jsCall_didd_0", "jsCall_didd_1", "jsCall_didd_2", "jsCall_didd_3", "jsCall_didd_4", "jsCall_didd_5", "jsCall_didd_6", "jsCall_didd_7", "jsCall_didd_8", "jsCall_didd_9", "jsCall_didd_10", "jsCall_didd_11", "jsCall_didd_12", "jsCall_didd_13", "jsCall_didd_14", "jsCall_didd_15", "jsCall_didd_16", "jsCall_didd_17", "jsCall_didd_18", "jsCall_didd_19", "jsCall_didd_20", "jsCall_didd_21", "jsCall_didd_22", "jsCall_didd_23", "jsCall_didd_24", "jsCall_didd_25", "jsCall_didd_26", "jsCall_didd_27", "jsCall_didd_28", "jsCall_didd_29", "jsCall_didd_30", "jsCall_didd_31", "jsCall_didd_32", "jsCall_didd_33", "jsCall_didd_34", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_fii = [0, "jsCall_fii_0", "jsCall_fii_1", "jsCall_fii_2", "jsCall_fii_3", "jsCall_fii_4", "jsCall_fii_5", "jsCall_fii_6", "jsCall_fii_7", "jsCall_fii_8", "jsCall_fii_9", "jsCall_fii_10", "jsCall_fii_11", "jsCall_fii_12", "jsCall_fii_13", "jsCall_fii_14", "jsCall_fii_15", "jsCall_fii_16", "jsCall_fii_17", "jsCall_fii_18", "jsCall_fii_19", "jsCall_fii_20", "jsCall_fii_21", "jsCall_fii_22", "jsCall_fii_23", "jsCall_fii_24", "jsCall_fii_25", "jsCall_fii_26", "jsCall_fii_27", "jsCall_fii_28", "jsCall_fii_29", "jsCall_fii_30", "jsCall_fii_31", "jsCall_fii_32", "jsCall_fii_33", "jsCall_fii_34", "_sbr_sum_square_c", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_fiii = [0, "jsCall_fiii_0", "jsCall_fiii_1", "jsCall_fiii_2", "jsCall_fiii_3", "jsCall_fiii_4", "jsCall_fiii_5", "jsCall_fiii_6", "jsCall_fiii_7", "jsCall_fiii_8", "jsCall_fiii_9", "jsCall_fiii_10", "jsCall_fiii_11", "jsCall_fiii_12", "jsCall_fiii_13", "jsCall_fiii_14", "jsCall_fiii_15", "jsCall_fiii_16", "jsCall_fiii_17", "jsCall_fiii_18", "jsCall_fiii_19", "jsCall_fiii_20", "jsCall_fiii_21", "jsCall_fiii_22", "jsCall_fiii_23", "jsCall_fiii_24", "jsCall_fiii_25", "jsCall_fiii_26", "jsCall_fiii_27", "jsCall_fiii_28", "jsCall_fiii_29", "jsCall_fiii_30", "jsCall_fiii_31", "jsCall_fiii_32", "jsCall_fiii_33", "jsCall_fiii_34", "_avpriv_scalarproduct_float_c", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_ii = [0, "jsCall_ii_0", "jsCall_ii_1", "jsCall_ii_2", "jsCall_ii_3", "jsCall_ii_4", "jsCall_ii_5", "jsCall_ii_6", "jsCall_ii_7", "jsCall_ii_8", "jsCall_ii_9", "jsCall_ii_10", "jsCall_ii_11", "jsCall_ii_12", "jsCall_ii_13", "jsCall_ii_14", "jsCall_ii_15", "jsCall_ii_16", "jsCall_ii_17", "jsCall_ii_18", "jsCall_ii_19", "jsCall_ii_20", "jsCall_ii_21", "jsCall_ii_22", "jsCall_ii_23", "jsCall_ii_24", "jsCall_ii_25", "jsCall_ii_26", "jsCall_ii_27", "jsCall_ii_28", "jsCall_ii_29", "jsCall_ii_30", "jsCall_ii_31", "jsCall_ii_32", "jsCall_ii_33", "jsCall_ii_34", "_avi_probe", "_avi_read_header", "_avi_read_close", "_av_default_item_name", "_ff_avio_child_class_next", "_flv_probe", "_flv_read_header", "_flv_read_close", "_live_flv_probe", "_h264_probe", "_ff_raw_video_read_header", "_hevc_probe", "_mpeg4video_probe", "_matroska_probe", "_matroska_read_header", "_matroska_read_close", "_mov_probe", "_mov_read_header", "_mov_read_close", "_mp3_read_probe", "_mp3_read_header", "_mpegps_probe", "_mpegps_read_header", "_mpegts_probe", "_mpegts_read_header", "_mpegts_read_close", "_mpegvideo_probe", "_format_to_name", "_format_child_class_next", "_get_category", "_pcm_read_header", "_urlcontext_to_name", "_ff_urlcontext_child_class_next", "_sws_context_to_name", "_ff_bsf_child_class_next", "_hevc_mp4toannexb_init", "_hevc_init_thread_copy", "_hevc_decode_init", "_hevc_decode_free", "_decode_init", "_context_to_name", "_codec_child_class_next", "_get_category_2911", "_pcm_decode_init", "_pcm_decode_close", "_aac_decode_init", "_aac_decode_close", "_init", "_context_to_name_6198", "_resample_flush", "___stdio_close", "___emscripten_stdout_close", "_releaseSniffStreamFunc", "_naluLListLengthFunc", "_hflv_releaseFunc", "_hflv_getBufferLength", "_g711_releaseFunc", "_g711_decodeVideoFrameFunc", "_g711_getBufferLength", "_initializeDecoderFunc", "__getFrame", "_closeVideoFunc", "_releaseFunc", "_initializeDemuxerFunc", "_getPacketFunc", "_releaseDemuxerFunc", "_io_short_seek", "_avio_rb16", "_avio_rl16", "_av_buffer_allocz", "_frame_worker_thread", "_av_buffer_alloc", "_thread_worker", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_iid = [0, "jsCall_iid_0", "jsCall_iid_1", "jsCall_iid_2", "jsCall_iid_3", "jsCall_iid_4", "jsCall_iid_5", "jsCall_iid_6", "jsCall_iid_7", "jsCall_iid_8", "jsCall_iid_9", "jsCall_iid_10", "jsCall_iid_11", "jsCall_iid_12", "jsCall_iid_13", "jsCall_iid_14", "jsCall_iid_15", "jsCall_iid_16", "jsCall_iid_17", "jsCall_iid_18", "jsCall_iid_19", "jsCall_iid_20", "jsCall_iid_21", "jsCall_iid_22", "jsCall_iid_23", "jsCall_iid_24", "jsCall_iid_25", "jsCall_iid_26", "jsCall_iid_27", "jsCall_iid_28", "jsCall_iid_29", "jsCall_iid_30", "jsCall_iid_31", "jsCall_iid_32", "jsCall_iid_33", "jsCall_iid_34", "_seekBufferFunc", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_iidiiii = [0, "jsCall_iidiiii_0", "jsCall_iidiiii_1", "jsCall_iidiiii_2", "jsCall_iidiiii_3", "jsCall_iidiiii_4", "jsCall_iidiiii_5", "jsCall_iidiiii_6", "jsCall_iidiiii_7", "jsCall_iidiiii_8", "jsCall_iidiiii_9", "jsCall_iidiiii_10", "jsCall_iidiiii_11", "jsCall_iidiiii_12", "jsCall_iidiiii_13", "jsCall_iidiiii_14", "jsCall_iidiiii_15", "jsCall_iidiiii_16", "jsCall_iidiiii_17", "jsCall_iidiiii_18", "jsCall_iidiiii_19", "jsCall_iidiiii_20", "jsCall_iidiiii_21", "jsCall_iidiiii_22", "jsCall_iidiiii_23", "jsCall_iidiiii_24", "jsCall_iidiiii_25", "jsCall_iidiiii_26", "jsCall_iidiiii_27", "jsCall_iidiiii_28", "jsCall_iidiiii_29", "jsCall_iidiiii_30", "jsCall_iidiiii_31", "jsCall_iidiiii_32", "jsCall_iidiiii_33", "jsCall_iidiiii_34", "_fmt_fp", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_iii = [0, "jsCall_iii_0", "jsCall_iii_1", "jsCall_iii_2", "jsCall_iii_3", "jsCall_iii_4", "jsCall_iii_5", "jsCall_iii_6", "jsCall_iii_7", "jsCall_iii_8", "jsCall_iii_9", "jsCall_iii_10", "jsCall_iii_11", "jsCall_iii_12", "jsCall_iii_13", "jsCall_iii_14", "jsCall_iii_15", "jsCall_iii_16", "jsCall_iii_17", "jsCall_iii_18", "jsCall_iii_19", "jsCall_iii_20", "jsCall_iii_21", "jsCall_iii_22", "jsCall_iii_23", "jsCall_iii_24", "jsCall_iii_25", "jsCall_iii_26", "jsCall_iii_27", "jsCall_iii_28", "jsCall_iii_29", "jsCall_iii_30", "jsCall_iii_31", "jsCall_iii_32", "jsCall_iii_33", "jsCall_iii_34", "_avi_read_packet", "_ff_avio_child_next", "_flv_read_packet", "_ff_raw_read_partial_packet", "_matroska_read_packet", "_mov_read_packet", "_mp3_read_packet", "_mpegps_read_packet", "_mpegts_read_packet", "_mpegts_raw_read_packet", "_format_child_next", "_ff_pcm_read_packet", "_urlcontext_child_next", "_bsf_child_next", "_hevc_mp4toannexb_filter", "_hevc_update_thread_context", "_null_filter", "_codec_child_next", "_initSniffStreamFunc", "_hflv_initFunc", "_hflv_getPacketFunc", "_g711_initFunc", "_io_read_pause", "_descriptor_compare", "_hls_decode_entry", "_avcodec_default_get_format", "_ff_startcode_find_candidate_c", "_color_table_compare"]; +var debug_table_iiii = [0, "jsCall_iiii_0", "jsCall_iiii_1", "jsCall_iiii_2", "jsCall_iiii_3", "jsCall_iiii_4", "jsCall_iiii_5", "jsCall_iiii_6", "jsCall_iiii_7", "jsCall_iiii_8", "jsCall_iiii_9", "jsCall_iiii_10", "jsCall_iiii_11", "jsCall_iiii_12", "jsCall_iiii_13", "jsCall_iiii_14", "jsCall_iiii_15", "jsCall_iiii_16", "jsCall_iiii_17", "jsCall_iiii_18", "jsCall_iiii_19", "jsCall_iiii_20", "jsCall_iiii_21", "jsCall_iiii_22", "jsCall_iiii_23", "jsCall_iiii_24", "jsCall_iiii_25", "jsCall_iiii_26", "jsCall_iiii_27", "jsCall_iiii_28", "jsCall_iiii_29", "jsCall_iiii_30", "jsCall_iiii_31", "jsCall_iiii_32", "jsCall_iiii_33", "jsCall_iiii_34", "_mov_read_aclr", "_mov_read_avid", "_mov_read_ares", "_mov_read_avss", "_mov_read_av1c", "_mov_read_chpl", "_mov_read_stco", "_mov_read_colr", "_mov_read_ctts", "_mov_read_default", "_mov_read_dpxe", "_mov_read_dref", "_mov_read_elst", "_mov_read_enda", "_mov_read_fiel", "_mov_read_adrm", "_mov_read_ftyp", "_mov_read_glbl", "_mov_read_hdlr", "_mov_read_ilst", "_mov_read_jp2h", "_mov_read_mdat", "_mov_read_mdhd", "_mov_read_meta", "_mov_read_moof", "_mov_read_moov", "_mov_read_mvhd", "_mov_read_svq3", "_mov_read_alac", "_mov_read_pasp", "_mov_read_sidx", "_mov_read_stps", "_mov_read_strf", "_mov_read_stsc", "_mov_read_stsd", "_mov_read_stss", "_mov_read_stsz", "_mov_read_stts", "_mov_read_tkhd", "_mov_read_tfdt", "_mov_read_tfhd", "_mov_read_trak", "_mov_read_tmcd", "_mov_read_chap", "_mov_read_trex", "_mov_read_trun", "_mov_read_wave", "_mov_read_esds", "_mov_read_dac3", "_mov_read_dec3", "_mov_read_ddts", "_mov_read_wide", "_mov_read_wfex", "_mov_read_cmov", "_mov_read_chan", "_mov_read_dvc1", "_mov_read_sbgp", "_mov_read_uuid", "_mov_read_targa_y216", "_mov_read_free", "_mov_read_custom", "_mov_read_frma", "_mov_read_senc", "_mov_read_saiz", "_mov_read_saio", "_mov_read_pssh", "_mov_read_schm", "_mov_read_tenc", "_mov_read_dfla", "_mov_read_st3d", "_mov_read_sv3d", "_mov_read_dops", "_mov_read_smdm", "_mov_read_coll", "_mov_read_vpcc", "_mov_read_mdcv", "_mov_read_clli", "_h264_split", "_hevc_split", "_set_compensation", "___stdio_write", "_sn_write", "_read_stream_live", "_read_stream_vod", "_getSniffStreamPacketFunc", "_hflv_read_stream_live", "_g711_read_stream_live", "_setCodecTypeFunc", "_read_packet", "_io_write_packet", "_io_read_packet", "_dyn_buf_write", "_mov_read_keys", "_mov_read_udta_string", "_ff_crcA001_update", "_avcodec_default_get_buffer2", "_do_read", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_iiiii = [0, "jsCall_iiiii_0", "jsCall_iiiii_1", "jsCall_iiiii_2", "jsCall_iiiii_3", "jsCall_iiiii_4", "jsCall_iiiii_5", "jsCall_iiiii_6", "jsCall_iiiii_7", "jsCall_iiiii_8", "jsCall_iiiii_9", "jsCall_iiiii_10", "jsCall_iiiii_11", "jsCall_iiiii_12", "jsCall_iiiii_13", "jsCall_iiiii_14", "jsCall_iiiii_15", "jsCall_iiiii_16", "jsCall_iiiii_17", "jsCall_iiiii_18", "jsCall_iiiii_19", "jsCall_iiiii_20", "jsCall_iiiii_21", "jsCall_iiiii_22", "jsCall_iiiii_23", "jsCall_iiiii_24", "jsCall_iiiii_25", "jsCall_iiiii_26", "jsCall_iiiii_27", "jsCall_iiiii_28", "jsCall_iiiii_29", "jsCall_iiiii_30", "jsCall_iiiii_31", "jsCall_iiiii_32", "jsCall_iiiii_33", "jsCall_iiiii_34", "_hevc_decode_frame", "_decode_frame", "_pcm_decode_frame", "_aac_decode_frame", "_hflv_pushBufferFunc", "_g711_pushBufferFunc", "_demuxBoxFunc", "_mov_metadata_int8_no_padding", "_mov_metadata_track_or_disc_number", "_mov_metadata_gnre", "_mov_metadata_int8_bypass_padding", "_lum_planar_vscale", "_chr_planar_vscale", "_any_vscale", "_packed_vscale", "_gamma_convert", "_lum_convert", "_lum_h_scale", "_chr_convert", "_chr_h_scale", "_no_chr_scale", "_hls_decode_entry_wpp", 0, 0, 0, 0, 0, 0]; +var debug_table_iiiiii = [0, "jsCall_iiiiii_0", "jsCall_iiiiii_1", "jsCall_iiiiii_2", "jsCall_iiiiii_3", "jsCall_iiiiii_4", "jsCall_iiiiii_5", "jsCall_iiiiii_6", "jsCall_iiiiii_7", "jsCall_iiiiii_8", "jsCall_iiiiii_9", "jsCall_iiiiii_10", "jsCall_iiiiii_11", "jsCall_iiiiii_12", "jsCall_iiiiii_13", "jsCall_iiiiii_14", "jsCall_iiiiii_15", "jsCall_iiiiii_16", "jsCall_iiiiii_17", "jsCall_iiiiii_18", "jsCall_iiiiii_19", "jsCall_iiiiii_20", "jsCall_iiiiii_21", "jsCall_iiiiii_22", "jsCall_iiiiii_23", "jsCall_iiiiii_24", "jsCall_iiiiii_25", "jsCall_iiiiii_26", "jsCall_iiiiii_27", "jsCall_iiiiii_28", "jsCall_iiiiii_29", "jsCall_iiiiii_30", "jsCall_iiiiii_31", "jsCall_iiiiii_32", "jsCall_iiiiii_33", "jsCall_iiiiii_34", "_pushBufferFunc", "_g711_setSniffStreamCodecTypeFunc", "_decodeCodecContextFunc", "_io_open_default", "_avcodec_default_execute2", "_thread_execute2", "_sbr_lf_gen", "_resample_common_int16", "_resample_linear_int16", "_resample_common_int32", "_resample_linear_int32", "_resample_common_float", "_resample_linear_float", "_resample_common_double", "_resample_linear_double", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_iiiiiii = [0, "jsCall_iiiiiii_0", "jsCall_iiiiiii_1", "jsCall_iiiiiii_2", "jsCall_iiiiiii_3", "jsCall_iiiiiii_4", "jsCall_iiiiiii_5", "jsCall_iiiiiii_6", "jsCall_iiiiiii_7", "jsCall_iiiiiii_8", "jsCall_iiiiiii_9", "jsCall_iiiiiii_10", "jsCall_iiiiiii_11", "jsCall_iiiiiii_12", "jsCall_iiiiiii_13", "jsCall_iiiiiii_14", "jsCall_iiiiiii_15", "jsCall_iiiiiii_16", "jsCall_iiiiiii_17", "jsCall_iiiiiii_18", "jsCall_iiiiiii_19", "jsCall_iiiiiii_20", "jsCall_iiiiiii_21", "jsCall_iiiiiii_22", "jsCall_iiiiiii_23", "jsCall_iiiiiii_24", "jsCall_iiiiiii_25", "jsCall_iiiiiii_26", "jsCall_iiiiiii_27", "jsCall_iiiiiii_28", "jsCall_iiiiiii_29", "jsCall_iiiiiii_30", "jsCall_iiiiiii_31", "jsCall_iiiiiii_32", "jsCall_iiiiiii_33", "jsCall_iiiiiii_34", "_h264_parse", "_hevc_parse", "_mpegaudio_parse", "_multiple_resample", "_invert_initial_buffer", "_hflv_decodeVideoFrameFunc", "_avcodec_default_execute", "_thread_execute", "_sbr_x_gen", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_iiiiiiidiiddii = [0, "jsCall_iiiiiiidiiddii_0", "jsCall_iiiiiiidiiddii_1", "jsCall_iiiiiiidiiddii_2", "jsCall_iiiiiiidiiddii_3", "jsCall_iiiiiiidiiddii_4", "jsCall_iiiiiiidiiddii_5", "jsCall_iiiiiiidiiddii_6", "jsCall_iiiiiiidiiddii_7", "jsCall_iiiiiiidiiddii_8", "jsCall_iiiiiiidiiddii_9", "jsCall_iiiiiiidiiddii_10", "jsCall_iiiiiiidiiddii_11", "jsCall_iiiiiiidiiddii_12", "jsCall_iiiiiiidiiddii_13", "jsCall_iiiiiiidiiddii_14", "jsCall_iiiiiiidiiddii_15", "jsCall_iiiiiiidiiddii_16", "jsCall_iiiiiiidiiddii_17", "jsCall_iiiiiiidiiddii_18", "jsCall_iiiiiiidiiddii_19", "jsCall_iiiiiiidiiddii_20", "jsCall_iiiiiiidiiddii_21", "jsCall_iiiiiiidiiddii_22", "jsCall_iiiiiiidiiddii_23", "jsCall_iiiiiiidiiddii_24", "jsCall_iiiiiiidiiddii_25", "jsCall_iiiiiiidiiddii_26", "jsCall_iiiiiiidiiddii_27", "jsCall_iiiiiiidiiddii_28", "jsCall_iiiiiiidiiddii_29", "jsCall_iiiiiiidiiddii_30", "jsCall_iiiiiiidiiddii_31", "jsCall_iiiiiiidiiddii_32", "jsCall_iiiiiiidiiddii_33", "jsCall_iiiiiiidiiddii_34", "_resample_init", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_iiiiiiii = [0, "jsCall_iiiiiiii_0", "jsCall_iiiiiiii_1", "jsCall_iiiiiiii_2", "jsCall_iiiiiiii_3", "jsCall_iiiiiiii_4", "jsCall_iiiiiiii_5", "jsCall_iiiiiiii_6", "jsCall_iiiiiiii_7", "jsCall_iiiiiiii_8", "jsCall_iiiiiiii_9", "jsCall_iiiiiiii_10", "jsCall_iiiiiiii_11", "jsCall_iiiiiiii_12", "jsCall_iiiiiiii_13", "jsCall_iiiiiiii_14", "jsCall_iiiiiiii_15", "jsCall_iiiiiiii_16", "jsCall_iiiiiiii_17", "jsCall_iiiiiiii_18", "jsCall_iiiiiiii_19", "jsCall_iiiiiiii_20", "jsCall_iiiiiiii_21", "jsCall_iiiiiiii_22", "jsCall_iiiiiiii_23", "jsCall_iiiiiiii_24", "jsCall_iiiiiiii_25", "jsCall_iiiiiiii_26", "jsCall_iiiiiiii_27", "jsCall_iiiiiiii_28", "jsCall_iiiiiiii_29", "jsCall_iiiiiiii_30", "jsCall_iiiiiiii_31", "jsCall_iiiiiiii_32", "jsCall_iiiiiiii_33", "jsCall_iiiiiiii_34", "_decodeVideoFrameFunc", "_hflv_setSniffStreamCodecTypeFunc", "_swscale", "_ff_sws_alphablendaway", "_yuv2rgb_c_32", "_yuva2rgba_c", "_yuv2rgb_c_bgr48", "_yuv2rgb_c_48", "_yuva2argb_c", "_yuv2rgb_c_24_rgb", "_yuv2rgb_c_24_bgr", "_yuv2rgb_c_16_ordered_dither", "_yuv2rgb_c_15_ordered_dither", "_yuv2rgb_c_12_ordered_dither", "_yuv2rgb_c_8_ordered_dither", "_yuv2rgb_c_4_ordered_dither", "_yuv2rgb_c_4b_ordered_dither", "_yuv2rgb_c_1_ordered_dither", "_planarToP01xWrapper", "_planar8ToP01xleWrapper", "_yvu9ToYv12Wrapper", "_bgr24ToYv12Wrapper", "_rgbToRgbWrapper", "_planarRgbToplanarRgbWrapper", "_planarRgbToRgbWrapper", "_planarRgbaToRgbWrapper", "_Rgb16ToPlanarRgb16Wrapper", "_planarRgb16ToRgb16Wrapper", "_rgbToPlanarRgbWrapper", "_bayer_to_rgb24_wrapper", "_bayer_to_yv12_wrapper", "_bswap_16bpc", "_palToRgbWrapper", "_yuv422pToYuy2Wrapper", "_yuv422pToUyvyWrapper", "_uint_y_to_float_y_wrapper", "_float_y_to_uint_y_wrapper", "_planarToYuy2Wrapper", "_planarToUyvyWrapper", "_yuyvToYuv420Wrapper", "_uyvyToYuv420Wrapper", "_yuyvToYuv422Wrapper", "_uyvyToYuv422Wrapper", "_packedCopyWrapper", "_planarCopyWrapper", "_planarToNv12Wrapper", "_planarToNv24Wrapper", "_nv12ToPlanarWrapper", "_nv24ToPlanarWrapper", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_iiiiiiiid = [0, "jsCall_iiiiiiiid_0", "jsCall_iiiiiiiid_1", "jsCall_iiiiiiiid_2", "jsCall_iiiiiiiid_3", "jsCall_iiiiiiiid_4", "jsCall_iiiiiiiid_5", "jsCall_iiiiiiiid_6", "jsCall_iiiiiiiid_7", "jsCall_iiiiiiiid_8", "jsCall_iiiiiiiid_9", "jsCall_iiiiiiiid_10", "jsCall_iiiiiiiid_11", "jsCall_iiiiiiiid_12", "jsCall_iiiiiiiid_13", "jsCall_iiiiiiiid_14", "jsCall_iiiiiiiid_15", "jsCall_iiiiiiiid_16", "jsCall_iiiiiiiid_17", "jsCall_iiiiiiiid_18", "jsCall_iiiiiiiid_19", "jsCall_iiiiiiiid_20", "jsCall_iiiiiiiid_21", "jsCall_iiiiiiiid_22", "jsCall_iiiiiiiid_23", "jsCall_iiiiiiiid_24", "jsCall_iiiiiiiid_25", "jsCall_iiiiiiiid_26", "jsCall_iiiiiiiid_27", "jsCall_iiiiiiiid_28", "jsCall_iiiiiiiid_29", "jsCall_iiiiiiiid_30", "jsCall_iiiiiiiid_31", "jsCall_iiiiiiiid_32", "jsCall_iiiiiiiid_33", "jsCall_iiiiiiiid_34", "_setSniffStreamCodecTypeFunc", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_iiiiij = [0, "jsCall_iiiiij_0", "jsCall_iiiiij_1", "jsCall_iiiiij_2", "jsCall_iiiiij_3", "jsCall_iiiiij_4", "jsCall_iiiiij_5", "jsCall_iiiiij_6", "jsCall_iiiiij_7", "jsCall_iiiiij_8", "jsCall_iiiiij_9", "jsCall_iiiiij_10", "jsCall_iiiiij_11", "jsCall_iiiiij_12", "jsCall_iiiiij_13", "jsCall_iiiiij_14", "jsCall_iiiiij_15", "jsCall_iiiiij_16", "jsCall_iiiiij_17", "jsCall_iiiiij_18", "jsCall_iiiiij_19", "jsCall_iiiiij_20", "jsCall_iiiiij_21", "jsCall_iiiiij_22", "jsCall_iiiiij_23", "jsCall_iiiiij_24", "jsCall_iiiiij_25", "jsCall_iiiiij_26", "jsCall_iiiiij_27", "jsCall_iiiiij_28", "jsCall_iiiiij_29", "jsCall_iiiiij_30", "jsCall_iiiiij_31", "jsCall_iiiiij_32", "jsCall_iiiiij_33", "jsCall_iiiiij_34", "_mpegts_push_data", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_iiiji = [0, "jsCall_iiiji_0", "jsCall_iiiji_1", "jsCall_iiiji_2", "jsCall_iiiji_3", "jsCall_iiiji_4", "jsCall_iiiji_5", "jsCall_iiiji_6", "jsCall_iiiji_7", "jsCall_iiiji_8", "jsCall_iiiji_9", "jsCall_iiiji_10", "jsCall_iiiji_11", "jsCall_iiiji_12", "jsCall_iiiji_13", "jsCall_iiiji_14", "jsCall_iiiji_15", "jsCall_iiiji_16", "jsCall_iiiji_17", "jsCall_iiiji_18", "jsCall_iiiji_19", "jsCall_iiiji_20", "jsCall_iiiji_21", "jsCall_iiiji_22", "jsCall_iiiji_23", "jsCall_iiiji_24", "jsCall_iiiji_25", "jsCall_iiiji_26", "jsCall_iiiji_27", "jsCall_iiiji_28", "jsCall_iiiji_29", "jsCall_iiiji_30", "jsCall_iiiji_31", "jsCall_iiiji_32", "jsCall_iiiji_33", "jsCall_iiiji_34", "_avi_read_seek", "_flv_read_seek", "_matroska_read_seek", "_mov_read_seek", "_mp3_seek", "_ff_pcm_read_seek", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_iiijjji = [0, "jsCall_iiijjji_0", "jsCall_iiijjji_1", "jsCall_iiijjji_2", "jsCall_iiijjji_3", "jsCall_iiijjji_4", "jsCall_iiijjji_5", "jsCall_iiijjji_6", "jsCall_iiijjji_7", "jsCall_iiijjji_8", "jsCall_iiijjji_9", "jsCall_iiijjji_10", "jsCall_iiijjji_11", "jsCall_iiijjji_12", "jsCall_iiijjji_13", "jsCall_iiijjji_14", "jsCall_iiijjji_15", "jsCall_iiijjji_16", "jsCall_iiijjji_17", "jsCall_iiijjji_18", "jsCall_iiijjji_19", "jsCall_iiijjji_20", "jsCall_iiijjji_21", "jsCall_iiijjji_22", "jsCall_iiijjji_23", "jsCall_iiijjji_24", "jsCall_iiijjji_25", "jsCall_iiijjji_26", "jsCall_iiijjji_27", "jsCall_iiijjji_28", "jsCall_iiijjji_29", "jsCall_iiijjji_30", "jsCall_iiijjji_31", "jsCall_iiijjji_32", "jsCall_iiijjji_33", "jsCall_iiijjji_34", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_jii = [0, "jsCall_jii_0", "jsCall_jii_1", "jsCall_jii_2", "jsCall_jii_3", "jsCall_jii_4", "jsCall_jii_5", "jsCall_jii_6", "jsCall_jii_7", "jsCall_jii_8", "jsCall_jii_9", "jsCall_jii_10", "jsCall_jii_11", "jsCall_jii_12", "jsCall_jii_13", "jsCall_jii_14", "jsCall_jii_15", "jsCall_jii_16", "jsCall_jii_17", "jsCall_jii_18", "jsCall_jii_19", "jsCall_jii_20", "jsCall_jii_21", "jsCall_jii_22", "jsCall_jii_23", "jsCall_jii_24", "jsCall_jii_25", "jsCall_jii_26", "jsCall_jii_27", "jsCall_jii_28", "jsCall_jii_29", "jsCall_jii_30", "jsCall_jii_31", "jsCall_jii_32", "jsCall_jii_33", "jsCall_jii_34", "_get_out_samples", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_jiiij = [0, "jsCall_jiiij_0", "jsCall_jiiij_1", "jsCall_jiiij_2", "jsCall_jiiij_3", "jsCall_jiiij_4", "jsCall_jiiij_5", "jsCall_jiiij_6", "jsCall_jiiij_7", "jsCall_jiiij_8", "jsCall_jiiij_9", "jsCall_jiiij_10", "jsCall_jiiij_11", "jsCall_jiiij_12", "jsCall_jiiij_13", "jsCall_jiiij_14", "jsCall_jiiij_15", "jsCall_jiiij_16", "jsCall_jiiij_17", "jsCall_jiiij_18", "jsCall_jiiij_19", "jsCall_jiiij_20", "jsCall_jiiij_21", "jsCall_jiiij_22", "jsCall_jiiij_23", "jsCall_jiiij_24", "jsCall_jiiij_25", "jsCall_jiiij_26", "jsCall_jiiij_27", "jsCall_jiiij_28", "jsCall_jiiij_29", "jsCall_jiiij_30", "jsCall_jiiij_31", "jsCall_jiiij_32", "jsCall_jiiij_33", "jsCall_jiiij_34", "_mpegps_read_dts", "_mpegts_get_dts", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_jiiji = [0, "jsCall_jiiji_0", "jsCall_jiiji_1", "jsCall_jiiji_2", "jsCall_jiiji_3", "jsCall_jiiji_4", "jsCall_jiiji_5", "jsCall_jiiji_6", "jsCall_jiiji_7", "jsCall_jiiji_8", "jsCall_jiiji_9", "jsCall_jiiji_10", "jsCall_jiiji_11", "jsCall_jiiji_12", "jsCall_jiiji_13", "jsCall_jiiji_14", "jsCall_jiiji_15", "jsCall_jiiji_16", "jsCall_jiiji_17", "jsCall_jiiji_18", "jsCall_jiiji_19", "jsCall_jiiji_20", "jsCall_jiiji_21", "jsCall_jiiji_22", "jsCall_jiiji_23", "jsCall_jiiji_24", "jsCall_jiiji_25", "jsCall_jiiji_26", "jsCall_jiiji_27", "jsCall_jiiji_28", "jsCall_jiiji_29", "jsCall_jiiji_30", "jsCall_jiiji_31", "jsCall_jiiji_32", "jsCall_jiiji_33", "jsCall_jiiji_34", "_io_read_seek", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_jij = [0, "jsCall_jij_0", "jsCall_jij_1", "jsCall_jij_2", "jsCall_jij_3", "jsCall_jij_4", "jsCall_jij_5", "jsCall_jij_6", "jsCall_jij_7", "jsCall_jij_8", "jsCall_jij_9", "jsCall_jij_10", "jsCall_jij_11", "jsCall_jij_12", "jsCall_jij_13", "jsCall_jij_14", "jsCall_jij_15", "jsCall_jij_16", "jsCall_jij_17", "jsCall_jij_18", "jsCall_jij_19", "jsCall_jij_20", "jsCall_jij_21", "jsCall_jij_22", "jsCall_jij_23", "jsCall_jij_24", "jsCall_jij_25", "jsCall_jij_26", "jsCall_jij_27", "jsCall_jij_28", "jsCall_jij_29", "jsCall_jij_30", "jsCall_jij_31", "jsCall_jij_32", "jsCall_jij_33", "jsCall_jij_34", "_get_delay", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_jiji = [0, "jsCall_jiji_0", "jsCall_jiji_1", "jsCall_jiji_2", "jsCall_jiji_3", "jsCall_jiji_4", "jsCall_jiji_5", "jsCall_jiji_6", "jsCall_jiji_7", "jsCall_jiji_8", "jsCall_jiji_9", "jsCall_jiji_10", "jsCall_jiji_11", "jsCall_jiji_12", "jsCall_jiji_13", "jsCall_jiji_14", "jsCall_jiji_15", "jsCall_jiji_16", "jsCall_jiji_17", "jsCall_jiji_18", "jsCall_jiji_19", "jsCall_jiji_20", "jsCall_jiji_21", "jsCall_jiji_22", "jsCall_jiji_23", "jsCall_jiji_24", "jsCall_jiji_25", "jsCall_jiji_26", "jsCall_jiji_27", "jsCall_jiji_28", "jsCall_jiji_29", "jsCall_jiji_30", "jsCall_jiji_31", "jsCall_jiji_32", "jsCall_jiji_33", "jsCall_jiji_34", "___stdio_seek", "___emscripten_stdout_seek", "_seek_in_buffer", "_io_seek", "_dyn_buf_seek", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_v = [0, "jsCall_v_0", "jsCall_v_1", "jsCall_v_2", "jsCall_v_3", "jsCall_v_4", "jsCall_v_5", "jsCall_v_6", "jsCall_v_7", "jsCall_v_8", "jsCall_v_9", "jsCall_v_10", "jsCall_v_11", "jsCall_v_12", "jsCall_v_13", "jsCall_v_14", "jsCall_v_15", "jsCall_v_16", "jsCall_v_17", "jsCall_v_18", "jsCall_v_19", "jsCall_v_20", "jsCall_v_21", "jsCall_v_22", "jsCall_v_23", "jsCall_v_24", "jsCall_v_25", "jsCall_v_26", "jsCall_v_27", "jsCall_v_28", "jsCall_v_29", "jsCall_v_30", "jsCall_v_31", "jsCall_v_32", "jsCall_v_33", "jsCall_v_34", "_init_ff_cos_tabs_16", "_init_ff_cos_tabs_32", "_init_ff_cos_tabs_64", "_init_ff_cos_tabs_128", "_init_ff_cos_tabs_256", "_init_ff_cos_tabs_512", "_init_ff_cos_tabs_1024", "_init_ff_cos_tabs_2048", "_init_ff_cos_tabs_4096", "_init_ff_cos_tabs_8192", "_init_ff_cos_tabs_16384", "_init_ff_cos_tabs_32768", "_init_ff_cos_tabs_65536", "_init_ff_cos_tabs_131072", "_introduce_mine", "_introduceMineFunc", "_av_format_init_next", "_av_codec_init_static", "_av_codec_init_next", "_ff_init_mpadsp_tabs_float", "_ff_init_mpadsp_tabs_fixed", "_aac_static_table_init", "_AV_CRC_8_ATM_init_table_once", "_AV_CRC_8_EBU_init_table_once", "_AV_CRC_16_ANSI_init_table_once", "_AV_CRC_16_CCITT_init_table_once", "_AV_CRC_24_IEEE_init_table_once", "_AV_CRC_32_IEEE_init_table_once", "_AV_CRC_32_IEEE_LE_init_table_once", "_AV_CRC_16_ANSI_LE_init_table_once", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_vdiidiiiii = [0, "jsCall_vdiidiiiii_0", "jsCall_vdiidiiiii_1", "jsCall_vdiidiiiii_2", "jsCall_vdiidiiiii_3", "jsCall_vdiidiiiii_4", "jsCall_vdiidiiiii_5", "jsCall_vdiidiiiii_6", "jsCall_vdiidiiiii_7", "jsCall_vdiidiiiii_8", "jsCall_vdiidiiiii_9", "jsCall_vdiidiiiii_10", "jsCall_vdiidiiiii_11", "jsCall_vdiidiiiii_12", "jsCall_vdiidiiiii_13", "jsCall_vdiidiiiii_14", "jsCall_vdiidiiiii_15", "jsCall_vdiidiiiii_16", "jsCall_vdiidiiiii_17", "jsCall_vdiidiiiii_18", "jsCall_vdiidiiiii_19", "jsCall_vdiidiiiii_20", "jsCall_vdiidiiiii_21", "jsCall_vdiidiiiii_22", "jsCall_vdiidiiiii_23", "jsCall_vdiidiiiii_24", "jsCall_vdiidiiiii_25", "jsCall_vdiidiiiii_26", "jsCall_vdiidiiiii_27", "jsCall_vdiidiiiii_28", "jsCall_vdiidiiiii_29", "jsCall_vdiidiiiii_30", "jsCall_vdiidiiiii_31", "jsCall_vdiidiiiii_32", "jsCall_vdiidiiiii_33", "jsCall_vdiidiiiii_34", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_vdiidiiiiii = [0, "jsCall_vdiidiiiiii_0", "jsCall_vdiidiiiiii_1", "jsCall_vdiidiiiiii_2", "jsCall_vdiidiiiiii_3", "jsCall_vdiidiiiiii_4", "jsCall_vdiidiiiiii_5", "jsCall_vdiidiiiiii_6", "jsCall_vdiidiiiiii_7", "jsCall_vdiidiiiiii_8", "jsCall_vdiidiiiiii_9", "jsCall_vdiidiiiiii_10", "jsCall_vdiidiiiiii_11", "jsCall_vdiidiiiiii_12", "jsCall_vdiidiiiiii_13", "jsCall_vdiidiiiiii_14", "jsCall_vdiidiiiiii_15", "jsCall_vdiidiiiiii_16", "jsCall_vdiidiiiiii_17", "jsCall_vdiidiiiiii_18", "jsCall_vdiidiiiiii_19", "jsCall_vdiidiiiiii_20", "jsCall_vdiidiiiiii_21", "jsCall_vdiidiiiiii_22", "jsCall_vdiidiiiiii_23", "jsCall_vdiidiiiiii_24", "jsCall_vdiidiiiiii_25", "jsCall_vdiidiiiiii_26", "jsCall_vdiidiiiiii_27", "jsCall_vdiidiiiiii_28", "jsCall_vdiidiiiiii_29", "jsCall_vdiidiiiiii_30", "jsCall_vdiidiiiiii_31", "jsCall_vdiidiiiiii_32", "jsCall_vdiidiiiiii_33", "jsCall_vdiidiiiiii_34", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_vi = [0, "jsCall_vi_0", "jsCall_vi_1", "jsCall_vi_2", "jsCall_vi_3", "jsCall_vi_4", "jsCall_vi_5", "jsCall_vi_6", "jsCall_vi_7", "jsCall_vi_8", "jsCall_vi_9", "jsCall_vi_10", "jsCall_vi_11", "jsCall_vi_12", "jsCall_vi_13", "jsCall_vi_14", "jsCall_vi_15", "jsCall_vi_16", "jsCall_vi_17", "jsCall_vi_18", "jsCall_vi_19", "jsCall_vi_20", "jsCall_vi_21", "jsCall_vi_22", "jsCall_vi_23", "jsCall_vi_24", "jsCall_vi_25", "jsCall_vi_26", "jsCall_vi_27", "jsCall_vi_28", "jsCall_vi_29", "jsCall_vi_30", "jsCall_vi_31", "jsCall_vi_32", "jsCall_vi_33", "jsCall_vi_34", "_free_geobtag", "_free_apic", "_free_chapter", "_free_priv", "_hevc_decode_flush", "_flush", "_flush_3915", "_fft4", "_fft8", "_fft16", "_fft32", "_fft64", "_fft128", "_fft256", "_fft512", "_fft1024", "_fft2048", "_fft4096", "_fft8192", "_fft16384", "_fft32768", "_fft65536", "_fft131072", "_h264_close", "_hevc_parser_close", "_ff_parse_close", "_resample_free", "_logRequest_downloadSucceeded", "_logRequest_downloadFailed", "_downloadSucceeded", "_downloadFailed", "_transform_4x4_luma_9", "_idct_4x4_dc_9", "_idct_8x8_dc_9", "_idct_16x16_dc_9", "_idct_32x32_dc_9", "_transform_4x4_luma_10", "_idct_4x4_dc_10", "_idct_8x8_dc_10", "_idct_16x16_dc_10", "_idct_32x32_dc_10", "_transform_4x4_luma_12", "_idct_4x4_dc_12", "_idct_8x8_dc_12", "_idct_16x16_dc_12", "_idct_32x32_dc_12", "_transform_4x4_luma_8", "_idct_4x4_dc_8", "_idct_8x8_dc_8", "_idct_16x16_dc_8", "_idct_32x32_dc_8", "_main_function", "_sbr_sum64x5_c", "_sbr_neg_odd_64_c", "_sbr_qmf_pre_shuffle_c", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_vii = [0, "jsCall_vii_0", "jsCall_vii_1", "jsCall_vii_2", "jsCall_vii_3", "jsCall_vii_4", "jsCall_vii_5", "jsCall_vii_6", "jsCall_vii_7", "jsCall_vii_8", "jsCall_vii_9", "jsCall_vii_10", "jsCall_vii_11", "jsCall_vii_12", "jsCall_vii_13", "jsCall_vii_14", "jsCall_vii_15", "jsCall_vii_16", "jsCall_vii_17", "jsCall_vii_18", "jsCall_vii_19", "jsCall_vii_20", "jsCall_vii_21", "jsCall_vii_22", "jsCall_vii_23", "jsCall_vii_24", "jsCall_vii_25", "jsCall_vii_26", "jsCall_vii_27", "jsCall_vii_28", "jsCall_vii_29", "jsCall_vii_30", "jsCall_vii_31", "jsCall_vii_32", "jsCall_vii_33", "jsCall_vii_34", "_io_close_default", "_lumRangeFromJpeg_c", "_lumRangeToJpeg_c", "_lumRangeFromJpeg16_c", "_lumRangeToJpeg16_c", "_decode_data_free", "_dequant_9", "_idct_4x4_9", "_idct_8x8_9", "_idct_16x16_9", "_idct_32x32_9", "_dequant_10", "_idct_4x4_10", "_idct_8x8_10", "_idct_16x16_10", "_idct_32x32_10", "_dequant_12", "_idct_4x4_12", "_idct_8x8_12", "_idct_16x16_12", "_idct_32x32_12", "_dequant_8", "_idct_4x4_8", "_idct_8x8_8", "_idct_16x16_8", "_idct_32x32_8", "_ff_dct32_fixed", "_imdct_and_windowing", "_apply_ltp", "_update_ltp", "_imdct_and_windowing_ld", "_imdct_and_windowing_eld", "_imdct_and_windowing_960", "_ff_dct32_float", "_dct32_func", "_dct_calc_I_c", "_dct_calc_II_c", "_dct_calc_III_c", "_dst_calc_I_c", "_fft_permute_c", "_fft_calc_c", "_ff_h264_chroma_dc_dequant_idct_9_c", "_ff_h264_chroma422_dc_dequant_idct_9_c", "_ff_h264_chroma_dc_dequant_idct_10_c", "_ff_h264_chroma422_dc_dequant_idct_10_c", "_ff_h264_chroma_dc_dequant_idct_12_c", "_ff_h264_chroma422_dc_dequant_idct_12_c", "_ff_h264_chroma_dc_dequant_idct_14_c", "_ff_h264_chroma422_dc_dequant_idct_14_c", "_ff_h264_chroma_dc_dequant_idct_8_c", "_ff_h264_chroma422_dc_dequant_idct_8_c", "_hevc_pps_free", "_rdft_calc_c", "_sbr_qmf_post_shuffle_c", "_sbr_qmf_deint_neg_c", "_sbr_autocorrelate_c", "_av_buffer_default_free", "_pool_release_buffer", "_sha1_transform", "_sha256_transform", "_pop_arg_long_double", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viidi = [0, "jsCall_viidi_0", "jsCall_viidi_1", "jsCall_viidi_2", "jsCall_viidi_3", "jsCall_viidi_4", "jsCall_viidi_5", "jsCall_viidi_6", "jsCall_viidi_7", "jsCall_viidi_8", "jsCall_viidi_9", "jsCall_viidi_10", "jsCall_viidi_11", "jsCall_viidi_12", "jsCall_viidi_13", "jsCall_viidi_14", "jsCall_viidi_15", "jsCall_viidi_16", "jsCall_viidi_17", "jsCall_viidi_18", "jsCall_viidi_19", "jsCall_viidi_20", "jsCall_viidi_21", "jsCall_viidi_22", "jsCall_viidi_23", "jsCall_viidi_24", "jsCall_viidi_25", "jsCall_viidi_26", "jsCall_viidi_27", "jsCall_viidi_28", "jsCall_viidi_29", "jsCall_viidi_30", "jsCall_viidi_31", "jsCall_viidi_32", "jsCall_viidi_33", "jsCall_viidi_34", "_vector_dmac_scalar_c", "_vector_dmul_scalar_c", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viifi = [0, "jsCall_viifi_0", "jsCall_viifi_1", "jsCall_viifi_2", "jsCall_viifi_3", "jsCall_viifi_4", "jsCall_viifi_5", "jsCall_viifi_6", "jsCall_viifi_7", "jsCall_viifi_8", "jsCall_viifi_9", "jsCall_viifi_10", "jsCall_viifi_11", "jsCall_viifi_12", "jsCall_viifi_13", "jsCall_viifi_14", "jsCall_viifi_15", "jsCall_viifi_16", "jsCall_viifi_17", "jsCall_viifi_18", "jsCall_viifi_19", "jsCall_viifi_20", "jsCall_viifi_21", "jsCall_viifi_22", "jsCall_viifi_23", "jsCall_viifi_24", "jsCall_viifi_25", "jsCall_viifi_26", "jsCall_viifi_27", "jsCall_viifi_28", "jsCall_viifi_29", "jsCall_viifi_30", "jsCall_viifi_31", "jsCall_viifi_32", "jsCall_viifi_33", "jsCall_viifi_34", "_vector_fmac_scalar_c", "_vector_fmul_scalar_c", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viii = [0, "jsCall_viii_0", "jsCall_viii_1", "jsCall_viii_2", "jsCall_viii_3", "jsCall_viii_4", "jsCall_viii_5", "jsCall_viii_6", "jsCall_viii_7", "jsCall_viii_8", "jsCall_viii_9", "jsCall_viii_10", "jsCall_viii_11", "jsCall_viii_12", "jsCall_viii_13", "jsCall_viii_14", "jsCall_viii_15", "jsCall_viii_16", "jsCall_viii_17", "jsCall_viii_18", "jsCall_viii_19", "jsCall_viii_20", "jsCall_viii_21", "jsCall_viii_22", "jsCall_viii_23", "jsCall_viii_24", "jsCall_viii_25", "jsCall_viii_26", "jsCall_viii_27", "jsCall_viii_28", "jsCall_viii_29", "jsCall_viii_30", "jsCall_viii_31", "jsCall_viii_32", "jsCall_viii_33", "jsCall_viii_34", "_avcHandleFrame", "_handleFrame", "_sdt_cb", "_pat_cb", "_pmt_cb", "_scte_data_cb", "_m4sl_cb", "_chrRangeFromJpeg_c", "_chrRangeToJpeg_c", "_chrRangeFromJpeg16_c", "_chrRangeToJpeg16_c", "_rgb15to16_c", "_rgb15tobgr24_c", "_rgb15to32_c", "_rgb16tobgr24_c", "_rgb16to32_c", "_rgb16to15_c", "_rgb24tobgr16_c", "_rgb24tobgr15_c", "_rgb24tobgr32_c", "_rgb32to16_c", "_rgb32to15_c", "_rgb32tobgr24_c", "_rgb24to15_c", "_rgb24to16_c", "_rgb24tobgr24_c", "_shuffle_bytes_0321_c", "_shuffle_bytes_2103_c", "_shuffle_bytes_1230_c", "_shuffle_bytes_3012_c", "_shuffle_bytes_3210_c", "_rgb32tobgr16_c", "_rgb32tobgr15_c", "_rgb48tobgr48_bswap", "_rgb48tobgr64_bswap", "_rgb48to64_bswap", "_rgb64to48_bswap", "_rgb48tobgr48_nobswap", "_rgb48tobgr64_nobswap", "_rgb48to64_nobswap", "_rgb64tobgr48_nobswap", "_rgb64tobgr48_bswap", "_rgb64to48_nobswap", "_rgb12to15", "_rgb15to24", "_rgb16to24", "_rgb32to24", "_rgb24to32", "_rgb12tobgr12", "_rgb15tobgr15", "_rgb16tobgr15", "_rgb15tobgr16", "_rgb16tobgr16", "_rgb15tobgr32", "_rgb16tobgr32", "_add_residual4x4_9", "_add_residual8x8_9", "_add_residual16x16_9", "_add_residual32x32_9", "_transform_rdpcm_9", "_add_residual4x4_10", "_add_residual8x8_10", "_add_residual16x16_10", "_add_residual32x32_10", "_transform_rdpcm_10", "_add_residual4x4_12", "_add_residual8x8_12", "_add_residual16x16_12", "_add_residual32x32_12", "_transform_rdpcm_12", "_add_residual4x4_8", "_add_residual8x8_8", "_add_residual16x16_8", "_add_residual32x32_8", "_transform_rdpcm_8", "_just_return", "_bswap_buf", "_bswap16_buf", "_ff_imdct_calc_c", "_ff_imdct_half_c", "_ff_mdct_calc_c", "_ff_h264_add_pixels4_16_c", "_ff_h264_add_pixels4_8_c", "_ff_h264_add_pixels8_16_c", "_ff_h264_add_pixels8_8_c", "_ff_h264_idct_add_9_c", "_ff_h264_idct8_add_9_c", "_ff_h264_idct_dc_add_9_c", "_ff_h264_idct8_dc_add_9_c", "_ff_h264_luma_dc_dequant_idct_9_c", "_ff_h264_idct_add_10_c", "_ff_h264_idct8_add_10_c", "_ff_h264_idct_dc_add_10_c", "_ff_h264_idct8_dc_add_10_c", "_ff_h264_luma_dc_dequant_idct_10_c", "_ff_h264_idct_add_12_c", "_ff_h264_idct8_add_12_c", "_ff_h264_idct_dc_add_12_c", "_ff_h264_idct8_dc_add_12_c", "_ff_h264_luma_dc_dequant_idct_12_c", "_ff_h264_idct_add_14_c", "_ff_h264_idct8_add_14_c", "_ff_h264_idct_dc_add_14_c", "_ff_h264_idct8_dc_add_14_c", "_ff_h264_luma_dc_dequant_idct_14_c", "_ff_h264_idct_add_8_c", "_ff_h264_idct8_add_8_c", "_ff_h264_idct_dc_add_8_c", "_ff_h264_idct8_dc_add_8_c", "_ff_h264_luma_dc_dequant_idct_8_c", "_sbr_qmf_deint_bfly_c", "_ps_add_squares_c", "_butterflies_float_c", "_cpy1", "_cpy2", "_cpy4", "_cpy8", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viiid = [0, "jsCall_viiid_0", "jsCall_viiid_1", "jsCall_viiid_2", "jsCall_viiid_3", "jsCall_viiid_4", "jsCall_viiid_5", "jsCall_viiid_6", "jsCall_viiid_7", "jsCall_viiid_8", "jsCall_viiid_9", "jsCall_viiid_10", "jsCall_viiid_11", "jsCall_viiid_12", "jsCall_viiid_13", "jsCall_viiid_14", "jsCall_viiid_15", "jsCall_viiid_16", "jsCall_viiid_17", "jsCall_viiid_18", "jsCall_viiid_19", "jsCall_viiid_20", "jsCall_viiid_21", "jsCall_viiid_22", "jsCall_viiid_23", "jsCall_viiid_24", "jsCall_viiid_25", "jsCall_viiid_26", "jsCall_viiid_27", "jsCall_viiid_28", "jsCall_viiid_29", "jsCall_viiid_30", "jsCall_viiid_31", "jsCall_viiid_32", "jsCall_viiid_33", "jsCall_viiid_34", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viiii = [0, "jsCall_viiii_0", "jsCall_viiii_1", "jsCall_viiii_2", "jsCall_viiii_3", "jsCall_viiii_4", "jsCall_viiii_5", "jsCall_viiii_6", "jsCall_viiii_7", "jsCall_viiii_8", "jsCall_viiii_9", "jsCall_viiii_10", "jsCall_viiii_11", "jsCall_viiii_12", "jsCall_viiii_13", "jsCall_viiii_14", "jsCall_viiii_15", "jsCall_viiii_16", "jsCall_viiii_17", "jsCall_viiii_18", "jsCall_viiii_19", "jsCall_viiii_20", "jsCall_viiii_21", "jsCall_viiii_22", "jsCall_viiii_23", "jsCall_viiii_24", "jsCall_viiii_25", "jsCall_viiii_26", "jsCall_viiii_27", "jsCall_viiii_28", "jsCall_viiii_29", "jsCall_viiii_30", "jsCall_viiii_31", "jsCall_viiii_32", "jsCall_viiii_33", "jsCall_viiii_34", "_planar_rgb9le_to_y", "_planar_rgb10le_to_a", "_planar_rgb10le_to_y", "_planar_rgb12le_to_a", "_planar_rgb12le_to_y", "_planar_rgb14le_to_y", "_planar_rgb16le_to_a", "_planar_rgb16le_to_y", "_planar_rgb9be_to_y", "_planar_rgb10be_to_a", "_planar_rgb10be_to_y", "_planar_rgb12be_to_a", "_planar_rgb12be_to_y", "_planar_rgb14be_to_y", "_planar_rgb16be_to_a", "_planar_rgb16be_to_y", "_planar_rgb_to_a", "_planar_rgb_to_y", "_gray8aToPacked32", "_gray8aToPacked32_1", "_gray8aToPacked24", "_sws_convertPalette8ToPacked32", "_sws_convertPalette8ToPacked24", "_intra_pred_2_9", "_intra_pred_3_9", "_intra_pred_4_9", "_intra_pred_5_9", "_pred_planar_0_9", "_pred_planar_1_9", "_pred_planar_2_9", "_pred_planar_3_9", "_intra_pred_2_10", "_intra_pred_3_10", "_intra_pred_4_10", "_intra_pred_5_10", "_pred_planar_0_10", "_pred_planar_1_10", "_pred_planar_2_10", "_pred_planar_3_10", "_intra_pred_2_12", "_intra_pred_3_12", "_intra_pred_4_12", "_intra_pred_5_12", "_pred_planar_0_12", "_pred_planar_1_12", "_pred_planar_2_12", "_pred_planar_3_12", "_intra_pred_2_8", "_intra_pred_3_8", "_intra_pred_4_8", "_intra_pred_5_8", "_pred_planar_0_8", "_pred_planar_1_8", "_pred_planar_2_8", "_pred_planar_3_8", "_apply_tns", "_windowing_and_mdct_ltp", "_h264_v_loop_filter_luma_intra_9_c", "_h264_h_loop_filter_luma_intra_9_c", "_h264_h_loop_filter_luma_mbaff_intra_9_c", "_h264_v_loop_filter_chroma_intra_9_c", "_h264_h_loop_filter_chroma_intra_9_c", "_h264_h_loop_filter_chroma422_intra_9_c", "_h264_h_loop_filter_chroma_mbaff_intra_9_c", "_h264_h_loop_filter_chroma422_mbaff_intra_9_c", "_h264_v_loop_filter_luma_intra_10_c", "_h264_h_loop_filter_luma_intra_10_c", "_h264_h_loop_filter_luma_mbaff_intra_10_c", "_h264_v_loop_filter_chroma_intra_10_c", "_h264_h_loop_filter_chroma_intra_10_c", "_h264_h_loop_filter_chroma422_intra_10_c", "_h264_h_loop_filter_chroma_mbaff_intra_10_c", "_h264_h_loop_filter_chroma422_mbaff_intra_10_c", "_h264_v_loop_filter_luma_intra_12_c", "_h264_h_loop_filter_luma_intra_12_c", "_h264_h_loop_filter_luma_mbaff_intra_12_c", "_h264_v_loop_filter_chroma_intra_12_c", "_h264_h_loop_filter_chroma_intra_12_c", "_h264_h_loop_filter_chroma422_intra_12_c", "_h264_h_loop_filter_chroma_mbaff_intra_12_c", "_h264_h_loop_filter_chroma422_mbaff_intra_12_c", "_h264_v_loop_filter_luma_intra_14_c", "_h264_h_loop_filter_luma_intra_14_c", "_h264_h_loop_filter_luma_mbaff_intra_14_c", "_h264_v_loop_filter_chroma_intra_14_c", "_h264_h_loop_filter_chroma_intra_14_c", "_h264_h_loop_filter_chroma422_intra_14_c", "_h264_h_loop_filter_chroma_mbaff_intra_14_c", "_h264_h_loop_filter_chroma422_mbaff_intra_14_c", "_h264_v_loop_filter_luma_intra_8_c", "_h264_h_loop_filter_luma_intra_8_c", "_h264_h_loop_filter_luma_mbaff_intra_8_c", "_h264_v_loop_filter_chroma_intra_8_c", "_h264_h_loop_filter_chroma_intra_8_c", "_h264_h_loop_filter_chroma422_intra_8_c", "_h264_h_loop_filter_chroma_mbaff_intra_8_c", "_h264_h_loop_filter_chroma422_mbaff_intra_8_c", "_fft15_c", "_mdct15", "_imdct15_half", "_ps_mul_pair_single_c", "_ps_hybrid_analysis_ileave_c", "_ps_hybrid_synthesis_deint_c", "_vector_fmul_c", "_vector_dmul_c", "_vector_fmul_reverse_c", "_av_log_default_callback", "_mix6to2_s16", "_mix8to2_s16", "_mix6to2_clip_s16", "_mix8to2_clip_s16", "_mix6to2_float", "_mix8to2_float", "_mix6to2_double", "_mix8to2_double", "_mix6to2_s32", "_mix8to2_s32", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viiiifii = [0, "jsCall_viiiifii_0", "jsCall_viiiifii_1", "jsCall_viiiifii_2", "jsCall_viiiifii_3", "jsCall_viiiifii_4", "jsCall_viiiifii_5", "jsCall_viiiifii_6", "jsCall_viiiifii_7", "jsCall_viiiifii_8", "jsCall_viiiifii_9", "jsCall_viiiifii_10", "jsCall_viiiifii_11", "jsCall_viiiifii_12", "jsCall_viiiifii_13", "jsCall_viiiifii_14", "jsCall_viiiifii_15", "jsCall_viiiifii_16", "jsCall_viiiifii_17", "jsCall_viiiifii_18", "jsCall_viiiifii_19", "jsCall_viiiifii_20", "jsCall_viiiifii_21", "jsCall_viiiifii_22", "jsCall_viiiifii_23", "jsCall_viiiifii_24", "jsCall_viiiifii_25", "jsCall_viiiifii_26", "jsCall_viiiifii_27", "jsCall_viiiifii_28", "jsCall_viiiifii_29", "jsCall_viiiifii_30", "jsCall_viiiifii_31", "jsCall_viiiifii_32", "jsCall_viiiifii_33", "jsCall_viiiifii_34", "_sbr_hf_gen_c", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viiiii = [0, "jsCall_viiiii_0", "jsCall_viiiii_1", "jsCall_viiiii_2", "jsCall_viiiii_3", "jsCall_viiiii_4", "jsCall_viiiii_5", "jsCall_viiiii_6", "jsCall_viiiii_7", "jsCall_viiiii_8", "jsCall_viiiii_9", "jsCall_viiiii_10", "jsCall_viiiii_11", "jsCall_viiiii_12", "jsCall_viiiii_13", "jsCall_viiiii_14", "jsCall_viiiii_15", "jsCall_viiiii_16", "jsCall_viiiii_17", "jsCall_viiiii_18", "jsCall_viiiii_19", "jsCall_viiiii_20", "jsCall_viiiii_21", "jsCall_viiiii_22", "jsCall_viiiii_23", "jsCall_viiiii_24", "jsCall_viiiii_25", "jsCall_viiiii_26", "jsCall_viiiii_27", "jsCall_viiiii_28", "jsCall_viiiii_29", "jsCall_viiiii_30", "jsCall_viiiii_31", "jsCall_viiiii_32", "jsCall_viiiii_33", "jsCall_viiiii_34", "_conv_AV_SAMPLE_FMT_U8_to_AV_SAMPLE_FMT_U8", "_conv_AV_SAMPLE_FMT_U8_to_AV_SAMPLE_FMT_S16", "_conv_AV_SAMPLE_FMT_U8_to_AV_SAMPLE_FMT_S32", "_conv_AV_SAMPLE_FMT_U8_to_AV_SAMPLE_FMT_FLT", "_conv_AV_SAMPLE_FMT_U8_to_AV_SAMPLE_FMT_DBL", "_conv_AV_SAMPLE_FMT_U8_to_AV_SAMPLE_FMT_S64", "_conv_AV_SAMPLE_FMT_S16_to_AV_SAMPLE_FMT_U8", "_conv_AV_SAMPLE_FMT_S16_to_AV_SAMPLE_FMT_S16", "_conv_AV_SAMPLE_FMT_S16_to_AV_SAMPLE_FMT_S32", "_conv_AV_SAMPLE_FMT_S16_to_AV_SAMPLE_FMT_FLT", "_conv_AV_SAMPLE_FMT_S16_to_AV_SAMPLE_FMT_DBL", "_conv_AV_SAMPLE_FMT_S16_to_AV_SAMPLE_FMT_S64", "_conv_AV_SAMPLE_FMT_S32_to_AV_SAMPLE_FMT_U8", "_conv_AV_SAMPLE_FMT_S32_to_AV_SAMPLE_FMT_S16", "_conv_AV_SAMPLE_FMT_S32_to_AV_SAMPLE_FMT_S32", "_conv_AV_SAMPLE_FMT_S32_to_AV_SAMPLE_FMT_FLT", "_conv_AV_SAMPLE_FMT_S32_to_AV_SAMPLE_FMT_DBL", "_conv_AV_SAMPLE_FMT_S32_to_AV_SAMPLE_FMT_S64", "_conv_AV_SAMPLE_FMT_FLT_to_AV_SAMPLE_FMT_U8", "_conv_AV_SAMPLE_FMT_FLT_to_AV_SAMPLE_FMT_S16", "_conv_AV_SAMPLE_FMT_FLT_to_AV_SAMPLE_FMT_S32", "_conv_AV_SAMPLE_FMT_FLT_to_AV_SAMPLE_FMT_FLT", "_conv_AV_SAMPLE_FMT_FLT_to_AV_SAMPLE_FMT_DBL", "_conv_AV_SAMPLE_FMT_FLT_to_AV_SAMPLE_FMT_S64", "_conv_AV_SAMPLE_FMT_DBL_to_AV_SAMPLE_FMT_U8", "_conv_AV_SAMPLE_FMT_DBL_to_AV_SAMPLE_FMT_S16", "_conv_AV_SAMPLE_FMT_DBL_to_AV_SAMPLE_FMT_S32", "_conv_AV_SAMPLE_FMT_DBL_to_AV_SAMPLE_FMT_FLT", "_conv_AV_SAMPLE_FMT_DBL_to_AV_SAMPLE_FMT_DBL", "_conv_AV_SAMPLE_FMT_DBL_to_AV_SAMPLE_FMT_S64", "_conv_AV_SAMPLE_FMT_S64_to_AV_SAMPLE_FMT_U8", "_conv_AV_SAMPLE_FMT_S64_to_AV_SAMPLE_FMT_S16", "_conv_AV_SAMPLE_FMT_S64_to_AV_SAMPLE_FMT_S32", "_conv_AV_SAMPLE_FMT_S64_to_AV_SAMPLE_FMT_FLT", "_conv_AV_SAMPLE_FMT_S64_to_AV_SAMPLE_FMT_DBL", "_conv_AV_SAMPLE_FMT_S64_to_AV_SAMPLE_FMT_S64", "_planar_rgb9le_to_uv", "_planar_rgb10le_to_uv", "_planar_rgb12le_to_uv", "_planar_rgb14le_to_uv", "_planar_rgb16le_to_uv", "_planar_rgb9be_to_uv", "_planar_rgb10be_to_uv", "_planar_rgb12be_to_uv", "_planar_rgb14be_to_uv", "_planar_rgb16be_to_uv", "_planar_rgb_to_uv", "_yuv2p010l1_LE_c", "_yuv2p010l1_BE_c", "_yuv2plane1_16LE_c", "_yuv2plane1_16BE_c", "_yuv2plane1_9LE_c", "_yuv2plane1_9BE_c", "_yuv2plane1_10LE_c", "_yuv2plane1_10BE_c", "_yuv2plane1_12LE_c", "_yuv2plane1_12BE_c", "_yuv2plane1_14LE_c", "_yuv2plane1_14BE_c", "_yuv2plane1_floatBE_c", "_yuv2plane1_floatLE_c", "_yuv2plane1_8_c", "_bayer_bggr8_to_rgb24_copy", "_bayer_bggr8_to_rgb24_interpolate", "_bayer_bggr16le_to_rgb24_copy", "_bayer_bggr16le_to_rgb24_interpolate", "_bayer_bggr16be_to_rgb24_copy", "_bayer_bggr16be_to_rgb24_interpolate", "_bayer_rggb8_to_rgb24_copy", "_bayer_rggb8_to_rgb24_interpolate", "_bayer_rggb16le_to_rgb24_copy", "_bayer_rggb16le_to_rgb24_interpolate", "_bayer_rggb16be_to_rgb24_copy", "_bayer_rggb16be_to_rgb24_interpolate", "_bayer_gbrg8_to_rgb24_copy", "_bayer_gbrg8_to_rgb24_interpolate", "_bayer_gbrg16le_to_rgb24_copy", "_bayer_gbrg16le_to_rgb24_interpolate", "_bayer_gbrg16be_to_rgb24_copy", "_bayer_gbrg16be_to_rgb24_interpolate", "_bayer_grbg8_to_rgb24_copy", "_bayer_grbg8_to_rgb24_interpolate", "_bayer_grbg16le_to_rgb24_copy", "_bayer_grbg16le_to_rgb24_interpolate", "_bayer_grbg16be_to_rgb24_copy", "_bayer_grbg16be_to_rgb24_interpolate", "_hevc_h_loop_filter_chroma_9", "_hevc_v_loop_filter_chroma_9", "_hevc_h_loop_filter_chroma_10", "_hevc_v_loop_filter_chroma_10", "_hevc_h_loop_filter_chroma_12", "_hevc_v_loop_filter_chroma_12", "_hevc_h_loop_filter_chroma_8", "_hevc_v_loop_filter_chroma_8", "_ff_mpadsp_apply_window_float", "_ff_mpadsp_apply_window_fixed", "_worker_func", "_sbr_hf_assemble", "_sbr_hf_inverse_filter", "_ff_h264_idct_add16_9_c", "_ff_h264_idct8_add4_9_c", "_ff_h264_idct_add8_9_c", "_ff_h264_idct_add8_422_9_c", "_ff_h264_idct_add16intra_9_c", "_h264_v_loop_filter_luma_9_c", "_h264_h_loop_filter_luma_9_c", "_h264_h_loop_filter_luma_mbaff_9_c", "_h264_v_loop_filter_chroma_9_c", "_h264_h_loop_filter_chroma_9_c", "_h264_h_loop_filter_chroma422_9_c", "_h264_h_loop_filter_chroma_mbaff_9_c", "_h264_h_loop_filter_chroma422_mbaff_9_c", "_ff_h264_idct_add16_10_c", "_ff_h264_idct8_add4_10_c", "_ff_h264_idct_add8_10_c", "_ff_h264_idct_add8_422_10_c", "_ff_h264_idct_add16intra_10_c", "_h264_v_loop_filter_luma_10_c", "_h264_h_loop_filter_luma_10_c", "_h264_h_loop_filter_luma_mbaff_10_c", "_h264_v_loop_filter_chroma_10_c", "_h264_h_loop_filter_chroma_10_c", "_h264_h_loop_filter_chroma422_10_c", "_h264_h_loop_filter_chroma_mbaff_10_c", "_h264_h_loop_filter_chroma422_mbaff_10_c", "_ff_h264_idct_add16_12_c", "_ff_h264_idct8_add4_12_c", "_ff_h264_idct_add8_12_c", "_ff_h264_idct_add8_422_12_c", "_ff_h264_idct_add16intra_12_c", "_h264_v_loop_filter_luma_12_c", "_h264_h_loop_filter_luma_12_c", "_h264_h_loop_filter_luma_mbaff_12_c", "_h264_v_loop_filter_chroma_12_c", "_h264_h_loop_filter_chroma_12_c", "_h264_h_loop_filter_chroma422_12_c", "_h264_h_loop_filter_chroma_mbaff_12_c", "_h264_h_loop_filter_chroma422_mbaff_12_c", "_ff_h264_idct_add16_14_c", "_ff_h264_idct8_add4_14_c", "_ff_h264_idct_add8_14_c", "_ff_h264_idct_add8_422_14_c", "_ff_h264_idct_add16intra_14_c", "_h264_v_loop_filter_luma_14_c", "_h264_h_loop_filter_luma_14_c", "_h264_h_loop_filter_luma_mbaff_14_c", "_h264_v_loop_filter_chroma_14_c", "_h264_h_loop_filter_chroma_14_c", "_h264_h_loop_filter_chroma422_14_c", "_h264_h_loop_filter_chroma_mbaff_14_c", "_h264_h_loop_filter_chroma422_mbaff_14_c", "_ff_h264_idct_add16_8_c", "_ff_h264_idct8_add4_8_c", "_ff_h264_idct_add8_8_c", "_ff_h264_idct_add8_422_8_c", "_ff_h264_idct_add16intra_8_c", "_h264_v_loop_filter_luma_8_c", "_h264_h_loop_filter_luma_8_c", "_h264_h_loop_filter_luma_mbaff_8_c", "_h264_v_loop_filter_chroma_8_c", "_h264_h_loop_filter_chroma_8_c", "_h264_h_loop_filter_chroma422_8_c", "_h264_h_loop_filter_chroma_mbaff_8_c", "_h264_h_loop_filter_chroma422_mbaff_8_c", "_postrotate_c", "_sbr_hf_g_filt_c", "_ps_hybrid_analysis_c", "_ps_stereo_interpolate_c", "_ps_stereo_interpolate_ipdopd_c", "_vector_fmul_window_c", "_vector_fmul_add_c", "_copy_s16", "_copy_clip_s16", "_copy_float", "_copy_double", "_copy_s32", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viiiiidd = [0, "jsCall_viiiiidd_0", "jsCall_viiiiidd_1", "jsCall_viiiiidd_2", "jsCall_viiiiidd_3", "jsCall_viiiiidd_4", "jsCall_viiiiidd_5", "jsCall_viiiiidd_6", "jsCall_viiiiidd_7", "jsCall_viiiiidd_8", "jsCall_viiiiidd_9", "jsCall_viiiiidd_10", "jsCall_viiiiidd_11", "jsCall_viiiiidd_12", "jsCall_viiiiidd_13", "jsCall_viiiiidd_14", "jsCall_viiiiidd_15", "jsCall_viiiiidd_16", "jsCall_viiiiidd_17", "jsCall_viiiiidd_18", "jsCall_viiiiidd_19", "jsCall_viiiiidd_20", "jsCall_viiiiidd_21", "jsCall_viiiiidd_22", "jsCall_viiiiidd_23", "jsCall_viiiiidd_24", "jsCall_viiiiidd_25", "jsCall_viiiiidd_26", "jsCall_viiiiidd_27", "jsCall_viiiiidd_28", "jsCall_viiiiidd_29", "jsCall_viiiiidd_30", "jsCall_viiiiidd_31", "jsCall_viiiiidd_32", "jsCall_viiiiidd_33", "jsCall_viiiiidd_34", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viiiiiddi = [0, "jsCall_viiiiiddi_0", "jsCall_viiiiiddi_1", "jsCall_viiiiiddi_2", "jsCall_viiiiiddi_3", "jsCall_viiiiiddi_4", "jsCall_viiiiiddi_5", "jsCall_viiiiiddi_6", "jsCall_viiiiiddi_7", "jsCall_viiiiiddi_8", "jsCall_viiiiiddi_9", "jsCall_viiiiiddi_10", "jsCall_viiiiiddi_11", "jsCall_viiiiiddi_12", "jsCall_viiiiiddi_13", "jsCall_viiiiiddi_14", "jsCall_viiiiiddi_15", "jsCall_viiiiiddi_16", "jsCall_viiiiiddi_17", "jsCall_viiiiiddi_18", "jsCall_viiiiiddi_19", "jsCall_viiiiiddi_20", "jsCall_viiiiiddi_21", "jsCall_viiiiiddi_22", "jsCall_viiiiiddi_23", "jsCall_viiiiiddi_24", "jsCall_viiiiiddi_25", "jsCall_viiiiiddi_26", "jsCall_viiiiiddi_27", "jsCall_viiiiiddi_28", "jsCall_viiiiiddi_29", "jsCall_viiiiiddi_30", "jsCall_viiiiiddi_31", "jsCall_viiiiiddi_32", "jsCall_viiiiiddi_33", "jsCall_viiiiiddi_34", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viiiiii = [0, "jsCall_viiiiii_0", "jsCall_viiiiii_1", "jsCall_viiiiii_2", "jsCall_viiiiii_3", "jsCall_viiiiii_4", "jsCall_viiiiii_5", "jsCall_viiiiii_6", "jsCall_viiiiii_7", "jsCall_viiiiii_8", "jsCall_viiiiii_9", "jsCall_viiiiii_10", "jsCall_viiiiii_11", "jsCall_viiiiii_12", "jsCall_viiiiii_13", "jsCall_viiiiii_14", "jsCall_viiiiii_15", "jsCall_viiiiii_16", "jsCall_viiiiii_17", "jsCall_viiiiii_18", "jsCall_viiiiii_19", "jsCall_viiiiii_20", "jsCall_viiiiii_21", "jsCall_viiiiii_22", "jsCall_viiiiii_23", "jsCall_viiiiii_24", "jsCall_viiiiii_25", "jsCall_viiiiii_26", "jsCall_viiiiii_27", "jsCall_viiiiii_28", "jsCall_viiiiii_29", "jsCall_viiiiii_30", "jsCall_viiiiii_31", "jsCall_viiiiii_32", "jsCall_viiiiii_33", "jsCall_viiiiii_34", "_read_geobtag", "_read_apic", "_read_chapter", "_read_priv", "_ff_hyscale_fast_c", "_bswap16Y_c", "_read_ya16le_gray_c", "_read_ya16be_gray_c", "_read_ayuv64le_Y_c", "_yuy2ToY_c", "_uyvyToY_c", "_bgr24ToY_c", "_bgr16leToY_c", "_bgr16beToY_c", "_bgr15leToY_c", "_bgr15beToY_c", "_bgr12leToY_c", "_bgr12beToY_c", "_rgb24ToY_c", "_rgb16leToY_c", "_rgb16beToY_c", "_rgb15leToY_c", "_rgb15beToY_c", "_rgb12leToY_c", "_rgb12beToY_c", "_palToY_c", "_monoblack2Y_c", "_monowhite2Y_c", "_bgr32ToY_c", "_bgr321ToY_c", "_rgb32ToY_c", "_rgb321ToY_c", "_rgb48BEToY_c", "_rgb48LEToY_c", "_bgr48BEToY_c", "_bgr48LEToY_c", "_rgb64BEToY_c", "_rgb64LEToY_c", "_bgr64BEToY_c", "_bgr64LEToY_c", "_p010LEToY_c", "_p010BEToY_c", "_grayf32ToY16_c", "_grayf32ToY16_bswap_c", "_rgba64leToA_c", "_rgba64beToA_c", "_rgbaToA_c", "_abgrToA_c", "_read_ya16le_alpha_c", "_read_ya16be_alpha_c", "_read_ayuv64le_A_c", "_palToA_c", "_put_pcm_9", "_hevc_h_loop_filter_luma_9", "_hevc_v_loop_filter_luma_9", "_put_pcm_10", "_hevc_h_loop_filter_luma_10", "_hevc_v_loop_filter_luma_10", "_put_pcm_12", "_hevc_h_loop_filter_luma_12", "_hevc_v_loop_filter_luma_12", "_put_pcm_8", "_hevc_h_loop_filter_luma_8", "_hevc_v_loop_filter_luma_8", "_pred_dc_9", "_pred_angular_0_9", "_pred_angular_1_9", "_pred_angular_2_9", "_pred_angular_3_9", "_pred_dc_10", "_pred_angular_0_10", "_pred_angular_1_10", "_pred_angular_2_10", "_pred_angular_3_10", "_pred_dc_12", "_pred_angular_0_12", "_pred_angular_1_12", "_pred_angular_2_12", "_pred_angular_3_12", "_pred_dc_8", "_pred_angular_0_8", "_pred_angular_1_8", "_pred_angular_2_8", "_pred_angular_3_8", "_ff_imdct36_blocks_float", "_ff_imdct36_blocks_fixed", "_weight_h264_pixels16_9_c", "_weight_h264_pixels8_9_c", "_weight_h264_pixels4_9_c", "_weight_h264_pixels2_9_c", "_weight_h264_pixels16_10_c", "_weight_h264_pixels8_10_c", "_weight_h264_pixels4_10_c", "_weight_h264_pixels2_10_c", "_weight_h264_pixels16_12_c", "_weight_h264_pixels8_12_c", "_weight_h264_pixels4_12_c", "_weight_h264_pixels2_12_c", "_weight_h264_pixels16_14_c", "_weight_h264_pixels8_14_c", "_weight_h264_pixels4_14_c", "_weight_h264_pixels2_14_c", "_weight_h264_pixels16_8_c", "_weight_h264_pixels8_8_c", "_weight_h264_pixels4_8_c", "_weight_h264_pixels2_8_c", "_sbr_hf_apply_noise_0", "_sbr_hf_apply_noise_1", "_sbr_hf_apply_noise_2", "_sbr_hf_apply_noise_3", "_aes_decrypt", "_aes_encrypt", "_image_copy_plane", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viiiiiifi = [0, "jsCall_viiiiiifi_0", "jsCall_viiiiiifi_1", "jsCall_viiiiiifi_2", "jsCall_viiiiiifi_3", "jsCall_viiiiiifi_4", "jsCall_viiiiiifi_5", "jsCall_viiiiiifi_6", "jsCall_viiiiiifi_7", "jsCall_viiiiiifi_8", "jsCall_viiiiiifi_9", "jsCall_viiiiiifi_10", "jsCall_viiiiiifi_11", "jsCall_viiiiiifi_12", "jsCall_viiiiiifi_13", "jsCall_viiiiiifi_14", "jsCall_viiiiiifi_15", "jsCall_viiiiiifi_16", "jsCall_viiiiiifi_17", "jsCall_viiiiiifi_18", "jsCall_viiiiiifi_19", "jsCall_viiiiiifi_20", "jsCall_viiiiiifi_21", "jsCall_viiiiiifi_22", "jsCall_viiiiiifi_23", "jsCall_viiiiiifi_24", "jsCall_viiiiiifi_25", "jsCall_viiiiiifi_26", "jsCall_viiiiiifi_27", "jsCall_viiiiiifi_28", "jsCall_viiiiiifi_29", "jsCall_viiiiiifi_30", "jsCall_viiiiiifi_31", "jsCall_viiiiiifi_32", "jsCall_viiiiiifi_33", "jsCall_viiiiiifi_34", "_ps_decorrelate_c", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viiiiiii = [0, "jsCall_viiiiiii_0", "jsCall_viiiiiii_1", "jsCall_viiiiiii_2", "jsCall_viiiiiii_3", "jsCall_viiiiiii_4", "jsCall_viiiiiii_5", "jsCall_viiiiiii_6", "jsCall_viiiiiii_7", "jsCall_viiiiiii_8", "jsCall_viiiiiii_9", "jsCall_viiiiiii_10", "jsCall_viiiiiii_11", "jsCall_viiiiiii_12", "jsCall_viiiiiii_13", "jsCall_viiiiiii_14", "jsCall_viiiiiii_15", "jsCall_viiiiiii_16", "jsCall_viiiiiii_17", "jsCall_viiiiiii_18", "jsCall_viiiiiii_19", "jsCall_viiiiiii_20", "jsCall_viiiiiii_21", "jsCall_viiiiiii_22", "jsCall_viiiiiii_23", "jsCall_viiiiiii_24", "jsCall_viiiiiii_25", "jsCall_viiiiiii_26", "jsCall_viiiiiii_27", "jsCall_viiiiiii_28", "jsCall_viiiiiii_29", "jsCall_viiiiiii_30", "jsCall_viiiiiii_31", "jsCall_viiiiiii_32", "jsCall_viiiiiii_33", "jsCall_viiiiiii_34", "_hScale8To15_c", "_hScale8To19_c", "_hScale16To19_c", "_hScale16To15_c", "_yuy2ToUV_c", "_yvy2ToUV_c", "_uyvyToUV_c", "_nv12ToUV_c", "_nv21ToUV_c", "_palToUV_c", "_bswap16UV_c", "_read_ayuv64le_UV_c", "_p010LEToUV_c", "_p010BEToUV_c", "_p016LEToUV_c", "_p016BEToUV_c", "_gbr24pToUV_half_c", "_rgb64BEToUV_half_c", "_rgb64LEToUV_half_c", "_bgr64BEToUV_half_c", "_bgr64LEToUV_half_c", "_rgb48BEToUV_half_c", "_rgb48LEToUV_half_c", "_bgr48BEToUV_half_c", "_bgr48LEToUV_half_c", "_bgr32ToUV_half_c", "_bgr321ToUV_half_c", "_bgr24ToUV_half_c", "_bgr16leToUV_half_c", "_bgr16beToUV_half_c", "_bgr15leToUV_half_c", "_bgr15beToUV_half_c", "_bgr12leToUV_half_c", "_bgr12beToUV_half_c", "_rgb32ToUV_half_c", "_rgb321ToUV_half_c", "_rgb24ToUV_half_c", "_rgb16leToUV_half_c", "_rgb16beToUV_half_c", "_rgb15leToUV_half_c", "_rgb15beToUV_half_c", "_rgb12leToUV_half_c", "_rgb12beToUV_half_c", "_rgb64BEToUV_c", "_rgb64LEToUV_c", "_bgr64BEToUV_c", "_bgr64LEToUV_c", "_rgb48BEToUV_c", "_rgb48LEToUV_c", "_bgr48BEToUV_c", "_bgr48LEToUV_c", "_bgr32ToUV_c", "_bgr321ToUV_c", "_bgr24ToUV_c", "_bgr16leToUV_c", "_bgr16beToUV_c", "_bgr15leToUV_c", "_bgr15beToUV_c", "_bgr12leToUV_c", "_bgr12beToUV_c", "_rgb32ToUV_c", "_rgb321ToUV_c", "_rgb24ToUV_c", "_rgb16leToUV_c", "_rgb16beToUV_c", "_rgb15leToUV_c", "_rgb15beToUV_c", "_rgb12leToUV_c", "_rgb12beToUV_c", "_yuv2p010lX_LE_c", "_yuv2p010lX_BE_c", "_yuv2p010cX_c", "_yuv2planeX_16LE_c", "_yuv2planeX_16BE_c", "_yuv2p016cX_c", "_yuv2planeX_9LE_c", "_yuv2planeX_9BE_c", "_yuv2planeX_10LE_c", "_yuv2planeX_10BE_c", "_yuv2planeX_12LE_c", "_yuv2planeX_12BE_c", "_yuv2planeX_14LE_c", "_yuv2planeX_14BE_c", "_yuv2planeX_floatBE_c", "_yuv2planeX_floatLE_c", "_yuv2planeX_8_c", "_yuv2nv12cX_c", "_sao_edge_filter_9", "_put_hevc_pel_pixels_9", "_put_hevc_qpel_h_9", "_put_hevc_qpel_v_9", "_put_hevc_qpel_hv_9", "_put_hevc_epel_h_9", "_put_hevc_epel_v_9", "_put_hevc_epel_hv_9", "_sao_edge_filter_10", "_put_hevc_pel_pixels_10", "_put_hevc_qpel_h_10", "_put_hevc_qpel_v_10", "_put_hevc_qpel_hv_10", "_put_hevc_epel_h_10", "_put_hevc_epel_v_10", "_put_hevc_epel_hv_10", "_sao_edge_filter_12", "_put_hevc_pel_pixels_12", "_put_hevc_qpel_h_12", "_put_hevc_qpel_v_12", "_put_hevc_qpel_hv_12", "_put_hevc_epel_h_12", "_put_hevc_epel_v_12", "_put_hevc_epel_hv_12", "_sao_edge_filter_8", "_put_hevc_pel_pixels_8", "_put_hevc_qpel_h_8", "_put_hevc_qpel_v_8", "_put_hevc_qpel_hv_8", "_put_hevc_epel_h_8", "_put_hevc_epel_v_8", "_put_hevc_epel_hv_8", "_sum2_s16", "_sum2_clip_s16", "_sum2_float", "_sum2_double", "_sum2_s32", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viiiiiiii = [0, "jsCall_viiiiiiii_0", "jsCall_viiiiiiii_1", "jsCall_viiiiiiii_2", "jsCall_viiiiiiii_3", "jsCall_viiiiiiii_4", "jsCall_viiiiiiii_5", "jsCall_viiiiiiii_6", "jsCall_viiiiiiii_7", "jsCall_viiiiiiii_8", "jsCall_viiiiiiii_9", "jsCall_viiiiiiii_10", "jsCall_viiiiiiii_11", "jsCall_viiiiiiii_12", "jsCall_viiiiiiii_13", "jsCall_viiiiiiii_14", "jsCall_viiiiiiii_15", "jsCall_viiiiiiii_16", "jsCall_viiiiiiii_17", "jsCall_viiiiiiii_18", "jsCall_viiiiiiii_19", "jsCall_viiiiiiii_20", "jsCall_viiiiiiii_21", "jsCall_viiiiiiii_22", "jsCall_viiiiiiii_23", "jsCall_viiiiiiii_24", "jsCall_viiiiiiii_25", "jsCall_viiiiiiii_26", "jsCall_viiiiiiii_27", "jsCall_viiiiiiii_28", "jsCall_viiiiiiii_29", "jsCall_viiiiiiii_30", "jsCall_viiiiiiii_31", "jsCall_viiiiiiii_32", "jsCall_viiiiiiii_33", "jsCall_viiiiiiii_34", "_ff_hcscale_fast_c", "_bayer_bggr8_to_yv12_copy", "_bayer_bggr8_to_yv12_interpolate", "_bayer_bggr16le_to_yv12_copy", "_bayer_bggr16le_to_yv12_interpolate", "_bayer_bggr16be_to_yv12_copy", "_bayer_bggr16be_to_yv12_interpolate", "_bayer_rggb8_to_yv12_copy", "_bayer_rggb8_to_yv12_interpolate", "_bayer_rggb16le_to_yv12_copy", "_bayer_rggb16le_to_yv12_interpolate", "_bayer_rggb16be_to_yv12_copy", "_bayer_rggb16be_to_yv12_interpolate", "_bayer_gbrg8_to_yv12_copy", "_bayer_gbrg8_to_yv12_interpolate", "_bayer_gbrg16le_to_yv12_copy", "_bayer_gbrg16le_to_yv12_interpolate", "_bayer_gbrg16be_to_yv12_copy", "_bayer_gbrg16be_to_yv12_interpolate", "_bayer_grbg8_to_yv12_copy", "_bayer_grbg8_to_yv12_interpolate", "_bayer_grbg16le_to_yv12_copy", "_bayer_grbg16le_to_yv12_interpolate", "_bayer_grbg16be_to_yv12_copy", "_bayer_grbg16be_to_yv12_interpolate", "_sao_band_filter_9", "_put_hevc_pel_uni_pixels_9", "_put_hevc_qpel_uni_h_9", "_put_hevc_qpel_uni_v_9", "_put_hevc_qpel_uni_hv_9", "_put_hevc_epel_uni_h_9", "_put_hevc_epel_uni_v_9", "_put_hevc_epel_uni_hv_9", "_sao_band_filter_10", "_put_hevc_pel_uni_pixels_10", "_put_hevc_qpel_uni_h_10", "_put_hevc_qpel_uni_v_10", "_put_hevc_qpel_uni_hv_10", "_put_hevc_epel_uni_h_10", "_put_hevc_epel_uni_v_10", "_put_hevc_epel_uni_hv_10", "_sao_band_filter_12", "_put_hevc_pel_uni_pixels_12", "_put_hevc_qpel_uni_h_12", "_put_hevc_qpel_uni_v_12", "_put_hevc_qpel_uni_hv_12", "_put_hevc_epel_uni_h_12", "_put_hevc_epel_uni_v_12", "_put_hevc_epel_uni_hv_12", "_sao_band_filter_8", "_put_hevc_pel_uni_pixels_8", "_put_hevc_qpel_uni_h_8", "_put_hevc_qpel_uni_v_8", "_put_hevc_qpel_uni_hv_8", "_put_hevc_epel_uni_h_8", "_put_hevc_epel_uni_v_8", "_put_hevc_epel_uni_hv_8", "_biweight_h264_pixels16_9_c", "_biweight_h264_pixels8_9_c", "_biweight_h264_pixels4_9_c", "_biweight_h264_pixels2_9_c", "_biweight_h264_pixels16_10_c", "_biweight_h264_pixels8_10_c", "_biweight_h264_pixels4_10_c", "_biweight_h264_pixels2_10_c", "_biweight_h264_pixels16_12_c", "_biweight_h264_pixels8_12_c", "_biweight_h264_pixels4_12_c", "_biweight_h264_pixels2_12_c", "_biweight_h264_pixels16_14_c", "_biweight_h264_pixels8_14_c", "_biweight_h264_pixels4_14_c", "_biweight_h264_pixels2_14_c", "_biweight_h264_pixels16_8_c", "_biweight_h264_pixels8_8_c", "_biweight_h264_pixels4_8_c", "_biweight_h264_pixels2_8_c", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viiiiiiiid = [0, "jsCall_viiiiiiiid_0", "jsCall_viiiiiiiid_1", "jsCall_viiiiiiiid_2", "jsCall_viiiiiiiid_3", "jsCall_viiiiiiiid_4", "jsCall_viiiiiiiid_5", "jsCall_viiiiiiiid_6", "jsCall_viiiiiiiid_7", "jsCall_viiiiiiiid_8", "jsCall_viiiiiiiid_9", "jsCall_viiiiiiiid_10", "jsCall_viiiiiiiid_11", "jsCall_viiiiiiiid_12", "jsCall_viiiiiiiid_13", "jsCall_viiiiiiiid_14", "jsCall_viiiiiiiid_15", "jsCall_viiiiiiiid_16", "jsCall_viiiiiiiid_17", "jsCall_viiiiiiiid_18", "jsCall_viiiiiiiid_19", "jsCall_viiiiiiiid_20", "jsCall_viiiiiiiid_21", "jsCall_viiiiiiiid_22", "jsCall_viiiiiiiid_23", "jsCall_viiiiiiiid_24", "jsCall_viiiiiiiid_25", "jsCall_viiiiiiiid_26", "jsCall_viiiiiiiid_27", "jsCall_viiiiiiiid_28", "jsCall_viiiiiiiid_29", "jsCall_viiiiiiiid_30", "jsCall_viiiiiiiid_31", "jsCall_viiiiiiiid_32", "jsCall_viiiiiiiid_33", "jsCall_viiiiiiiid_34", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viiiiiiiidi = [0, "jsCall_viiiiiiiidi_0", "jsCall_viiiiiiiidi_1", "jsCall_viiiiiiiidi_2", "jsCall_viiiiiiiidi_3", "jsCall_viiiiiiiidi_4", "jsCall_viiiiiiiidi_5", "jsCall_viiiiiiiidi_6", "jsCall_viiiiiiiidi_7", "jsCall_viiiiiiiidi_8", "jsCall_viiiiiiiidi_9", "jsCall_viiiiiiiidi_10", "jsCall_viiiiiiiidi_11", "jsCall_viiiiiiiidi_12", "jsCall_viiiiiiiidi_13", "jsCall_viiiiiiiidi_14", "jsCall_viiiiiiiidi_15", "jsCall_viiiiiiiidi_16", "jsCall_viiiiiiiidi_17", "jsCall_viiiiiiiidi_18", "jsCall_viiiiiiiidi_19", "jsCall_viiiiiiiidi_20", "jsCall_viiiiiiiidi_21", "jsCall_viiiiiiiidi_22", "jsCall_viiiiiiiidi_23", "jsCall_viiiiiiiidi_24", "jsCall_viiiiiiiidi_25", "jsCall_viiiiiiiidi_26", "jsCall_viiiiiiiidi_27", "jsCall_viiiiiiiidi_28", "jsCall_viiiiiiiidi_29", "jsCall_viiiiiiiidi_30", "jsCall_viiiiiiiidi_31", "jsCall_viiiiiiiidi_32", "jsCall_viiiiiiiidi_33", "jsCall_viiiiiiiidi_34", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viiiiiiiii = [0, "jsCall_viiiiiiiii_0", "jsCall_viiiiiiiii_1", "jsCall_viiiiiiiii_2", "jsCall_viiiiiiiii_3", "jsCall_viiiiiiiii_4", "jsCall_viiiiiiiii_5", "jsCall_viiiiiiiii_6", "jsCall_viiiiiiiii_7", "jsCall_viiiiiiiii_8", "jsCall_viiiiiiiii_9", "jsCall_viiiiiiiii_10", "jsCall_viiiiiiiii_11", "jsCall_viiiiiiiii_12", "jsCall_viiiiiiiii_13", "jsCall_viiiiiiiii_14", "jsCall_viiiiiiiii_15", "jsCall_viiiiiiiii_16", "jsCall_viiiiiiiii_17", "jsCall_viiiiiiiii_18", "jsCall_viiiiiiiii_19", "jsCall_viiiiiiiii_20", "jsCall_viiiiiiiii_21", "jsCall_viiiiiiiii_22", "jsCall_viiiiiiiii_23", "jsCall_viiiiiiiii_24", "jsCall_viiiiiiiii_25", "jsCall_viiiiiiiii_26", "jsCall_viiiiiiiii_27", "jsCall_viiiiiiiii_28", "jsCall_viiiiiiiii_29", "jsCall_viiiiiiiii_30", "jsCall_viiiiiiiii_31", "jsCall_viiiiiiiii_32", "jsCall_viiiiiiiii_33", "jsCall_viiiiiiiii_34", "_yuv2rgba32_full_1_c", "_yuv2rgbx32_full_1_c", "_yuv2argb32_full_1_c", "_yuv2xrgb32_full_1_c", "_yuv2bgra32_full_1_c", "_yuv2bgrx32_full_1_c", "_yuv2abgr32_full_1_c", "_yuv2xbgr32_full_1_c", "_yuv2rgba64le_full_1_c", "_yuv2rgbx64le_full_1_c", "_yuv2rgba64be_full_1_c", "_yuv2rgbx64be_full_1_c", "_yuv2bgra64le_full_1_c", "_yuv2bgrx64le_full_1_c", "_yuv2bgra64be_full_1_c", "_yuv2bgrx64be_full_1_c", "_yuv2rgb24_full_1_c", "_yuv2bgr24_full_1_c", "_yuv2rgb48le_full_1_c", "_yuv2bgr48le_full_1_c", "_yuv2rgb48be_full_1_c", "_yuv2bgr48be_full_1_c", "_yuv2bgr4_byte_full_1_c", "_yuv2rgb4_byte_full_1_c", "_yuv2bgr8_full_1_c", "_yuv2rgb8_full_1_c", "_yuv2rgbx64le_1_c", "_yuv2rgba64le_1_c", "_yuv2rgbx64be_1_c", "_yuv2rgba64be_1_c", "_yuv2bgrx64le_1_c", "_yuv2bgra64le_1_c", "_yuv2bgrx64be_1_c", "_yuv2bgra64be_1_c", "_yuv2rgba32_1_c", "_yuv2rgbx32_1_c", "_yuv2rgba32_1_1_c", "_yuv2rgbx32_1_1_c", "_yuv2rgb16_1_c", "_yuv2rgb15_1_c", "_yuv2rgb12_1_c", "_yuv2rgb8_1_c", "_yuv2rgb4_1_c", "_yuv2rgb4b_1_c", "_yuv2rgb48le_1_c", "_yuv2rgb48be_1_c", "_yuv2bgr48le_1_c", "_yuv2bgr48be_1_c", "_yuv2rgb24_1_c", "_yuv2bgr24_1_c", "_yuv2monowhite_1_c", "_yuv2monoblack_1_c", "_yuv2yuyv422_1_c", "_yuv2yvyu422_1_c", "_yuv2uyvy422_1_c", "_yuv2ya8_1_c", "_yuv2ya16le_1_c", "_yuv2ya16be_1_c", "_yuy2toyv12_c", "_put_hevc_pel_bi_pixels_9", "_put_hevc_qpel_bi_h_9", "_put_hevc_qpel_bi_v_9", "_put_hevc_qpel_bi_hv_9", "_put_hevc_epel_bi_h_9", "_put_hevc_epel_bi_v_9", "_put_hevc_epel_bi_hv_9", "_put_hevc_pel_bi_pixels_10", "_put_hevc_qpel_bi_h_10", "_put_hevc_qpel_bi_v_10", "_put_hevc_qpel_bi_hv_10", "_put_hevc_epel_bi_h_10", "_put_hevc_epel_bi_v_10", "_put_hevc_epel_bi_hv_10", "_put_hevc_pel_bi_pixels_12", "_put_hevc_qpel_bi_h_12", "_put_hevc_qpel_bi_v_12", "_put_hevc_qpel_bi_hv_12", "_put_hevc_epel_bi_h_12", "_put_hevc_epel_bi_v_12", "_put_hevc_epel_bi_hv_12", "_put_hevc_pel_bi_pixels_8", "_put_hevc_qpel_bi_h_8", "_put_hevc_qpel_bi_v_8", "_put_hevc_qpel_bi_hv_8", "_put_hevc_epel_bi_h_8", "_put_hevc_epel_bi_v_8", "_put_hevc_epel_bi_hv_8", 0, 0, 0, 0, 0]; +var debug_table_viiiiiiiiii = [0, "jsCall_viiiiiiiiii_0", "jsCall_viiiiiiiiii_1", "jsCall_viiiiiiiiii_2", "jsCall_viiiiiiiiii_3", "jsCall_viiiiiiiiii_4", "jsCall_viiiiiiiiii_5", "jsCall_viiiiiiiiii_6", "jsCall_viiiiiiiiii_7", "jsCall_viiiiiiiiii_8", "jsCall_viiiiiiiiii_9", "jsCall_viiiiiiiiii_10", "jsCall_viiiiiiiiii_11", "jsCall_viiiiiiiiii_12", "jsCall_viiiiiiiiii_13", "jsCall_viiiiiiiiii_14", "jsCall_viiiiiiiiii_15", "jsCall_viiiiiiiiii_16", "jsCall_viiiiiiiiii_17", "jsCall_viiiiiiiiii_18", "jsCall_viiiiiiiiii_19", "jsCall_viiiiiiiiii_20", "jsCall_viiiiiiiiii_21", "jsCall_viiiiiiiiii_22", "jsCall_viiiiiiiiii_23", "jsCall_viiiiiiiiii_24", "jsCall_viiiiiiiiii_25", "jsCall_viiiiiiiiii_26", "jsCall_viiiiiiiiii_27", "jsCall_viiiiiiiiii_28", "jsCall_viiiiiiiiii_29", "jsCall_viiiiiiiiii_30", "jsCall_viiiiiiiiii_31", "jsCall_viiiiiiiiii_32", "jsCall_viiiiiiiiii_33", "jsCall_viiiiiiiiii_34", "_yuv2rgba32_full_2_c", "_yuv2rgbx32_full_2_c", "_yuv2argb32_full_2_c", "_yuv2xrgb32_full_2_c", "_yuv2bgra32_full_2_c", "_yuv2bgrx32_full_2_c", "_yuv2abgr32_full_2_c", "_yuv2xbgr32_full_2_c", "_yuv2rgba64le_full_2_c", "_yuv2rgbx64le_full_2_c", "_yuv2rgba64be_full_2_c", "_yuv2rgbx64be_full_2_c", "_yuv2bgra64le_full_2_c", "_yuv2bgrx64le_full_2_c", "_yuv2bgra64be_full_2_c", "_yuv2bgrx64be_full_2_c", "_yuv2rgb24_full_2_c", "_yuv2bgr24_full_2_c", "_yuv2rgb48le_full_2_c", "_yuv2bgr48le_full_2_c", "_yuv2rgb48be_full_2_c", "_yuv2bgr48be_full_2_c", "_yuv2bgr4_byte_full_2_c", "_yuv2rgb4_byte_full_2_c", "_yuv2bgr8_full_2_c", "_yuv2rgb8_full_2_c", "_yuv2rgbx64le_2_c", "_yuv2rgba64le_2_c", "_yuv2rgbx64be_2_c", "_yuv2rgba64be_2_c", "_yuv2bgrx64le_2_c", "_yuv2bgra64le_2_c", "_yuv2bgrx64be_2_c", "_yuv2bgra64be_2_c", "_yuv2rgba32_2_c", "_yuv2rgbx32_2_c", "_yuv2rgba32_1_2_c", "_yuv2rgbx32_1_2_c", "_yuv2rgb16_2_c", "_yuv2rgb15_2_c", "_yuv2rgb12_2_c", "_yuv2rgb8_2_c", "_yuv2rgb4_2_c", "_yuv2rgb4b_2_c", "_yuv2rgb48le_2_c", "_yuv2rgb48be_2_c", "_yuv2bgr48le_2_c", "_yuv2bgr48be_2_c", "_yuv2rgb24_2_c", "_yuv2bgr24_2_c", "_yuv2monowhite_2_c", "_yuv2monoblack_2_c", "_yuv2yuyv422_2_c", "_yuv2yvyu422_2_c", "_yuv2uyvy422_2_c", "_yuv2ya8_2_c", "_yuv2ya16le_2_c", "_yuv2ya16be_2_c", "_vu9_to_vu12_c", "_yvu9_to_yuy2_c", "_ff_emulated_edge_mc_8", "_ff_emulated_edge_mc_16", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viiiiiiiiiii = [0, "jsCall_viiiiiiiiiii_0", "jsCall_viiiiiiiiiii_1", "jsCall_viiiiiiiiiii_2", "jsCall_viiiiiiiiiii_3", "jsCall_viiiiiiiiiii_4", "jsCall_viiiiiiiiiii_5", "jsCall_viiiiiiiiiii_6", "jsCall_viiiiiiiiiii_7", "jsCall_viiiiiiiiiii_8", "jsCall_viiiiiiiiiii_9", "jsCall_viiiiiiiiiii_10", "jsCall_viiiiiiiiiii_11", "jsCall_viiiiiiiiiii_12", "jsCall_viiiiiiiiiii_13", "jsCall_viiiiiiiiiii_14", "jsCall_viiiiiiiiiii_15", "jsCall_viiiiiiiiiii_16", "jsCall_viiiiiiiiiii_17", "jsCall_viiiiiiiiiii_18", "jsCall_viiiiiiiiiii_19", "jsCall_viiiiiiiiiii_20", "jsCall_viiiiiiiiiii_21", "jsCall_viiiiiiiiiii_22", "jsCall_viiiiiiiiiii_23", "jsCall_viiiiiiiiiii_24", "jsCall_viiiiiiiiiii_25", "jsCall_viiiiiiiiiii_26", "jsCall_viiiiiiiiiii_27", "jsCall_viiiiiiiiiii_28", "jsCall_viiiiiiiiiii_29", "jsCall_viiiiiiiiiii_30", "jsCall_viiiiiiiiiii_31", "jsCall_viiiiiiiiiii_32", "jsCall_viiiiiiiiiii_33", "jsCall_viiiiiiiiiii_34", "_put_hevc_pel_uni_w_pixels_9", "_put_hevc_qpel_uni_w_h_9", "_put_hevc_qpel_uni_w_v_9", "_put_hevc_qpel_uni_w_hv_9", "_put_hevc_epel_uni_w_h_9", "_put_hevc_epel_uni_w_v_9", "_put_hevc_epel_uni_w_hv_9", "_put_hevc_pel_uni_w_pixels_10", "_put_hevc_qpel_uni_w_h_10", "_put_hevc_qpel_uni_w_v_10", "_put_hevc_qpel_uni_w_hv_10", "_put_hevc_epel_uni_w_h_10", "_put_hevc_epel_uni_w_v_10", "_put_hevc_epel_uni_w_hv_10", "_put_hevc_pel_uni_w_pixels_12", "_put_hevc_qpel_uni_w_h_12", "_put_hevc_qpel_uni_w_v_12", "_put_hevc_qpel_uni_w_hv_12", "_put_hevc_epel_uni_w_h_12", "_put_hevc_epel_uni_w_v_12", "_put_hevc_epel_uni_w_hv_12", "_put_hevc_pel_uni_w_pixels_8", "_put_hevc_qpel_uni_w_h_8", "_put_hevc_qpel_uni_w_v_8", "_put_hevc_qpel_uni_w_hv_8", "_put_hevc_epel_uni_w_h_8", "_put_hevc_epel_uni_w_v_8", "_put_hevc_epel_uni_w_hv_8"]; +var debug_table_viiiiiiiiiiii = [0, "jsCall_viiiiiiiiiiii_0", "jsCall_viiiiiiiiiiii_1", "jsCall_viiiiiiiiiiii_2", "jsCall_viiiiiiiiiiii_3", "jsCall_viiiiiiiiiiii_4", "jsCall_viiiiiiiiiiii_5", "jsCall_viiiiiiiiiiii_6", "jsCall_viiiiiiiiiiii_7", "jsCall_viiiiiiiiiiii_8", "jsCall_viiiiiiiiiiii_9", "jsCall_viiiiiiiiiiii_10", "jsCall_viiiiiiiiiiii_11", "jsCall_viiiiiiiiiiii_12", "jsCall_viiiiiiiiiiii_13", "jsCall_viiiiiiiiiiii_14", "jsCall_viiiiiiiiiiii_15", "jsCall_viiiiiiiiiiii_16", "jsCall_viiiiiiiiiiii_17", "jsCall_viiiiiiiiiiii_18", "jsCall_viiiiiiiiiiii_19", "jsCall_viiiiiiiiiiii_20", "jsCall_viiiiiiiiiiii_21", "jsCall_viiiiiiiiiiii_22", "jsCall_viiiiiiiiiiii_23", "jsCall_viiiiiiiiiiii_24", "jsCall_viiiiiiiiiiii_25", "jsCall_viiiiiiiiiiii_26", "jsCall_viiiiiiiiiiii_27", "jsCall_viiiiiiiiiiii_28", "jsCall_viiiiiiiiiiii_29", "jsCall_viiiiiiiiiiii_30", "jsCall_viiiiiiiiiiii_31", "jsCall_viiiiiiiiiiii_32", "jsCall_viiiiiiiiiiii_33", "jsCall_viiiiiiiiiiii_34", "_yuv2rgba32_full_X_c", "_yuv2rgbx32_full_X_c", "_yuv2argb32_full_X_c", "_yuv2xrgb32_full_X_c", "_yuv2bgra32_full_X_c", "_yuv2bgrx32_full_X_c", "_yuv2abgr32_full_X_c", "_yuv2xbgr32_full_X_c", "_yuv2rgba64le_full_X_c", "_yuv2rgbx64le_full_X_c", "_yuv2rgba64be_full_X_c", "_yuv2rgbx64be_full_X_c", "_yuv2bgra64le_full_X_c", "_yuv2bgrx64le_full_X_c", "_yuv2bgra64be_full_X_c", "_yuv2bgrx64be_full_X_c", "_yuv2rgb24_full_X_c", "_yuv2bgr24_full_X_c", "_yuv2rgb48le_full_X_c", "_yuv2bgr48le_full_X_c", "_yuv2rgb48be_full_X_c", "_yuv2bgr48be_full_X_c", "_yuv2bgr4_byte_full_X_c", "_yuv2rgb4_byte_full_X_c", "_yuv2bgr8_full_X_c", "_yuv2rgb8_full_X_c", "_yuv2gbrp_full_X_c", "_yuv2gbrp16_full_X_c", "_yuv2rgbx64le_X_c", "_yuv2rgba64le_X_c", "_yuv2rgbx64be_X_c", "_yuv2rgba64be_X_c", "_yuv2bgrx64le_X_c", "_yuv2bgra64le_X_c", "_yuv2bgrx64be_X_c", "_yuv2bgra64be_X_c", "_yuv2rgba32_X_c", "_yuv2rgbx32_X_c", "_yuv2rgba32_1_X_c", "_yuv2rgbx32_1_X_c", "_yuv2rgb16_X_c", "_yuv2rgb15_X_c", "_yuv2rgb12_X_c", "_yuv2rgb8_X_c", "_yuv2rgb4_X_c", "_yuv2rgb4b_X_c", "_yuv2rgb48le_X_c", "_yuv2rgb48be_X_c", "_yuv2bgr48le_X_c", "_yuv2bgr48be_X_c", "_yuv2rgb24_X_c", "_yuv2bgr24_X_c", "_yuv2monowhite_X_c", "_yuv2ayuv64le_X_c", "_yuv2monoblack_X_c", "_yuv2yuyv422_X_c", "_yuv2yvyu422_X_c", "_yuv2uyvy422_X_c", "_yuv2ya8_X_c", "_yuv2ya16le_X_c", "_yuv2ya16be_X_c", "_sao_edge_restore_0_9", "_sao_edge_restore_1_9", "_sao_edge_restore_0_10", "_sao_edge_restore_1_10", "_sao_edge_restore_0_12", "_sao_edge_restore_1_12", "_sao_edge_restore_0_8", "_sao_edge_restore_1_8", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_table_viiiiiiiiiiiiii = [0, "jsCall_viiiiiiiiiiiiii_0", "jsCall_viiiiiiiiiiiiii_1", "jsCall_viiiiiiiiiiiiii_2", "jsCall_viiiiiiiiiiiiii_3", "jsCall_viiiiiiiiiiiiii_4", "jsCall_viiiiiiiiiiiiii_5", "jsCall_viiiiiiiiiiiiii_6", "jsCall_viiiiiiiiiiiiii_7", "jsCall_viiiiiiiiiiiiii_8", "jsCall_viiiiiiiiiiiiii_9", "jsCall_viiiiiiiiiiiiii_10", "jsCall_viiiiiiiiiiiiii_11", "jsCall_viiiiiiiiiiiiii_12", "jsCall_viiiiiiiiiiiiii_13", "jsCall_viiiiiiiiiiiiii_14", "jsCall_viiiiiiiiiiiiii_15", "jsCall_viiiiiiiiiiiiii_16", "jsCall_viiiiiiiiiiiiii_17", "jsCall_viiiiiiiiiiiiii_18", "jsCall_viiiiiiiiiiiiii_19", "jsCall_viiiiiiiiiiiiii_20", "jsCall_viiiiiiiiiiiiii_21", "jsCall_viiiiiiiiiiiiii_22", "jsCall_viiiiiiiiiiiiii_23", "jsCall_viiiiiiiiiiiiii_24", "jsCall_viiiiiiiiiiiiii_25", "jsCall_viiiiiiiiiiiiii_26", "jsCall_viiiiiiiiiiiiii_27", "jsCall_viiiiiiiiiiiiii_28", "jsCall_viiiiiiiiiiiiii_29", "jsCall_viiiiiiiiiiiiii_30", "jsCall_viiiiiiiiiiiiii_31", "jsCall_viiiiiiiiiiiiii_32", "jsCall_viiiiiiiiiiiiii_33", "jsCall_viiiiiiiiiiiiii_34", "_put_hevc_pel_bi_w_pixels_9", "_put_hevc_qpel_bi_w_h_9", "_put_hevc_qpel_bi_w_v_9", "_put_hevc_qpel_bi_w_hv_9", "_put_hevc_epel_bi_w_h_9", "_put_hevc_epel_bi_w_v_9", "_put_hevc_epel_bi_w_hv_9", "_put_hevc_pel_bi_w_pixels_10", "_put_hevc_qpel_bi_w_h_10", "_put_hevc_qpel_bi_w_v_10", "_put_hevc_qpel_bi_w_hv_10", "_put_hevc_epel_bi_w_h_10", "_put_hevc_epel_bi_w_v_10", "_put_hevc_epel_bi_w_hv_10", "_put_hevc_pel_bi_w_pixels_12", "_put_hevc_qpel_bi_w_h_12", "_put_hevc_qpel_bi_w_v_12", "_put_hevc_qpel_bi_w_hv_12", "_put_hevc_epel_bi_w_h_12", "_put_hevc_epel_bi_w_v_12", "_put_hevc_epel_bi_w_hv_12", "_put_hevc_pel_bi_w_pixels_8", "_put_hevc_qpel_bi_w_h_8", "_put_hevc_qpel_bi_w_v_8", "_put_hevc_qpel_bi_w_hv_8", "_put_hevc_epel_bi_w_h_8", "_put_hevc_epel_bi_w_v_8", "_put_hevc_epel_bi_w_hv_8"]; +var debug_table_viiijj = [0, "jsCall_viiijj_0", "jsCall_viiijj_1", "jsCall_viiijj_2", "jsCall_viiijj_3", "jsCall_viiijj_4", "jsCall_viiijj_5", "jsCall_viiijj_6", "jsCall_viiijj_7", "jsCall_viiijj_8", "jsCall_viiijj_9", "jsCall_viiijj_10", "jsCall_viiijj_11", "jsCall_viiijj_12", "jsCall_viiijj_13", "jsCall_viiijj_14", "jsCall_viiijj_15", "jsCall_viiijj_16", "jsCall_viiijj_17", "jsCall_viiijj_18", "jsCall_viiijj_19", "jsCall_viiijj_20", "jsCall_viiijj_21", "jsCall_viiijj_22", "jsCall_viiijj_23", "jsCall_viiijj_24", "jsCall_viiijj_25", "jsCall_viiijj_26", "jsCall_viiijj_27", "jsCall_viiijj_28", "jsCall_viiijj_29", "jsCall_viiijj_30", "jsCall_viiijj_31", "jsCall_viiijj_32", "jsCall_viiijj_33", "jsCall_viiijj_34", "_resample_one_int16", "_resample_one_int32", "_resample_one_float", "_resample_one_double", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var debug_tables = { + "dd": debug_table_dd, + "did": debug_table_did, + "didd": debug_table_didd, + "fii": debug_table_fii, + "fiii": debug_table_fiii, + "ii": debug_table_ii, + "iid": debug_table_iid, + "iidiiii": debug_table_iidiiii, + "iii": debug_table_iii, + "iiii": debug_table_iiii, + "iiiii": debug_table_iiiii, + "iiiiii": debug_table_iiiiii, + "iiiiiii": debug_table_iiiiiii, + "iiiiiiidiiddii": debug_table_iiiiiiidiiddii, + "iiiiiiii": debug_table_iiiiiiii, + "iiiiiiiid": debug_table_iiiiiiiid, + "iiiiij": debug_table_iiiiij, + "iiiji": debug_table_iiiji, + "iiijjji": debug_table_iiijjji, + "jii": debug_table_jii, + "jiiij": debug_table_jiiij, + "jiiji": debug_table_jiiji, + "jij": debug_table_jij, + "jiji": debug_table_jiji, + "v": debug_table_v, + "vdiidiiiii": debug_table_vdiidiiiii, + "vdiidiiiiii": debug_table_vdiidiiiiii, + "vi": debug_table_vi, + "vii": debug_table_vii, + "viidi": debug_table_viidi, + "viifi": debug_table_viifi, + "viii": debug_table_viii, + "viiid": debug_table_viiid, + "viiii": debug_table_viiii, + "viiiifii": debug_table_viiiifii, + "viiiii": debug_table_viiiii, + "viiiiidd": debug_table_viiiiidd, + "viiiiiddi": debug_table_viiiiiddi, + "viiiiii": debug_table_viiiiii, + "viiiiiifi": debug_table_viiiiiifi, + "viiiiiii": debug_table_viiiiiii, + "viiiiiiii": debug_table_viiiiiiii, + "viiiiiiiid": debug_table_viiiiiiiid, + "viiiiiiiidi": debug_table_viiiiiiiidi, + "viiiiiiiii": debug_table_viiiiiiiii, + "viiiiiiiiii": debug_table_viiiiiiiiii, + "viiiiiiiiiii": debug_table_viiiiiiiiiii, + "viiiiiiiiiiii": debug_table_viiiiiiiiiiii, + "viiiiiiiiiiiiii": debug_table_viiiiiiiiiiiiii, + "viiijj": debug_table_viiijj +}; + +function nullFunc_dd(x) { + abortFnPtrError(x, "dd") +} + +function nullFunc_did(x) { + abortFnPtrError(x, "did") +} + +function nullFunc_didd(x) { + abortFnPtrError(x, "didd") +} + +function nullFunc_fii(x) { + abortFnPtrError(x, "fii") +} + +function nullFunc_fiii(x) { + abortFnPtrError(x, "fiii") +} + +function nullFunc_ii(x) { + abortFnPtrError(x, "ii") +} + +function nullFunc_iid(x) { + abortFnPtrError(x, "iid") +} + +function nullFunc_iidiiii(x) { + abortFnPtrError(x, "iidiiii") +} + +function nullFunc_iii(x) { + abortFnPtrError(x, "iii") +} + +function nullFunc_iiii(x) { + abortFnPtrError(x, "iiii") +} + +function nullFunc_iiiii(x) { + abortFnPtrError(x, "iiiii") +} + +function nullFunc_iiiiii(x) { + abortFnPtrError(x, "iiiiii") +} + +function nullFunc_iiiiiii(x) { + abortFnPtrError(x, "iiiiiii") +} + +function nullFunc_iiiiiiidiiddii(x) { + abortFnPtrError(x, "iiiiiiidiiddii") +} + +function nullFunc_iiiiiiii(x) { + abortFnPtrError(x, "iiiiiiii") +} + +function nullFunc_iiiiiiiid(x) { + abortFnPtrError(x, "iiiiiiiid") +} + +function nullFunc_iiiiij(x) { + abortFnPtrError(x, "iiiiij") +} + +function nullFunc_iiiji(x) { + abortFnPtrError(x, "iiiji") +} + +function nullFunc_iiijjji(x) { + abortFnPtrError(x, "iiijjji") +} + +function nullFunc_jii(x) { + abortFnPtrError(x, "jii") +} + +function nullFunc_jiiij(x) { + abortFnPtrError(x, "jiiij") +} + +function nullFunc_jiiji(x) { + abortFnPtrError(x, "jiiji") +} + +function nullFunc_jij(x) { + abortFnPtrError(x, "jij") +} + +function nullFunc_jiji(x) { + abortFnPtrError(x, "jiji") +} + +function nullFunc_v(x) { + abortFnPtrError(x, "v") +} + +function nullFunc_vdiidiiiii(x) { + abortFnPtrError(x, "vdiidiiiii") +} + +function nullFunc_vdiidiiiiii(x) { + abortFnPtrError(x, "vdiidiiiiii") +} + +function nullFunc_vi(x) { + abortFnPtrError(x, "vi") +} + +function nullFunc_vii(x) { + abortFnPtrError(x, "vii") +} + +function nullFunc_viidi(x) { + abortFnPtrError(x, "viidi") +} + +function nullFunc_viifi(x) { + abortFnPtrError(x, "viifi") +} + +function nullFunc_viii(x) { + abortFnPtrError(x, "viii") +} + +function nullFunc_viiid(x) { + abortFnPtrError(x, "viiid") +} + +function nullFunc_viiii(x) { + abortFnPtrError(x, "viiii") +} + +function nullFunc_viiiifii(x) { + abortFnPtrError(x, "viiiifii") +} + +function nullFunc_viiiii(x) { + abortFnPtrError(x, "viiiii") +} + +function nullFunc_viiiiidd(x) { + abortFnPtrError(x, "viiiiidd") +} + +function nullFunc_viiiiiddi(x) { + abortFnPtrError(x, "viiiiiddi") +} + +function nullFunc_viiiiii(x) { + abortFnPtrError(x, "viiiiii") +} + +function nullFunc_viiiiiifi(x) { + abortFnPtrError(x, "viiiiiifi") +} + +function nullFunc_viiiiiii(x) { + abortFnPtrError(x, "viiiiiii") +} + +function nullFunc_viiiiiiii(x) { + abortFnPtrError(x, "viiiiiiii") +} + +function nullFunc_viiiiiiiid(x) { + abortFnPtrError(x, "viiiiiiiid") +} + +function nullFunc_viiiiiiiidi(x) { + abortFnPtrError(x, "viiiiiiiidi") +} + +function nullFunc_viiiiiiiii(x) { + abortFnPtrError(x, "viiiiiiiii") +} + +function nullFunc_viiiiiiiiii(x) { + abortFnPtrError(x, "viiiiiiiiii") +} + +function nullFunc_viiiiiiiiiii(x) { + abortFnPtrError(x, "viiiiiiiiiii") +} + +function nullFunc_viiiiiiiiiiii(x) { + abortFnPtrError(x, "viiiiiiiiiiii") +} + +function nullFunc_viiiiiiiiiiiiii(x) { + abortFnPtrError(x, "viiiiiiiiiiiiii") +} + +function nullFunc_viiijj(x) { + abortFnPtrError(x, "viiijj") +} + +function jsCall_dd(index, a1) { + return functionPointers[index](a1) +} + +function jsCall_did(index, a1, a2) { + return functionPointers[index](a1, a2) +} + +function jsCall_didd(index, a1, a2, a3) { + return functionPointers[index](a1, a2, a3) +} + +function jsCall_fii(index, a1, a2) { + return functionPointers[index](a1, a2) +} + +function jsCall_fiii(index, a1, a2, a3) { + return functionPointers[index](a1, a2, a3) +} + +function jsCall_ii(index, a1) { + return functionPointers[index](a1) +} + +function jsCall_iid(index, a1, a2) { + return functionPointers[index](a1, a2) +} + +function jsCall_iidiiii(index, a1, a2, a3, a4, a5, a6) { + return functionPointers[index](a1, a2, a3, a4, a5, a6) +} + +function jsCall_iii(index, a1, a2) { + return functionPointers[index](a1, a2) +} + +function jsCall_iiii(index, a1, a2, a3) { + return functionPointers[index](a1, a2, a3) +} + +function jsCall_iiiii(index, a1, a2, a3, a4) { + return functionPointers[index](a1, a2, a3, a4) +} + +function jsCall_iiiiii(index, a1, a2, a3, a4, a5) { + return functionPointers[index](a1, a2, a3, a4, a5) +} + +function jsCall_iiiiiii(index, a1, a2, a3, a4, a5, a6) { + return functionPointers[index](a1, a2, a3, a4, a5, a6) +} + +function jsCall_iiiiiiidiiddii(index, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13) { + return functionPointers[index](a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13) +} + +function jsCall_iiiiiiii(index, a1, a2, a3, a4, a5, a6, a7) { + return functionPointers[index](a1, a2, a3, a4, a5, a6, a7) +} + +function jsCall_iiiiiiiid(index, a1, a2, a3, a4, a5, a6, a7, a8) { + return functionPointers[index](a1, a2, a3, a4, a5, a6, a7, a8) +} + +function jsCall_iiiiij(index, a1, a2, a3, a4, a5) { + return functionPointers[index](a1, a2, a3, a4, a5) +} + +function jsCall_iiiji(index, a1, a2, a3, a4) { + return functionPointers[index](a1, a2, a3, a4) +} + +function jsCall_iiijjji(index, a1, a2, a3, a4, a5, a6) { + return functionPointers[index](a1, a2, a3, a4, a5, a6) +} + +function jsCall_jii(index, a1, a2) { + return functionPointers[index](a1, a2) +} + +function jsCall_jiiij(index, a1, a2, a3, a4) { + return functionPointers[index](a1, a2, a3, a4) +} + +function jsCall_jiiji(index, a1, a2, a3, a4) { + return functionPointers[index](a1, a2, a3, a4) +} + +function jsCall_jij(index, a1, a2) { + return functionPointers[index](a1, a2) +} + +function jsCall_jiji(index, a1, a2, a3) { + return functionPointers[index](a1, a2, a3) +} + +function jsCall_v(index) { + functionPointers[index]() +} + +function jsCall_vdiidiiiii(index, a1, a2, a3, a4, a5, a6, a7, a8, a9) { + functionPointers[index](a1, a2, a3, a4, a5, a6, a7, a8, a9) +} + +function jsCall_vdiidiiiiii(index, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) { + functionPointers[index](a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) +} + +function jsCall_vi(index, a1) { + functionPointers[index](a1) +} + +function jsCall_vii(index, a1, a2) { + functionPointers[index](a1, a2) +} + +function jsCall_viidi(index, a1, a2, a3, a4) { + functionPointers[index](a1, a2, a3, a4) +} + +function jsCall_viifi(index, a1, a2, a3, a4) { + functionPointers[index](a1, a2, a3, a4) +} + +function jsCall_viii(index, a1, a2, a3) { + functionPointers[index](a1, a2, a3) +} + +function jsCall_viiid(index, a1, a2, a3, a4) { + functionPointers[index](a1, a2, a3, a4) +} + +function jsCall_viiii(index, a1, a2, a3, a4) { + functionPointers[index](a1, a2, a3, a4) +} + +function jsCall_viiiifii(index, a1, a2, a3, a4, a5, a6, a7) { + functionPointers[index](a1, a2, a3, a4, a5, a6, a7) +} + +function jsCall_viiiii(index, a1, a2, a3, a4, a5) { + functionPointers[index](a1, a2, a3, a4, a5) +} + +function jsCall_viiiiidd(index, a1, a2, a3, a4, a5, a6, a7) { + functionPointers[index](a1, a2, a3, a4, a5, a6, a7) +} + +function jsCall_viiiiiddi(index, a1, a2, a3, a4, a5, a6, a7, a8) { + functionPointers[index](a1, a2, a3, a4, a5, a6, a7, a8) +} + +function jsCall_viiiiii(index, a1, a2, a3, a4, a5, a6) { + functionPointers[index](a1, a2, a3, a4, a5, a6) +} + +function jsCall_viiiiiifi(index, a1, a2, a3, a4, a5, a6, a7, a8) { + functionPointers[index](a1, a2, a3, a4, a5, a6, a7, a8) +} + +function jsCall_viiiiiii(index, a1, a2, a3, a4, a5, a6, a7) { + functionPointers[index](a1, a2, a3, a4, a5, a6, a7) +} + +function jsCall_viiiiiiii(index, a1, a2, a3, a4, a5, a6, a7, a8) { + functionPointers[index](a1, a2, a3, a4, a5, a6, a7, a8) +} + +function jsCall_viiiiiiiid(index, a1, a2, a3, a4, a5, a6, a7, a8, a9) { + functionPointers[index](a1, a2, a3, a4, a5, a6, a7, a8, a9) +} + +function jsCall_viiiiiiiidi(index, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) { + functionPointers[index](a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) +} + +function jsCall_viiiiiiiii(index, a1, a2, a3, a4, a5, a6, a7, a8, a9) { + functionPointers[index](a1, a2, a3, a4, a5, a6, a7, a8, a9) +} + +function jsCall_viiiiiiiiii(index, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) { + functionPointers[index](a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) +} + +function jsCall_viiiiiiiiiii(index, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11) { + functionPointers[index](a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11) +} + +function jsCall_viiiiiiiiiiii(index, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12) { + functionPointers[index](a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12) +} + +function jsCall_viiiiiiiiiiiiii(index, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14) { + functionPointers[index](a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14) +} + +function jsCall_viiijj(index, a1, a2, a3, a4, a5) { + functionPointers[index](a1, a2, a3, a4, a5) +} +var asmGlobalArg = {}; +var asmLibraryArg = { + "___buildEnvironment": ___buildEnvironment, + "___lock": ___lock, + "___syscall221": ___syscall221, + "___syscall3": ___syscall3, + "___syscall5": ___syscall5, + "___unlock": ___unlock, + "___wasi_fd_close": ___wasi_fd_close, + "___wasi_fd_fdstat_get": ___wasi_fd_fdstat_get, + "___wasi_fd_seek": ___wasi_fd_seek, + "___wasi_fd_write": ___wasi_fd_write, + "__emscripten_fetch_free": __emscripten_fetch_free, + "__memory_base": 1024, + "__table_base": 0, + "_abort": _abort, + "_clock": _clock, + "_clock_gettime": _clock_gettime, + "_emscripten_asm_const_i": _emscripten_asm_const_i, + "_emscripten_get_heap_size": _emscripten_get_heap_size, + "_emscripten_is_main_browser_thread": _emscripten_is_main_browser_thread, + "_emscripten_memcpy_big": _emscripten_memcpy_big, + "_emscripten_resize_heap": _emscripten_resize_heap, + "_emscripten_start_fetch": _emscripten_start_fetch, + "_fabs": _fabs, + "_getenv": _getenv, + "_gettimeofday": _gettimeofday, + "_gmtime_r": _gmtime_r, + "_llvm_exp2_f64": _llvm_exp2_f64, + "_llvm_log2_f32": _llvm_log2_f32, + "_llvm_stackrestore": _llvm_stackrestore, + "_llvm_stacksave": _llvm_stacksave, + "_llvm_trunc_f64": _llvm_trunc_f64, + "_localtime_r": _localtime_r, + "_nanosleep": _nanosleep, + "_pthread_cond_destroy": _pthread_cond_destroy, + "_pthread_cond_init": _pthread_cond_init, + "_pthread_create": _pthread_create, + "_pthread_join": _pthread_join, + "_strftime": _strftime, + "_sysconf": _sysconf, + "_time": _time, + "abortStackOverflow": abortStackOverflow, + "getTempRet0": getTempRet0, + "jsCall_dd": jsCall_dd, + "jsCall_did": jsCall_did, + "jsCall_didd": jsCall_didd, + "jsCall_fii": jsCall_fii, + "jsCall_fiii": jsCall_fiii, + "jsCall_ii": jsCall_ii, + "jsCall_iid": jsCall_iid, + "jsCall_iidiiii": jsCall_iidiiii, + "jsCall_iii": jsCall_iii, + "jsCall_iiii": jsCall_iiii, + "jsCall_iiiii": jsCall_iiiii, + "jsCall_iiiiii": jsCall_iiiiii, + "jsCall_iiiiiii": jsCall_iiiiiii, + "jsCall_iiiiiiidiiddii": jsCall_iiiiiiidiiddii, + "jsCall_iiiiiiii": jsCall_iiiiiiii, + "jsCall_iiiiiiiid": jsCall_iiiiiiiid, + "jsCall_iiiiij": jsCall_iiiiij, + "jsCall_iiiji": jsCall_iiiji, + "jsCall_iiijjji": jsCall_iiijjji, + "jsCall_jii": jsCall_jii, + "jsCall_jiiij": jsCall_jiiij, + "jsCall_jiiji": jsCall_jiiji, + "jsCall_jij": jsCall_jij, + "jsCall_jiji": jsCall_jiji, + "jsCall_v": jsCall_v, + "jsCall_vdiidiiiii": jsCall_vdiidiiiii, + "jsCall_vdiidiiiiii": jsCall_vdiidiiiiii, + "jsCall_vi": jsCall_vi, + "jsCall_vii": jsCall_vii, + "jsCall_viidi": jsCall_viidi, + "jsCall_viifi": jsCall_viifi, + "jsCall_viii": jsCall_viii, + "jsCall_viiid": jsCall_viiid, + "jsCall_viiii": jsCall_viiii, + "jsCall_viiiifii": jsCall_viiiifii, + "jsCall_viiiii": jsCall_viiiii, + "jsCall_viiiiidd": jsCall_viiiiidd, + "jsCall_viiiiiddi": jsCall_viiiiiddi, + "jsCall_viiiiii": jsCall_viiiiii, + "jsCall_viiiiiifi": jsCall_viiiiiifi, + "jsCall_viiiiiii": jsCall_viiiiiii, + "jsCall_viiiiiiii": jsCall_viiiiiiii, + "jsCall_viiiiiiiid": jsCall_viiiiiiiid, + "jsCall_viiiiiiiidi": jsCall_viiiiiiiidi, + "jsCall_viiiiiiiii": jsCall_viiiiiiiii, + "jsCall_viiiiiiiiii": jsCall_viiiiiiiiii, + "jsCall_viiiiiiiiiii": jsCall_viiiiiiiiiii, + "jsCall_viiiiiiiiiiii": jsCall_viiiiiiiiiiii, + "jsCall_viiiiiiiiiiiiii": jsCall_viiiiiiiiiiiiii, + "jsCall_viiijj": jsCall_viiijj, + "memory": wasmMemory, + "nullFunc_dd": nullFunc_dd, + "nullFunc_did": nullFunc_did, + "nullFunc_didd": nullFunc_didd, + "nullFunc_fii": nullFunc_fii, + "nullFunc_fiii": nullFunc_fiii, + "nullFunc_ii": nullFunc_ii, + "nullFunc_iid": nullFunc_iid, + "nullFunc_iidiiii": nullFunc_iidiiii, + "nullFunc_iii": nullFunc_iii, + "nullFunc_iiii": nullFunc_iiii, + "nullFunc_iiiii": nullFunc_iiiii, + "nullFunc_iiiiii": nullFunc_iiiiii, + "nullFunc_iiiiiii": nullFunc_iiiiiii, + "nullFunc_iiiiiiidiiddii": nullFunc_iiiiiiidiiddii, + "nullFunc_iiiiiiii": nullFunc_iiiiiiii, + "nullFunc_iiiiiiiid": nullFunc_iiiiiiiid, + "nullFunc_iiiiij": nullFunc_iiiiij, + "nullFunc_iiiji": nullFunc_iiiji, + "nullFunc_iiijjji": nullFunc_iiijjji, + "nullFunc_jii": nullFunc_jii, + "nullFunc_jiiij": nullFunc_jiiij, + "nullFunc_jiiji": nullFunc_jiiji, + "nullFunc_jij": nullFunc_jij, + "nullFunc_jiji": nullFunc_jiji, + "nullFunc_v": nullFunc_v, + "nullFunc_vdiidiiiii": nullFunc_vdiidiiiii, + "nullFunc_vdiidiiiiii": nullFunc_vdiidiiiiii, + "nullFunc_vi": nullFunc_vi, + "nullFunc_vii": nullFunc_vii, + "nullFunc_viidi": nullFunc_viidi, + "nullFunc_viifi": nullFunc_viifi, + "nullFunc_viii": nullFunc_viii, + "nullFunc_viiid": nullFunc_viiid, + "nullFunc_viiii": nullFunc_viiii, + "nullFunc_viiiifii": nullFunc_viiiifii, + "nullFunc_viiiii": nullFunc_viiiii, + "nullFunc_viiiiidd": nullFunc_viiiiidd, + "nullFunc_viiiiiddi": nullFunc_viiiiiddi, + "nullFunc_viiiiii": nullFunc_viiiiii, + "nullFunc_viiiiiifi": nullFunc_viiiiiifi, + "nullFunc_viiiiiii": nullFunc_viiiiiii, + "nullFunc_viiiiiiii": nullFunc_viiiiiiii, + "nullFunc_viiiiiiiid": nullFunc_viiiiiiiid, + "nullFunc_viiiiiiiidi": nullFunc_viiiiiiiidi, + "nullFunc_viiiiiiiii": nullFunc_viiiiiiiii, + "nullFunc_viiiiiiiiii": nullFunc_viiiiiiiiii, + "nullFunc_viiiiiiiiiii": nullFunc_viiiiiiiiiii, + "nullFunc_viiiiiiiiiiii": nullFunc_viiiiiiiiiiii, + "nullFunc_viiiiiiiiiiiiii": nullFunc_viiiiiiiiiiiiii, + "nullFunc_viiijj": nullFunc_viiijj, + "table": wasmTable +}; +var asm = Module["asm"](asmGlobalArg, asmLibraryArg, buffer); +Module["asm"] = asm; +var _AVPlayerInit = Module["_AVPlayerInit"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_AVPlayerInit"].apply(null, arguments) +}; +var _AVSniffHttpFlvInit = Module["_AVSniffHttpFlvInit"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_AVSniffHttpFlvInit"].apply(null, arguments) +}; +var _AVSniffHttpG711Init = Module["_AVSniffHttpG711Init"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_AVSniffHttpG711Init"].apply(null, arguments) +}; +var _AVSniffStreamInit = Module["_AVSniffStreamInit"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_AVSniffStreamInit"].apply(null, arguments) +}; +var ___emscripten_environ_constructor = Module["___emscripten_environ_constructor"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["___emscripten_environ_constructor"].apply(null, arguments) +}; +var ___errno_location = Module["___errno_location"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["___errno_location"].apply(null, arguments) +}; +var __get_daylight = Module["__get_daylight"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["__get_daylight"].apply(null, arguments) +}; +var __get_timezone = Module["__get_timezone"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["__get_timezone"].apply(null, arguments) +}; +var __get_tzname = Module["__get_tzname"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["__get_tzname"].apply(null, arguments) +}; +var _closeVideo = Module["_closeVideo"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_closeVideo"].apply(null, arguments) +}; +var _decodeCodecContext = Module["_decodeCodecContext"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_decodeCodecContext"].apply(null, arguments) +}; +var _decodeG711Frame = Module["_decodeG711Frame"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_decodeG711Frame"].apply(null, arguments) +}; +var _decodeHttpFlvVideoFrame = Module["_decodeHttpFlvVideoFrame"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_decodeHttpFlvVideoFrame"].apply(null, arguments) +}; +var _decodeVideoFrame = Module["_decodeVideoFrame"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_decodeVideoFrame"].apply(null, arguments) +}; +var _demuxBox = Module["_demuxBox"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_demuxBox"].apply(null, arguments) +}; +var _exitMissile = Module["_exitMissile"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_exitMissile"].apply(null, arguments) +}; +var _exitTsMissile = Module["_exitTsMissile"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_exitTsMissile"].apply(null, arguments) +}; +var _fflush = Module["_fflush"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_fflush"].apply(null, arguments) +}; +var _free = Module["_free"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_free"].apply(null, arguments) +}; +var _getAudioCodecID = Module["_getAudioCodecID"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getAudioCodecID"].apply(null, arguments) +}; +var _getBufferLengthApi = Module["_getBufferLengthApi"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getBufferLengthApi"].apply(null, arguments) +}; +var _getExtensionInfo = Module["_getExtensionInfo"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getExtensionInfo"].apply(null, arguments) +}; +var _getG711BufferLengthApi = Module["_getG711BufferLengthApi"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getG711BufferLengthApi"].apply(null, arguments) +}; +var _getMediaInfo = Module["_getMediaInfo"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getMediaInfo"].apply(null, arguments) +}; +var _getPPS = Module["_getPPS"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getPPS"].apply(null, arguments) +}; +var _getPPSLen = Module["_getPPSLen"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getPPSLen"].apply(null, arguments) +}; +var _getPacket = Module["_getPacket"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getPacket"].apply(null, arguments) +}; +var _getSEI = Module["_getSEI"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getSEI"].apply(null, arguments) +}; +var _getSEILen = Module["_getSEILen"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getSEILen"].apply(null, arguments) +}; +var _getSPS = Module["_getSPS"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getSPS"].apply(null, arguments) +}; +var _getSPSLen = Module["_getSPSLen"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getSPSLen"].apply(null, arguments) +}; +var _getSniffHttpFlvPkg = Module["_getSniffHttpFlvPkg"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getSniffHttpFlvPkg"].apply(null, arguments) +}; +var _getSniffHttpFlvPkgNoCheckProbe = Module["_getSniffHttpFlvPkgNoCheckProbe"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getSniffHttpFlvPkgNoCheckProbe"].apply(null, arguments) +}; +var _getSniffStreamPkg = Module["_getSniffStreamPkg"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getSniffStreamPkg"].apply(null, arguments) +}; +var _getSniffStreamPkgNoCheckProbe = Module["_getSniffStreamPkgNoCheckProbe"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getSniffStreamPkgNoCheckProbe"].apply(null, arguments) +}; +var _getVLC = Module["_getVLC"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getVLC"].apply(null, arguments) +}; +var _getVLCLen = Module["_getVLCLen"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getVLCLen"].apply(null, arguments) +}; +var _getVPS = Module["_getVPS"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getVPS"].apply(null, arguments) +}; +var _getVPSLen = Module["_getVPSLen"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getVPSLen"].apply(null, arguments) +}; +var _getVideoCodecID = Module["_getVideoCodecID"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_getVideoCodecID"].apply(null, arguments) +}; +var _initTsMissile = Module["_initTsMissile"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_initTsMissile"].apply(null, arguments) +}; +var _initializeDecoder = Module["_initializeDecoder"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_initializeDecoder"].apply(null, arguments) +}; +var _initializeDemuxer = Module["_initializeDemuxer"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_initializeDemuxer"].apply(null, arguments) +}; +var _initializeSniffG711Module = Module["_initializeSniffG711Module"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_initializeSniffG711Module"].apply(null, arguments) +}; +var _initializeSniffHttpFlvModule = Module["_initializeSniffHttpFlvModule"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_initializeSniffHttpFlvModule"].apply(null, arguments) +}; +var _initializeSniffHttpFlvModuleWithAOpt = Module["_initializeSniffHttpFlvModuleWithAOpt"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_initializeSniffHttpFlvModuleWithAOpt"].apply(null, arguments) +}; +var _initializeSniffStreamModule = Module["_initializeSniffStreamModule"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_initializeSniffStreamModule"].apply(null, arguments) +}; +var _initializeSniffStreamModuleWithAOpt = Module["_initializeSniffStreamModuleWithAOpt"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_initializeSniffStreamModuleWithAOpt"].apply(null, arguments) +}; +var _main = Module["_main"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_main"].apply(null, arguments) +}; +var _malloc = Module["_malloc"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_malloc"].apply(null, arguments) +}; +var _naluLListLength = Module["_naluLListLength"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_naluLListLength"].apply(null, arguments) +}; +var _pushSniffG711FlvData = Module["_pushSniffG711FlvData"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_pushSniffG711FlvData"].apply(null, arguments) +}; +var _pushSniffHttpFlvData = Module["_pushSniffHttpFlvData"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_pushSniffHttpFlvData"].apply(null, arguments) +}; +var _pushSniffStreamData = Module["_pushSniffStreamData"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_pushSniffStreamData"].apply(null, arguments) +}; +var _registerPlayer = Module["_registerPlayer"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_registerPlayer"].apply(null, arguments) +}; +var _release = Module["_release"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_release"].apply(null, arguments) +}; +var _releaseG711 = Module["_releaseG711"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_releaseG711"].apply(null, arguments) +}; +var _releaseHttpFLV = Module["_releaseHttpFLV"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_releaseHttpFLV"].apply(null, arguments) +}; +var _releaseSniffHttpFlv = Module["_releaseSniffHttpFlv"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_releaseSniffHttpFlv"].apply(null, arguments) +}; +var _releaseSniffStream = Module["_releaseSniffStream"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_releaseSniffStream"].apply(null, arguments) +}; +var _setCodecType = Module["_setCodecType"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["_setCodecType"].apply(null, arguments) +}; +var establishStackSpace = Module["establishStackSpace"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["establishStackSpace"].apply(null, arguments) +}; +var stackAlloc = Module["stackAlloc"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["stackAlloc"].apply(null, arguments) +}; +var stackRestore = Module["stackRestore"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["stackRestore"].apply(null, arguments) +}; +var stackSave = Module["stackSave"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["stackSave"].apply(null, arguments) +}; +var dynCall_v = Module["dynCall_v"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["dynCall_v"].apply(null, arguments) +}; +var dynCall_vi = Module["dynCall_vi"] = function() { + assert(runtimeInitialized, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"); + assert(!runtimeExited, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"); + return Module["asm"]["dynCall_vi"].apply(null, arguments) +}; +Module["asm"] = asm; +if (!Object.getOwnPropertyDescriptor(Module, "intArrayFromString")) Module["intArrayFromString"] = function() { + abort("'intArrayFromString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "intArrayToString")) Module["intArrayToString"] = function() { + abort("'intArrayToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +Module["ccall"] = ccall; +Module["cwrap"] = cwrap; +if (!Object.getOwnPropertyDescriptor(Module, "setValue")) Module["setValue"] = function() { + abort("'setValue' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "getValue")) Module["getValue"] = function() { + abort("'getValue' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "allocate")) Module["allocate"] = function() { + abort("'allocate' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "getMemory")) Module["getMemory"] = function() { + abort("'getMemory' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") +}; +if (!Object.getOwnPropertyDescriptor(Module, "AsciiToString")) Module["AsciiToString"] = function() { + abort("'AsciiToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "stringToAscii")) Module["stringToAscii"] = function() { + abort("'stringToAscii' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "UTF8ArrayToString")) Module["UTF8ArrayToString"] = function() { + abort("'UTF8ArrayToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "UTF8ToString")) Module["UTF8ToString"] = function() { + abort("'UTF8ToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "stringToUTF8Array")) Module["stringToUTF8Array"] = function() { + abort("'stringToUTF8Array' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "stringToUTF8")) Module["stringToUTF8"] = function() { + abort("'stringToUTF8' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "lengthBytesUTF8")) Module["lengthBytesUTF8"] = function() { + abort("'lengthBytesUTF8' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "UTF16ToString")) Module["UTF16ToString"] = function() { + abort("'UTF16ToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "stringToUTF16")) Module["stringToUTF16"] = function() { + abort("'stringToUTF16' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "lengthBytesUTF16")) Module["lengthBytesUTF16"] = function() { + abort("'lengthBytesUTF16' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "UTF32ToString")) Module["UTF32ToString"] = function() { + abort("'UTF32ToString' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "stringToUTF32")) Module["stringToUTF32"] = function() { + abort("'stringToUTF32' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "lengthBytesUTF32")) Module["lengthBytesUTF32"] = function() { + abort("'lengthBytesUTF32' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "allocateUTF8")) Module["allocateUTF8"] = function() { + abort("'allocateUTF8' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "stackTrace")) Module["stackTrace"] = function() { + abort("'stackTrace' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "addOnPreRun")) Module["addOnPreRun"] = function() { + abort("'addOnPreRun' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "addOnInit")) Module["addOnInit"] = function() { + abort("'addOnInit' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "addOnPreMain")) Module["addOnPreMain"] = function() { + abort("'addOnPreMain' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "addOnExit")) Module["addOnExit"] = function() { + abort("'addOnExit' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "addOnPostRun")) Module["addOnPostRun"] = function() { + abort("'addOnPostRun' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "writeStringToMemory")) Module["writeStringToMemory"] = function() { + abort("'writeStringToMemory' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "writeArrayToMemory")) Module["writeArrayToMemory"] = function() { + abort("'writeArrayToMemory' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "writeAsciiToMemory")) Module["writeAsciiToMemory"] = function() { + abort("'writeAsciiToMemory' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "addRunDependency")) Module["addRunDependency"] = function() { + abort("'addRunDependency' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") +}; +if (!Object.getOwnPropertyDescriptor(Module, "removeRunDependency")) Module["removeRunDependency"] = function() { + abort("'removeRunDependency' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") +}; +if (!Object.getOwnPropertyDescriptor(Module, "ENV")) Module["ENV"] = function() { + abort("'ENV' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "FS")) Module["FS"] = function() { + abort("'FS' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "FS_createFolder")) Module["FS_createFolder"] = function() { + abort("'FS_createFolder' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") +}; +if (!Object.getOwnPropertyDescriptor(Module, "FS_createPath")) Module["FS_createPath"] = function() { + abort("'FS_createPath' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") +}; +if (!Object.getOwnPropertyDescriptor(Module, "FS_createDataFile")) Module["FS_createDataFile"] = function() { + abort("'FS_createDataFile' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") +}; +if (!Object.getOwnPropertyDescriptor(Module, "FS_createPreloadedFile")) Module["FS_createPreloadedFile"] = function() { + abort("'FS_createPreloadedFile' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") +}; +if (!Object.getOwnPropertyDescriptor(Module, "FS_createLazyFile")) Module["FS_createLazyFile"] = function() { + abort("'FS_createLazyFile' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") +}; +if (!Object.getOwnPropertyDescriptor(Module, "FS_createLink")) Module["FS_createLink"] = function() { + abort("'FS_createLink' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") +}; +if (!Object.getOwnPropertyDescriptor(Module, "FS_createDevice")) Module["FS_createDevice"] = function() { + abort("'FS_createDevice' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") +}; +if (!Object.getOwnPropertyDescriptor(Module, "FS_unlink")) Module["FS_unlink"] = function() { + abort("'FS_unlink' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") +}; +if (!Object.getOwnPropertyDescriptor(Module, "GL")) Module["GL"] = function() { + abort("'GL' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "dynamicAlloc")) Module["dynamicAlloc"] = function() { + abort("'dynamicAlloc' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "loadDynamicLibrary")) Module["loadDynamicLibrary"] = function() { + abort("'loadDynamicLibrary' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "loadWebAssemblyModule")) Module["loadWebAssemblyModule"] = function() { + abort("'loadWebAssemblyModule' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "getLEB")) Module["getLEB"] = function() { + abort("'getLEB' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "getFunctionTables")) Module["getFunctionTables"] = function() { + abort("'getFunctionTables' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "alignFunctionTables")) Module["alignFunctionTables"] = function() { + abort("'alignFunctionTables' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "registerFunctions")) Module["registerFunctions"] = function() { + abort("'registerFunctions' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +Module["addFunction"] = addFunction; +Module["removeFunction"] = removeFunction; +if (!Object.getOwnPropertyDescriptor(Module, "getFuncWrapper")) Module["getFuncWrapper"] = function() { + abort("'getFuncWrapper' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "prettyPrint")) Module["prettyPrint"] = function() { + abort("'prettyPrint' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "makeBigInt")) Module["makeBigInt"] = function() { + abort("'makeBigInt' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "dynCall")) Module["dynCall"] = function() { + abort("'dynCall' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "getCompilerSetting")) Module["getCompilerSetting"] = function() { + abort("'getCompilerSetting' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "stackSave")) Module["stackSave"] = function() { + abort("'stackSave' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "stackRestore")) Module["stackRestore"] = function() { + abort("'stackRestore' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "stackAlloc")) Module["stackAlloc"] = function() { + abort("'stackAlloc' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "establishStackSpace")) Module["establishStackSpace"] = function() { + abort("'establishStackSpace' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "print")) Module["print"] = function() { + abort("'print' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "printErr")) Module["printErr"] = function() { + abort("'printErr' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "getTempRet0")) Module["getTempRet0"] = function() { + abort("'getTempRet0' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "setTempRet0")) Module["setTempRet0"] = function() { + abort("'setTempRet0' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "callMain")) Module["callMain"] = function() { + abort("'callMain' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "abort")) Module["abort"] = function() { + abort("'abort' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "Pointer_stringify")) Module["Pointer_stringify"] = function() { + abort("'Pointer_stringify' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "warnOnce")) Module["warnOnce"] = function() { + abort("'warnOnce' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") +}; +if (!Object.getOwnPropertyDescriptor(Module, "ALLOC_NORMAL")) Object.defineProperty(Module, "ALLOC_NORMAL", { + configurable: true, + get: function() { + abort("'ALLOC_NORMAL' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") + } +}); +if (!Object.getOwnPropertyDescriptor(Module, "ALLOC_STACK")) Object.defineProperty(Module, "ALLOC_STACK", { + configurable: true, + get: function() { + abort("'ALLOC_STACK' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") + } +}); +if (!Object.getOwnPropertyDescriptor(Module, "ALLOC_DYNAMIC")) Object.defineProperty(Module, "ALLOC_DYNAMIC", { + configurable: true, + get: function() { + abort("'ALLOC_DYNAMIC' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") + } +}); +if (!Object.getOwnPropertyDescriptor(Module, "ALLOC_NONE")) Object.defineProperty(Module, "ALLOC_NONE", { + configurable: true, + get: function() { + abort("'ALLOC_NONE' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ)") + } +}); +if (!Object.getOwnPropertyDescriptor(Module, "calledRun")) Object.defineProperty(Module, "calledRun", { + configurable: true, + get: function() { + abort("'calledRun' was not exported. add it to EXTRA_EXPORTED_RUNTIME_METHODS (see the FAQ). Alternatively, forcing filesystem support (-s FORCE_FILESYSTEM=1) can export this for you") + } +}); +var calledRun; + +function ExitStatus(status) { + this.name = "ExitStatus"; + this.message = "Program terminated with exit(" + status + ")"; + this.status = status +} +var calledMain = false; +dependenciesFulfilled = function runCaller() { + if (!calledRun) run(); + if (!calledRun) dependenciesFulfilled = runCaller +}; + +function callMain(args) { + assert(runDependencies == 0, 'cannot call main when async dependencies remain! (listen on Module["onRuntimeInitialized"])'); + assert(__ATPRERUN__.length == 0, "cannot call main when preRun functions remain to be called"); + args = args || []; + var argc = args.length + 1; + var argv = stackAlloc((argc + 1) * 4); + HEAP32[argv >> 2] = allocateUTF8OnStack(thisProgram); + for (var i = 1; i < argc; i++) { + HEAP32[(argv >> 2) + i] = allocateUTF8OnStack(args[i - 1]) + } + HEAP32[(argv >> 2) + argc] = 0; + try { + var ret = Module["_main"](argc, argv); + exit(ret, true) + } catch (e) { + if (e instanceof ExitStatus) { + return + } else if (e == "SimulateInfiniteLoop") { + noExitRuntime = true; + return + } else { + var toLog = e; + if (e && typeof e === "object" && e.stack) { + toLog = [e, e.stack] + } + err("exception thrown: " + toLog); + quit_(1, e) + } + } finally { + calledMain = true + } +} + +function run(args) { + args = args || arguments_; + if (runDependencies > 0) { + return + } + writeStackCookie(); + preRun(); + if (runDependencies > 0) return; + + function doRun() { + if (calledRun) return; + calledRun = true; + if (ABORT) return; + initRuntime(); + preMain(); + if (Module["onRuntimeInitialized"]) Module["onRuntimeInitialized"](); + if (shouldRunNow) callMain(args); + postRun() + } + if (Module["setStatus"]) { + Module["setStatus"]("Running..."); + setTimeout(function() { + setTimeout(function() { + Module["setStatus"]("") + }, 1); + doRun() + }, 1) + } else { + doRun() + } + checkStackCookie() +} +Module["run"] = run; + +function checkUnflushedContent() { + var print = out; + var printErr = err; + var has = false; + out = err = function(x) { + has = true + }; + try { + var flush = Module["_fflush"]; + if (flush) flush(0); + ["stdout", "stderr"].forEach(function(name) { + var info = FS.analyzePath("/dev/" + name); + if (!info) return; + var stream = info.object; + var rdev = stream.rdev; + var tty = TTY.ttys[rdev]; + if (tty && tty.output && tty.output.length) { + has = true + } + }) + } catch (e) {} + out = print; + err = printErr; + if (has) { + warnOnce("stdio streams had content in them that was not flushed. you should set EXIT_RUNTIME to 1 (see the FAQ), or make sure to emit a newline when you printf etc.") + } +} + +function exit(status, implicit) { + checkUnflushedContent(); + if (implicit && noExitRuntime && status === 0) { + return + } + if (noExitRuntime) { + if (!implicit) { + err("exit(" + status + ") called, but EXIT_RUNTIME is not set, so halting execution but not exiting the runtime or preventing further async execution (build with EXIT_RUNTIME=1, if you want a true shutdown)") + } + } else { + ABORT = true; + EXITSTATUS = status; + exitRuntime(); + if (Module["onExit"]) Module["onExit"](status) + } + quit_(status, new ExitStatus(status)) +} +if (Module["preInit"]) { + if (typeof Module["preInit"] == "function") Module["preInit"] = [Module["preInit"]]; + while (Module["preInit"].length > 0) { + Module["preInit"].pop()() + } +} +var shouldRunNow = true; +if (Module["noInitialRun"]) shouldRunNow = false; +noExitRuntime = true; +run(); \ No newline at end of file diff --git a/web/public/static/js/jessibuca/decoder.js b/web/public/static/js/jessibuca/decoder.js new file mode 100644 index 0000000..a302084 --- /dev/null +++ b/web/public/static/js/jessibuca/decoder.js @@ -0,0 +1 @@ +!function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(require("path"),require("fs"),require("crypto")):"function"==typeof define&&define.amd?define(["path","fs","crypto"],r):r((e="undefined"!=typeof globalThis?globalThis:e||self).path,e.fs,e.crypto$1)}(this,function(e,r,t){"use strict";function n(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var o=n(e),i=n(r),a=n(t);function s(e,r){return e(r={exports:{}},r.exports),r.exports}var l=s(function(e){var r=void 0!==r?r:{},t=(r={print:function(e){console.log("Jessibuca: [worker]:",e)},printErr:function(e){console.warn("Jessibuca: [worker]:",e),postMessage({cmd:"wasmError",message:e})}},Object.assign({},r)),n="./this.program",s="object"==typeof window,l="function"==typeof importScripts,u="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,c=!s&&!u&&!l;if(r.ENVIRONMENT)throw new Error("Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)");var d,f,p,m,h,g,v="";if(u){if("object"!=typeof process)throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)");v=l?o.default.dirname(v)+"/":__dirname+"/",g=()=>{h||(m=i.default,h=o.default)},d=function(e,r){return g(),e=h.normalize(e),m.readFileSync(e,r?void 0:"utf8")},p=e=>{var r=d(e,!0);return r.buffer||(r=new Uint8Array(r)),F(r.buffer),r},f=(e,r,t)=>{g(),e=h.normalize(e),m.readFile(e,function(e,n){e?t(e):r(n.buffer)})},process.argv.length>1&&(n=process.argv[1].replace(/\\/g,"/")),process.argv.slice(2),e.exports=r,process.on("uncaughtException",function(e){if(!(e instanceof At))throw e}),process.on("unhandledRejection",function(e){throw e}),r.inspect=function(){return"[Emscripten Module object]"}}else if(c){if("object"==typeof process||"object"==typeof window||"function"==typeof importScripts)throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)");"undefined"!=typeof read&&(d=function(e){return read(e)}),p=function(e){let r;return"function"==typeof readbuffer?new Uint8Array(readbuffer(e)):(r=read(e,"binary"),F("object"==typeof r),r)},f=function(e,r,t){setTimeout(()=>r(p(e)),0)},"undefined"!=typeof scriptArgs&&scriptArgs,"undefined"!=typeof print&&("undefined"==typeof console&&(console={}),console.log=print,console.warn=console.error="undefined"!=typeof printErr?printErr:print)}else{if(!s&&!l)throw new Error("environment detection error");if(l?v=self.location.href:"undefined"!=typeof document&&document.currentScript&&(v=document.currentScript.src),v=0!==v.indexOf("blob:")?v.substr(0,v.replace(/[?#].*/,"").lastIndexOf("/")+1):"","object"!=typeof window&&"function"!=typeof importScripts)throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)");d=e=>{var r=new XMLHttpRequest;return r.open("GET",e,!1),r.send(null),r.responseText},l&&(p=e=>{var r=new XMLHttpRequest;return r.open("GET",e,!1),r.responseType="arraybuffer",r.send(null),new Uint8Array(r.response)}),f=(e,r,t)=>{var n=new XMLHttpRequest;n.open("GET",e,!0),n.responseType="arraybuffer",n.onload=()=>{200==n.status||0==n.status&&n.response?r(n.response):t()},n.onerror=t,n.send(null)}}var y,E,w,b=r.print||console.log.bind(console),_=r.printErr||console.warn.bind(console);function T(e){T.shown||(T.shown={}),T.shown[e]||(T.shown[e]=1,_(e))}function k(e,t){Object.getOwnPropertyDescriptor(r,e)||Object.defineProperty(r,e,{configurable:!0,get:function(){ye("Module."+e+" has been replaced with plain "+t+" (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)")}})}function S(e,r){var t="'"+e+"' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)";return r&&(t+=". Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you"),t}function C(e,t){Object.getOwnPropertyDescriptor(r,e)||Object.defineProperty(r,e,{configurable:!0,get:function(){ye(S(e,t))}})}function P(e,t){Object.getOwnPropertyDescriptor(r,e)||(r[e]=()=>ye(S(e,t)))}Object.assign(r,t),t=null,y="fetchSettings",Object.getOwnPropertyDescriptor(r,y)&&ye("`Module."+y+"` was supplied but `"+y+"` not included in INCOMING_MODULE_JS_API"),r.arguments&&r.arguments,k("arguments","arguments_"),r.thisProgram&&(n=r.thisProgram),k("thisProgram","thisProgram"),r.quit&&r.quit,k("quit","quit_"),F(void 0===r.memoryInitializerPrefixURL,"Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead"),F(void 0===r.pthreadMainPrefixURL,"Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead"),F(void 0===r.cdInitializerPrefixURL,"Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead"),F(void 0===r.filePackagePrefixURL,"Module.filePackagePrefixURL option was removed, use Module.locateFile instead"),F(void 0===r.read,"Module.read option was removed (modify read_ in JS)"),F(void 0===r.readAsync,"Module.readAsync option was removed (modify readAsync in JS)"),F(void 0===r.readBinary,"Module.readBinary option was removed (modify readBinary in JS)"),F(void 0===r.setWindowTitle,"Module.setWindowTitle option was removed (modify setWindowTitle in JS)"),F(void 0===r.TOTAL_MEMORY,"Module.TOTAL_MEMORY has been renamed Module.INITIAL_MEMORY"),k("read","read_"),k("readAsync","readAsync"),k("readBinary","readBinary"),k("setWindowTitle","setWindowTitle"),F(!c,"shell environment detected but not enabled at build time. Add 'shell' to `-sENVIRONMENT` to enable."),r.wasmBinary&&(E=r.wasmBinary),k("wasmBinary","wasmBinary"),r.noExitRuntime,k("noExitRuntime","noExitRuntime"),"object"!=typeof WebAssembly&&ye("no native wasm support detected");var A=!1;function F(e,r){e||ye("Assertion failed"+(r?": "+r:""))}var D="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0;function O(e,r,t){for(var n=r+t,o=r;e[o]&&!(o>=n);)++o;if(o-r>16&&e.buffer&&D)return D.decode(e.subarray(r,o));for(var i="";r>10,56320|1023&u)}}else i+=String.fromCharCode((31&a)<<6|s)}else i+=String.fromCharCode(a)}return i}function R(e,r){return e?O(U,e,r):""}function M(e,r,t,n){if(!(n>0))return 0;for(var o=t,i=t+n-1,a=0;a=55296&&s<=57343)s=65536+((1023&s)<<10)|1023&e.charCodeAt(++a);if(s<=127){if(t>=i)break;r[t++]=s}else if(s<=2047){if(t+1>=i)break;r[t++]=192|s>>6,r[t++]=128|63&s}else if(s<=65535){if(t+2>=i)break;r[t++]=224|s>>12,r[t++]=128|s>>6&63,r[t++]=128|63&s}else{if(t+3>=i)break;s>1114111&&T("Invalid Unicode code point 0x"+s.toString(16)+" encountered when serializing a JS string to a UTF-8 string in wasm memory! (Valid unicode code points should be in range 0-0x10FFFF)."),r[t++]=240|s>>18,r[t++]=128|s>>12&63,r[t++]=128|s>>6&63,r[t++]=128|63&s}}return r[t]=0,t-o}function N(e,r,t){return F("number"==typeof t,"stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!"),M(e,U,r,t)}function I(e){for(var r=0,t=0;t=55296&&n<=57343&&(n=65536+((1023&n)<<10)|1023&e.charCodeAt(++t)),n<=127?++r:r+=n<=2047?2:n<=65535?3:4}return r}var L,x,U,B,j,$,W,z,H,G="undefined"!=typeof TextDecoder?new TextDecoder("utf-16le"):void 0;function V(e,r){F(e%2==0,"Pointer passed to UTF16ToString must be aligned to two bytes!");for(var t=e,n=t>>1,o=n+r/2;!(n>=o)&&j[n];)++n;if((t=n<<1)-e>32&&G)return G.decode(U.subarray(e,t));for(var i="",a=0;!(a>=r/2);++a){var s=B[e+2*a>>1];if(0==s)break;i+=String.fromCharCode(s)}return i}function q(e,r,t){if(F(r%2==0,"Pointer passed to stringToUTF16 must be aligned to two bytes!"),F("number"==typeof t,"stringToUTF16(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!"),void 0===t&&(t=2147483647),t<2)return 0;for(var n=r,o=(t-=2)<2*e.length?t/2:e.length,i=0;i>1]=a,r+=2}return B[r>>1]=0,r-n}function Y(e){return 2*e.length}function X(e,r){F(e%4==0,"Pointer passed to UTF32ToString must be aligned to four bytes!");for(var t=0,n="";!(t>=r/4);){var o=$[e+4*t>>2];if(0==o)break;if(++t,o>=65536){var i=o-65536;n+=String.fromCharCode(55296|i>>10,56320|1023&i)}else n+=String.fromCharCode(o)}return n}function K(e,r,t){if(F(r%4==0,"Pointer passed to stringToUTF32 must be aligned to four bytes!"),F("number"==typeof t,"stringToUTF32(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!"),void 0===t&&(t=2147483647),t<4)return 0;for(var n=r,o=n+t-4,i=0;i=55296&&a<=57343)a=65536+((1023&a)<<10)|1023&e.charCodeAt(++i);if($[r>>2]=a,(r+=4)+4>o)break}return $[r>>2]=0,r-n}function J(e){for(var r=0,t=0;t=55296&&n<=57343&&++t,r+=4}return r}function Q(e){var r=I(e)+1,t=Et(r);return t&&M(e,x,t,r),t}function Z(e){L=e,r.HEAP8=x=new Int8Array(e),r.HEAP16=B=new Int16Array(e),r.HEAP32=$=new Int32Array(e),r.HEAPU8=U=new Uint8Array(e),r.HEAPU16=j=new Uint16Array(e),r.HEAPU32=W=new Uint32Array(e),r.HEAPF32=z=new Float32Array(e),r.HEAPF64=H=new Float64Array(e)}var ee=5242880;r.TOTAL_STACK&&F(ee===r.TOTAL_STACK,"the stack size can no longer be determined at runtime");var re,te=r.INITIAL_MEMORY||67108864;function ne(){var e=Pt();F(!(3&e)),$[e>>2]=34821223,$[e+4>>2]=2310721022,$[0]=1668509029}function oe(){if(!A){var e=Pt(),r=W[e>>2],t=W[e+4>>2];34821223==r&&2310721022==t||ye("Stack overflow! Stack cookie has been overwritten, expected hex dwords 0x89BACDFE and 0x2135467, but received 0x"+t.toString(16)+" 0x"+r.toString(16)),1668509029!==$[0]&&ye("Runtime error: The application has corrupted its heap memory area (address zero)!")}}k("INITIAL_MEMORY","INITIAL_MEMORY"),F(te>=ee,"INITIAL_MEMORY should be larger than TOTAL_STACK, was "+te+"! (TOTAL_STACK="+ee+")"),F("undefined"!=typeof Int32Array&&"undefined"!=typeof Float64Array&&null!=Int32Array.prototype.subarray&&null!=Int32Array.prototype.set,"JS engine does not provide full typed array support"),F(!r.wasmMemory,"Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally"),F(67108864==te,"Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically"),function(){var e=new Int16Array(1),r=new Int8Array(e.buffer);if(e[0]=25459,115!==r[0]||99!==r[1])throw"Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)"}();var ie=[],ae=[],se=[],le=!1;function ue(e){ie.unshift(e)}function ce(e){se.unshift(e)}F(Math.imul,"This browser does not support Math.imul(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill"),F(Math.fround,"This browser does not support Math.fround(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill"),F(Math.clz32,"This browser does not support Math.clz32(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill"),F(Math.trunc,"This browser does not support Math.trunc(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");var de=0,fe=null,pe=null,me={};function he(e){for(var r=e;;){if(!me[e])return e;e=r+Math.random()}}function ge(e){de++,r.monitorRunDependencies&&r.monitorRunDependencies(de),e?(F(!me[e]),me[e]=1,null===fe&&"undefined"!=typeof setInterval&&(fe=setInterval(function(){if(A)return clearInterval(fe),void(fe=null);var e=!1;for(var r in me)e||(e=!0,_("still waiting on run dependencies:")),_("dependency: "+r);e&&_("(end of list)")},1e4))):_("warning: run dependency added without ID")}function ve(e){if(de--,r.monitorRunDependencies&&r.monitorRunDependencies(de),e?(F(me[e]),delete me[e]):_("warning: run dependency removed without ID"),0==de&&(null!==fe&&(clearInterval(fe),fe=null),pe)){var t=pe;pe=null,t()}}function ye(e){throw r.onAbort&&r.onAbort(e),_(e="Aborted("+e+")"),A=!0,new WebAssembly.RuntimeError(e)}var Ee,we,be;function _e(e){return e.startsWith("data:application/octet-stream;base64,")}function Te(e){return e.startsWith("file://")}function ke(e,t){return function(){var n=e,o=t;return t||(o=r.asm),F(le,"native function `"+n+"` called before runtime initialization"),o[e]||F(o[e],"exported native function `"+n+"` not found"),o[e].apply(null,arguments)}}function Se(e){try{if(e==Ee&&E)return new Uint8Array(E);if(p)return p(e);throw"both async and sync fetching of the wasm failed"}catch(e){ye(e)}}function Ce(e){for(;e.length>0;){var t=e.shift();if("function"!=typeof t){var n=t.func;"number"==typeof n?void 0===t.arg?Ae(n)():Ae(n)(t.arg):n(void 0===t.arg?null:t.arg)}else t(r)}}function Pe(e){return e.replace(/\b_Z[\w\d_]+/g,function(e){var r,t=(r=e,T("warning: build with -sDEMANGLE_SUPPORT to link in libcxxabi demangling"),r);return e===t?e:t+" ["+e+"]"})}function Ae(e){return re.get(e)}_e(Ee="decoder.wasm")||(Ee=function(e){return r.locateFile?r.locateFile(e,v):v+e}(Ee));var Fe={isAbs:e=>"/"===e.charAt(0),splitPath:e=>/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/.exec(e).slice(1),normalizeArray:(e,r)=>{for(var t=0,n=e.length-1;n>=0;n--){var o=e[n];"."===o?e.splice(n,1):".."===o?(e.splice(n,1),t++):t&&(e.splice(n,1),t--)}if(r)for(;t;t--)e.unshift("..");return e},normalize:e=>{var r=Fe.isAbs(e),t="/"===e.substr(-1);return(e=Fe.normalizeArray(e.split("/").filter(e=>!!e),!r).join("/"))||r||(e="."),e&&t&&(e+="/"),(r?"/":"")+e},dirname:e=>{var r=Fe.splitPath(e),t=r[0],n=r[1];return t||n?(n&&(n=n.substr(0,n.length-1)),t+n):"."},basename:e=>{if("/"===e)return"/";var r=(e=(e=Fe.normalize(e)).replace(/\/$/,"")).lastIndexOf("/");return-1===r?e:e.substr(r+1)},join:function(){var e=Array.prototype.slice.call(arguments,0);return Fe.normalize(e.join("/"))},join2:(e,r)=>Fe.normalize(e+"/"+r)};var De={resolve:function(){for(var e="",r=!1,t=arguments.length-1;t>=-1&&!r;t--){var n=t>=0?arguments[t]:Le.cwd();if("string"!=typeof n)throw new TypeError("Arguments to path.resolve must be strings");if(!n)return"";e=n+"/"+e,r=Fe.isAbs(n)}return(r?"/":"")+(e=Fe.normalizeArray(e.split("/").filter(e=>!!e),!r).join("/"))||"."},relative:(e,r)=>{function t(e){for(var r=0;r=0&&""===e[t];t--);return r>t?[]:e.slice(r,t-r+1)}e=De.resolve(e).substr(1),r=De.resolve(r).substr(1);for(var n=t(e.split("/")),o=t(r.split("/")),i=Math.min(n.length,o.length),a=i,s=0;s0?t.slice(0,n).toString("utf-8"):null}else"undefined"!=typeof window&&"function"==typeof window.prompt?null!==(r=window.prompt("Input: "))&&(r+="\n"):"function"==typeof readline&&null!==(r=readline())&&(r+="\n");if(!r)return null;e.input=gt(r,!0)}return e.input.shift()},put_char:function(e,r){null===r||10===r?(b(O(e.output,0)),e.output=[]):0!=r&&e.output.push(r)},flush:function(e){e.output&&e.output.length>0&&(b(O(e.output,0)),e.output=[])}},default_tty1_ops:{put_char:function(e,r){null===r||10===r?(_(O(e.output,0)),e.output=[]):0!=r&&e.output.push(r)},flush:function(e){e.output&&e.output.length>0&&(_(O(e.output,0)),e.output=[])}}};function Re(e){e=function(e,r){return F(r,"alignment argument is required"),Math.ceil(e/r)*r}(e,65536);var r=kt(65536,e);return r?(function(e,r){U.fill(0,e,e+r)}(r,e),r):0}var Me={ops_table:null,mount:function(e){return Me.createNode(null,"/",16895,0)},createNode:function(e,r,t,n){if(Le.isBlkdev(t)||Le.isFIFO(t))throw new Le.ErrnoError(63);Me.ops_table||(Me.ops_table={dir:{node:{getattr:Me.node_ops.getattr,setattr:Me.node_ops.setattr,lookup:Me.node_ops.lookup,mknod:Me.node_ops.mknod,rename:Me.node_ops.rename,unlink:Me.node_ops.unlink,rmdir:Me.node_ops.rmdir,readdir:Me.node_ops.readdir,symlink:Me.node_ops.symlink},stream:{llseek:Me.stream_ops.llseek}},file:{node:{getattr:Me.node_ops.getattr,setattr:Me.node_ops.setattr},stream:{llseek:Me.stream_ops.llseek,read:Me.stream_ops.read,write:Me.stream_ops.write,allocate:Me.stream_ops.allocate,mmap:Me.stream_ops.mmap,msync:Me.stream_ops.msync}},link:{node:{getattr:Me.node_ops.getattr,setattr:Me.node_ops.setattr,readlink:Me.node_ops.readlink},stream:{}},chrdev:{node:{getattr:Me.node_ops.getattr,setattr:Me.node_ops.setattr},stream:Le.chrdev_stream_ops}});var o=Le.createNode(e,r,t,n);return Le.isDir(o.mode)?(o.node_ops=Me.ops_table.dir.node,o.stream_ops=Me.ops_table.dir.stream,o.contents={}):Le.isFile(o.mode)?(o.node_ops=Me.ops_table.file.node,o.stream_ops=Me.ops_table.file.stream,o.usedBytes=0,o.contents=null):Le.isLink(o.mode)?(o.node_ops=Me.ops_table.link.node,o.stream_ops=Me.ops_table.link.stream):Le.isChrdev(o.mode)&&(o.node_ops=Me.ops_table.chrdev.node,o.stream_ops=Me.ops_table.chrdev.stream),o.timestamp=Date.now(),e&&(e.contents[r]=o,e.timestamp=o.timestamp),o},getFileDataAsTypedArray:function(e){return e.contents?e.contents.subarray?e.contents.subarray(0,e.usedBytes):new Uint8Array(e.contents):new Uint8Array(0)},expandFileStorage:function(e,r){var t=e.contents?e.contents.length:0;if(!(t>=r)){r=Math.max(r,t*(t<1048576?2:1.125)>>>0),0!=t&&(r=Math.max(r,256));var n=e.contents;e.contents=new Uint8Array(r),e.usedBytes>0&&e.contents.set(n.subarray(0,e.usedBytes),0)}},resizeFileStorage:function(e,r){if(e.usedBytes!=r)if(0==r)e.contents=null,e.usedBytes=0;else{var t=e.contents;e.contents=new Uint8Array(r),t&&e.contents.set(t.subarray(0,Math.min(r,e.usedBytes))),e.usedBytes=r}},node_ops:{getattr:function(e){var r={};return r.dev=Le.isChrdev(e.mode)?e.id:1,r.ino=e.id,r.mode=e.mode,r.nlink=1,r.uid=0,r.gid=0,r.rdev=e.rdev,Le.isDir(e.mode)?r.size=4096:Le.isFile(e.mode)?r.size=e.usedBytes:Le.isLink(e.mode)?r.size=e.link.length:r.size=0,r.atime=new Date(e.timestamp),r.mtime=new Date(e.timestamp),r.ctime=new Date(e.timestamp),r.blksize=4096,r.blocks=Math.ceil(r.size/r.blksize),r},setattr:function(e,r){void 0!==r.mode&&(e.mode=r.mode),void 0!==r.timestamp&&(e.timestamp=r.timestamp),void 0!==r.size&&Me.resizeFileStorage(e,r.size)},lookup:function(e,r){throw Le.genericErrors[44]},mknod:function(e,r,t,n){return Me.createNode(e,r,t,n)},rename:function(e,r,t){if(Le.isDir(e.mode)){var n;try{n=Le.lookupNode(r,t)}catch(e){}if(n)for(var o in n.contents)throw new Le.ErrnoError(55)}delete e.parent.contents[e.name],e.parent.timestamp=Date.now(),e.name=t,r.contents[t]=e,r.timestamp=e.parent.timestamp,e.parent=r},unlink:function(e,r){delete e.contents[r],e.timestamp=Date.now()},rmdir:function(e,r){var t=Le.lookupNode(e,r);for(var n in t.contents)throw new Le.ErrnoError(55);delete e.contents[r],e.timestamp=Date.now()},readdir:function(e){var r=[".",".."];for(var t in e.contents)e.contents.hasOwnProperty(t)&&r.push(t);return r},symlink:function(e,r,t){var n=Me.createNode(e,r,41471,0);return n.link=t,n},readlink:function(e){if(!Le.isLink(e.mode))throw new Le.ErrnoError(28);return e.link}},stream_ops:{read:function(e,r,t,n,o){var i=e.node.contents;if(o>=e.node.usedBytes)return 0;var a=Math.min(e.node.usedBytes-o,n);if(F(a>=0),a>8&&i.subarray)r.set(i.subarray(o,o+a),t);else for(var s=0;s0||n+t{if(!(e=De.resolve(Le.cwd(),e)))return{path:"",node:null};if((r=Object.assign({follow_mount:!0,recurse_count:0},r)).recurse_count>8)throw new Le.ErrnoError(32);for(var t=Fe.normalizeArray(e.split("/").filter(e=>!!e),!1),n=Le.root,o="/",i=0;i40)throw new Le.ErrnoError(32)}}return{path:o,node:n}},getPath:e=>{for(var r;;){if(Le.isRoot(e)){var t=e.mount.mountpoint;return r?"/"!==t[t.length-1]?t+"/"+r:t+r:t}r=r?e.name+"/"+r:e.name,e=e.parent}},hashName:(e,r)=>{for(var t=0,n=0;n>>0)%Le.nameTable.length},hashAddNode:e=>{var r=Le.hashName(e.parent.id,e.name);e.name_next=Le.nameTable[r],Le.nameTable[r]=e},hashRemoveNode:e=>{var r=Le.hashName(e.parent.id,e.name);if(Le.nameTable[r]===e)Le.nameTable[r]=e.name_next;else for(var t=Le.nameTable[r];t;){if(t.name_next===e){t.name_next=e.name_next;break}t=t.name_next}},lookupNode:(e,r)=>{var t=Le.mayLookup(e);if(t)throw new Le.ErrnoError(t,e);for(var n=Le.hashName(e.id,r),o=Le.nameTable[n];o;o=o.name_next){var i=o.name;if(o.parent.id===e.id&&i===r)return o}return Le.lookup(e,r)},createNode:(e,r,t,n)=>{F("object"==typeof e);var o=new Le.FSNode(e,r,t,n);return Le.hashAddNode(o),o},destroyNode:e=>{Le.hashRemoveNode(e)},isRoot:e=>e===e.parent,isMountpoint:e=>!!e.mounted,isFile:e=>32768==(61440&e),isDir:e=>16384==(61440&e),isLink:e=>40960==(61440&e),isChrdev:e=>8192==(61440&e),isBlkdev:e=>24576==(61440&e),isFIFO:e=>4096==(61440&e),isSocket:e=>!(49152&~e),flagModes:{r:0,"r+":2,w:577,"w+":578,a:1089,"a+":1090},modeStringToFlags:e=>{var r=Le.flagModes[e];if(void 0===r)throw new Error("Unknown file open mode: "+e);return r},flagsToPermissionString:e=>{var r=["r","w","rw"][3&e];return 512&e&&(r+="w"),r},nodePermissions:(e,r)=>Le.ignorePermissions||(!r.includes("r")||292&e.mode)&&(!r.includes("w")||146&e.mode)&&(!r.includes("x")||73&e.mode)?0:2,mayLookup:e=>{var r=Le.nodePermissions(e,"x");return r||(e.node_ops.lookup?0:2)},mayCreate:(e,r)=>{try{Le.lookupNode(e,r);return 20}catch(e){}return Le.nodePermissions(e,"wx")},mayDelete:(e,r,t)=>{var n;try{n=Le.lookupNode(e,r)}catch(e){return e.errno}var o=Le.nodePermissions(e,"wx");if(o)return o;if(t){if(!Le.isDir(n.mode))return 54;if(Le.isRoot(n)||Le.getPath(n)===Le.cwd())return 10}else if(Le.isDir(n.mode))return 31;return 0},mayOpen:(e,r)=>e?Le.isLink(e.mode)?32:Le.isDir(e.mode)&&("r"!==Le.flagsToPermissionString(r)||512&r)?31:Le.nodePermissions(e,Le.flagsToPermissionString(r)):44,MAX_OPEN_FDS:4096,nextfd:(e=0,r=Le.MAX_OPEN_FDS)=>{for(var t=e;t<=r;t++)if(!Le.streams[t])return t;throw new Le.ErrnoError(33)},getStream:e=>Le.streams[e],createStream:(e,r,t)=>{Le.FSStream||(Le.FSStream=function(){this.shared={}},Le.FSStream.prototype={object:{get:function(){return this.node},set:function(e){this.node=e}},isRead:{get:function(){return 1!=(2097155&this.flags)}},isWrite:{get:function(){return!!(2097155&this.flags)}},isAppend:{get:function(){return 1024&this.flags}},flags:{get:function(){return this.shared.flags},set:function(e){this.shared.flags=e}},position:{get function(){return this.shared.position},set:function(e){this.shared.position=e}}}),e=Object.assign(new Le.FSStream,e);var n=Le.nextfd(r,t);return e.fd=n,Le.streams[n]=e,e},closeStream:e=>{Le.streams[e]=null},chrdev_stream_ops:{open:e=>{var r=Le.getDevice(e.node.rdev);e.stream_ops=r.stream_ops,e.stream_ops.open&&e.stream_ops.open(e)},llseek:()=>{throw new Le.ErrnoError(70)}},major:e=>e>>8,minor:e=>255&e,makedev:(e,r)=>e<<8|r,registerDevice:(e,r)=>{Le.devices[e]={stream_ops:r}},getDevice:e=>Le.devices[e],getMounts:e=>{for(var r=[],t=[e];t.length;){var n=t.pop();r.push(n),t.push.apply(t,n.mounts)}return r},syncfs:(e,r)=>{"function"==typeof e&&(r=e,e=!1),Le.syncFSRequests++,Le.syncFSRequests>1&&_("warning: "+Le.syncFSRequests+" FS.syncfs operations in flight at once, probably just doing extra work");var t=Le.getMounts(Le.root.mount),n=0;function o(e){return F(Le.syncFSRequests>0),Le.syncFSRequests--,r(e)}function i(e){if(e)return i.errored?void 0:(i.errored=!0,o(e));++n>=t.length&&o(null)}t.forEach(r=>{if(!r.type.syncfs)return i(null);r.type.syncfs(r,e,i)})},mount:(e,r,t)=>{if("string"==typeof e)throw e;var n,o="/"===t,i=!t;if(o&&Le.root)throw new Le.ErrnoError(10);if(!o&&!i){var a=Le.lookupPath(t,{follow_mount:!1});if(t=a.path,n=a.node,Le.isMountpoint(n))throw new Le.ErrnoError(10);if(!Le.isDir(n.mode))throw new Le.ErrnoError(54)}var s={type:e,opts:r,mountpoint:t,mounts:[]},l=e.mount(s);return l.mount=s,s.root=l,o?Le.root=l:n&&(n.mounted=s,n.mount&&n.mount.mounts.push(s)),l},unmount:e=>{var r=Le.lookupPath(e,{follow_mount:!1});if(!Le.isMountpoint(r.node))throw new Le.ErrnoError(28);var t=r.node,n=t.mounted,o=Le.getMounts(n);Object.keys(Le.nameTable).forEach(e=>{for(var r=Le.nameTable[e];r;){var t=r.name_next;o.includes(r.mount)&&Le.destroyNode(r),r=t}}),t.mounted=null;var i=t.mount.mounts.indexOf(n);F(-1!==i),t.mount.mounts.splice(i,1)},lookup:(e,r)=>e.node_ops.lookup(e,r),mknod:(e,r,t)=>{var n=Le.lookupPath(e,{parent:!0}).node,o=Fe.basename(e);if(!o||"."===o||".."===o)throw new Le.ErrnoError(28);var i=Le.mayCreate(n,o);if(i)throw new Le.ErrnoError(i);if(!n.node_ops.mknod)throw new Le.ErrnoError(63);return n.node_ops.mknod(n,o,r,t)},create:(e,r)=>(r=void 0!==r?r:438,r&=4095,r|=32768,Le.mknod(e,r,0)),mkdir:(e,r)=>(r=void 0!==r?r:511,r&=1023,r|=16384,Le.mknod(e,r,0)),mkdirTree:(e,r)=>{for(var t=e.split("/"),n="",o=0;o(void 0===t&&(t=r,r=438),r|=8192,Le.mknod(e,r,t)),symlink:(e,r)=>{if(!De.resolve(e))throw new Le.ErrnoError(44);var t=Le.lookupPath(r,{parent:!0}).node;if(!t)throw new Le.ErrnoError(44);var n=Fe.basename(r),o=Le.mayCreate(t,n);if(o)throw new Le.ErrnoError(o);if(!t.node_ops.symlink)throw new Le.ErrnoError(63);return t.node_ops.symlink(t,n,e)},rename:(e,r)=>{var t,n,o=Fe.dirname(e),i=Fe.dirname(r),a=Fe.basename(e),s=Fe.basename(r);if(t=Le.lookupPath(e,{parent:!0}).node,n=Le.lookupPath(r,{parent:!0}).node,!t||!n)throw new Le.ErrnoError(44);if(t.mount!==n.mount)throw new Le.ErrnoError(75);var l,u=Le.lookupNode(t,a),c=De.relative(e,i);if("."!==c.charAt(0))throw new Le.ErrnoError(28);if("."!==(c=De.relative(r,o)).charAt(0))throw new Le.ErrnoError(55);try{l=Le.lookupNode(n,s)}catch(e){}if(u!==l){var d=Le.isDir(u.mode),f=Le.mayDelete(t,a,d);if(f)throw new Le.ErrnoError(f);if(f=l?Le.mayDelete(n,s,d):Le.mayCreate(n,s))throw new Le.ErrnoError(f);if(!t.node_ops.rename)throw new Le.ErrnoError(63);if(Le.isMountpoint(u)||l&&Le.isMountpoint(l))throw new Le.ErrnoError(10);if(n!==t&&(f=Le.nodePermissions(t,"w")))throw new Le.ErrnoError(f);Le.hashRemoveNode(u);try{t.node_ops.rename(u,n,s)}catch(e){throw e}finally{Le.hashAddNode(u)}}},rmdir:e=>{var r=Le.lookupPath(e,{parent:!0}).node,t=Fe.basename(e),n=Le.lookupNode(r,t),o=Le.mayDelete(r,t,!0);if(o)throw new Le.ErrnoError(o);if(!r.node_ops.rmdir)throw new Le.ErrnoError(63);if(Le.isMountpoint(n))throw new Le.ErrnoError(10);r.node_ops.rmdir(r,t),Le.destroyNode(n)},readdir:e=>{var r=Le.lookupPath(e,{follow:!0}).node;if(!r.node_ops.readdir)throw new Le.ErrnoError(54);return r.node_ops.readdir(r)},unlink:e=>{var r=Le.lookupPath(e,{parent:!0}).node;if(!r)throw new Le.ErrnoError(44);var t=Fe.basename(e),n=Le.lookupNode(r,t),o=Le.mayDelete(r,t,!1);if(o)throw new Le.ErrnoError(o);if(!r.node_ops.unlink)throw new Le.ErrnoError(63);if(Le.isMountpoint(n))throw new Le.ErrnoError(10);r.node_ops.unlink(r,t),Le.destroyNode(n)},readlink:e=>{var r=Le.lookupPath(e).node;if(!r)throw new Le.ErrnoError(44);if(!r.node_ops.readlink)throw new Le.ErrnoError(28);return De.resolve(Le.getPath(r.parent),r.node_ops.readlink(r))},stat:(e,r)=>{var t=Le.lookupPath(e,{follow:!r}).node;if(!t)throw new Le.ErrnoError(44);if(!t.node_ops.getattr)throw new Le.ErrnoError(63);return t.node_ops.getattr(t)},lstat:e=>Le.stat(e,!0),chmod:(e,r,t)=>{var n;"string"==typeof e?n=Le.lookupPath(e,{follow:!t}).node:n=e;if(!n.node_ops.setattr)throw new Le.ErrnoError(63);n.node_ops.setattr(n,{mode:4095&r|-4096&n.mode,timestamp:Date.now()})},lchmod:(e,r)=>{Le.chmod(e,r,!0)},fchmod:(e,r)=>{var t=Le.getStream(e);if(!t)throw new Le.ErrnoError(8);Le.chmod(t.node,r)},chown:(e,r,t,n)=>{var o;"string"==typeof e?o=Le.lookupPath(e,{follow:!n}).node:o=e;if(!o.node_ops.setattr)throw new Le.ErrnoError(63);o.node_ops.setattr(o,{timestamp:Date.now()})},lchown:(e,r,t)=>{Le.chown(e,r,t,!0)},fchown:(e,r,t)=>{var n=Le.getStream(e);if(!n)throw new Le.ErrnoError(8);Le.chown(n.node,r,t)},truncate:(e,r)=>{if(r<0)throw new Le.ErrnoError(28);var t;"string"==typeof e?t=Le.lookupPath(e,{follow:!0}).node:t=e;if(!t.node_ops.setattr)throw new Le.ErrnoError(63);if(Le.isDir(t.mode))throw new Le.ErrnoError(31);if(!Le.isFile(t.mode))throw new Le.ErrnoError(28);var n=Le.nodePermissions(t,"w");if(n)throw new Le.ErrnoError(n);t.node_ops.setattr(t,{size:r,timestamp:Date.now()})},ftruncate:(e,r)=>{var t=Le.getStream(e);if(!t)throw new Le.ErrnoError(8);if(!(2097155&t.flags))throw new Le.ErrnoError(28);Le.truncate(t.node,r)},utime:(e,r,t)=>{var n=Le.lookupPath(e,{follow:!0}).node;n.node_ops.setattr(n,{timestamp:Math.max(r,t)})},open:(e,t,n,o,i)=>{if(""===e)throw new Le.ErrnoError(44);var a;if(n=void 0===n?438:n,n=64&(t="string"==typeof t?Le.modeStringToFlags(t):t)?4095&n|32768:0,"object"==typeof e)a=e;else{e=Fe.normalize(e);try{a=Le.lookupPath(e,{follow:!(131072&t)}).node}catch(e){}}var s=!1;if(64&t)if(a){if(128&t)throw new Le.ErrnoError(20)}else a=Le.mknod(e,n,0),s=!0;if(!a)throw new Le.ErrnoError(44);if(Le.isChrdev(a.mode)&&(t&=-513),65536&t&&!Le.isDir(a.mode))throw new Le.ErrnoError(54);if(!s){var l=Le.mayOpen(a,t);if(l)throw new Le.ErrnoError(l)}512&t&&Le.truncate(a,0),t&=-131713;var u=Le.createStream({node:a,path:Le.getPath(a),flags:t,seekable:!0,position:0,stream_ops:a.stream_ops,ungotten:[],error:!1},o,i);return u.stream_ops.open&&u.stream_ops.open(u),!r.logReadFiles||1&t||(Le.readFiles||(Le.readFiles={}),e in Le.readFiles||(Le.readFiles[e]=1)),u},close:e=>{if(Le.isClosed(e))throw new Le.ErrnoError(8);e.getdents&&(e.getdents=null);try{e.stream_ops.close&&e.stream_ops.close(e)}catch(e){throw e}finally{Le.closeStream(e.fd)}e.fd=null},isClosed:e=>null===e.fd,llseek:(e,r,t)=>{if(Le.isClosed(e))throw new Le.ErrnoError(8);if(!e.seekable||!e.stream_ops.llseek)throw new Le.ErrnoError(70);if(0!=t&&1!=t&&2!=t)throw new Le.ErrnoError(28);return e.position=e.stream_ops.llseek(e,r,t),e.ungotten=[],e.position},read:(e,r,t,n,o)=>{if(n<0||o<0)throw new Le.ErrnoError(28);if(Le.isClosed(e))throw new Le.ErrnoError(8);if(1==(2097155&e.flags))throw new Le.ErrnoError(8);if(Le.isDir(e.node.mode))throw new Le.ErrnoError(31);if(!e.stream_ops.read)throw new Le.ErrnoError(28);var i=void 0!==o;if(i){if(!e.seekable)throw new Le.ErrnoError(70)}else o=e.position;var a=e.stream_ops.read(e,r,t,n,o);return i||(e.position+=a),a},write:(e,r,t,n,o,i)=>{if(n<0||o<0)throw new Le.ErrnoError(28);if(Le.isClosed(e))throw new Le.ErrnoError(8);if(!(2097155&e.flags))throw new Le.ErrnoError(8);if(Le.isDir(e.node.mode))throw new Le.ErrnoError(31);if(!e.stream_ops.write)throw new Le.ErrnoError(28);e.seekable&&1024&e.flags&&Le.llseek(e,0,2);var a=void 0!==o;if(a){if(!e.seekable)throw new Le.ErrnoError(70)}else o=e.position;var s=e.stream_ops.write(e,r,t,n,o,i);return a||(e.position+=s),s},allocate:(e,r,t)=>{if(Le.isClosed(e))throw new Le.ErrnoError(8);if(r<0||t<=0)throw new Le.ErrnoError(28);if(!(2097155&e.flags))throw new Le.ErrnoError(8);if(!Le.isFile(e.node.mode)&&!Le.isDir(e.node.mode))throw new Le.ErrnoError(43);if(!e.stream_ops.allocate)throw new Le.ErrnoError(138);e.stream_ops.allocate(e,r,t)},mmap:(e,r,t,n,o,i)=>{if(2&o&&!(2&i)&&2!=(2097155&e.flags))throw new Le.ErrnoError(2);if(1==(2097155&e.flags))throw new Le.ErrnoError(2);if(!e.stream_ops.mmap)throw new Le.ErrnoError(43);return e.stream_ops.mmap(e,r,t,n,o,i)},msync:(e,r,t,n,o)=>e&&e.stream_ops.msync?e.stream_ops.msync(e,r,t,n,o):0,munmap:e=>0,ioctl:(e,r,t)=>{if(!e.stream_ops.ioctl)throw new Le.ErrnoError(59);return e.stream_ops.ioctl(e,r,t)},readFile:(e,r={})=>{if(r.flags=r.flags||0,r.encoding=r.encoding||"binary","utf8"!==r.encoding&&"binary"!==r.encoding)throw new Error('Invalid encoding type "'+r.encoding+'"');var t,n=Le.open(e,r.flags),o=Le.stat(e).size,i=new Uint8Array(o);return Le.read(n,i,0,o,0),"utf8"===r.encoding?t=O(i,0):"binary"===r.encoding&&(t=i),Le.close(n),t},writeFile:(e,r,t={})=>{t.flags=t.flags||577;var n=Le.open(e,t.flags,t.mode);if("string"==typeof r){var o=new Uint8Array(I(r)+1),i=M(r,o,0,o.length);Le.write(n,o,0,i,void 0,t.canOwn)}else{if(!ArrayBuffer.isView(r))throw new Error("Unsupported data type");Le.write(n,r,0,r.byteLength,void 0,t.canOwn)}Le.close(n)},cwd:()=>Le.currentPath,chdir:e=>{var r=Le.lookupPath(e,{follow:!0});if(null===r.node)throw new Le.ErrnoError(44);if(!Le.isDir(r.node.mode))throw new Le.ErrnoError(54);var t=Le.nodePermissions(r.node,"x");if(t)throw new Le.ErrnoError(t);Le.currentPath=r.path},createDefaultDirectories:()=>{Le.mkdir("/tmp"),Le.mkdir("/home"),Le.mkdir("/home/web_user")},createDefaultDevices:()=>{Le.mkdir("/dev"),Le.registerDevice(Le.makedev(1,3),{read:()=>0,write:(e,r,t,n,o)=>n}),Le.mkdev("/dev/null",Le.makedev(1,3)),Oe.register(Le.makedev(5,0),Oe.default_tty_ops),Oe.register(Le.makedev(6,0),Oe.default_tty1_ops),Le.mkdev("/dev/tty",Le.makedev(5,0)),Le.mkdev("/dev/tty1",Le.makedev(6,0));var e=function(){if("object"==typeof crypto&&"function"==typeof crypto.getRandomValues){var e=new Uint8Array(1);return function(){return crypto.getRandomValues(e),e[0]}}if(u)try{var r=a.default;return function(){return r.randomBytes(1)[0]}}catch(e){}return function(){ye("no cryptographic support found for randomDevice. consider polyfilling it if you want to use something insecure like Math.random(), e.g. put this in a --pre-js: var crypto = { getRandomValues: function(array) { for (var i = 0; i < array.length; i++) array[i] = (Math.random()*256)|0 } };")}}();Le.createDevice("/dev","random",e),Le.createDevice("/dev","urandom",e),Le.mkdir("/dev/shm"),Le.mkdir("/dev/shm/tmp")},createSpecialDirectories:()=>{Le.mkdir("/proc");var e=Le.mkdir("/proc/self");Le.mkdir("/proc/self/fd"),Le.mount({mount:()=>{var r=Le.createNode(e,"fd",16895,73);return r.node_ops={lookup:(e,r)=>{var t=+r,n=Le.getStream(t);if(!n)throw new Le.ErrnoError(8);var o={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>n.path}};return o.parent=o,o}},r}},{},"/proc/self/fd")},createStandardStreams:()=>{r.stdin?Le.createDevice("/dev","stdin",r.stdin):Le.symlink("/dev/tty","/dev/stdin"),r.stdout?Le.createDevice("/dev","stdout",null,r.stdout):Le.symlink("/dev/tty","/dev/stdout"),r.stderr?Le.createDevice("/dev","stderr",null,r.stderr):Le.symlink("/dev/tty1","/dev/stderr");var e=Le.open("/dev/stdin",0),t=Le.open("/dev/stdout",1),n=Le.open("/dev/stderr",1);F(0===e.fd,"invalid handle for stdin ("+e.fd+")"),F(1===t.fd,"invalid handle for stdout ("+t.fd+")"),F(2===n.fd,"invalid handle for stderr ("+n.fd+")")},ensureErrnoError:()=>{Le.ErrnoError||(Le.ErrnoError=function(e,r){this.node=r,this.setErrno=function(e){for(var r in this.errno=e,Ie)if(Ie[r]===e){this.code=r;break}},this.setErrno(e),this.message=Ne[e],this.stack&&(Object.defineProperty(this,"stack",{value:(new Error).stack,writable:!0}),this.stack=Pe(this.stack))},Le.ErrnoError.prototype=new Error,Le.ErrnoError.prototype.constructor=Le.ErrnoError,[44].forEach(e=>{Le.genericErrors[e]=new Le.ErrnoError(e),Le.genericErrors[e].stack=""}))},staticInit:()=>{Le.ensureErrnoError(),Le.nameTable=new Array(4096),Le.mount(Me,{},"/"),Le.createDefaultDirectories(),Le.createDefaultDevices(),Le.createSpecialDirectories(),Le.filesystems={MEMFS:Me}},init:(e,t,n)=>{F(!Le.init.initialized,"FS.init was previously called. If you want to initialize later with custom parameters, remove any earlier calls (note that one is automatically added to the generated code)"),Le.init.initialized=!0,Le.ensureErrnoError(),r.stdin=e||r.stdin,r.stdout=t||r.stdout,r.stderr=n||r.stderr,Le.createStandardStreams()},quit:()=>{Le.init.initialized=!1,Tt();for(var e=0;e{var t=0;return e&&(t|=365),r&&(t|=146),t},findObject:(e,r)=>{var t=Le.analyzePath(e,r);return t.exists?t.object:null},analyzePath:(e,r)=>{try{e=(n=Le.lookupPath(e,{follow:!r})).path}catch(e){}var t={isRoot:!1,exists:!1,error:0,name:null,path:null,object:null,parentExists:!1,parentPath:null,parentObject:null};try{var n=Le.lookupPath(e,{parent:!0});t.parentExists=!0,t.parentPath=n.path,t.parentObject=n.node,t.name=Fe.basename(e),n=Le.lookupPath(e,{follow:!r}),t.exists=!0,t.path=n.path,t.object=n.node,t.name=n.node.name,t.isRoot="/"===n.path}catch(e){t.error=e.errno}return t},createPath:(e,r,t,n)=>{e="string"==typeof e?e:Le.getPath(e);for(var o=r.split("/").reverse();o.length;){var i=o.pop();if(i){var a=Fe.join2(e,i);try{Le.mkdir(a)}catch(e){}e=a}}return a},createFile:(e,r,t,n,o)=>{var i=Fe.join2("string"==typeof e?e:Le.getPath(e),r),a=Le.getMode(n,o);return Le.create(i,a)},createDataFile:(e,r,t,n,o,i)=>{var a=r;e&&(e="string"==typeof e?e:Le.getPath(e),a=r?Fe.join2(e,r):e);var s=Le.getMode(n,o),l=Le.create(a,s);if(t){if("string"==typeof t){for(var u=new Array(t.length),c=0,d=t.length;c{var o=Fe.join2("string"==typeof e?e:Le.getPath(e),r),i=Le.getMode(!!t,!!n);Le.createDevice.major||(Le.createDevice.major=64);var a=Le.makedev(Le.createDevice.major++,0);return Le.registerDevice(a,{open:e=>{e.seekable=!1},close:e=>{n&&n.buffer&&n.buffer.length&&n(10)},read:(e,r,n,o,i)=>{for(var a=0,s=0;s{for(var a=0;a{if(e.isDevice||e.isFolder||e.link||e.contents)return!0;if("undefined"!=typeof XMLHttpRequest)throw new Error("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread.");if(!d)throw new Error("Cannot load without read() or XMLHttpRequest.");try{e.contents=gt(d(e.url),!0),e.usedBytes=e.contents.length}catch(e){throw new Le.ErrnoError(29)}},createLazyFile:(e,r,t,n,o)=>{function i(){this.lengthKnown=!1,this.chunks=[]}if(i.prototype.get=function(e){if(!(e>this.length-1||e<0)){var r=e%this.chunkSize,t=e/this.chunkSize|0;return this.getter(t)[r]}},i.prototype.setDataGetter=function(e){this.getter=e},i.prototype.cacheLength=function(){var e=new XMLHttpRequest;if(e.open("HEAD",t,!1),e.send(null),!(e.status>=200&&e.status<300||304===e.status))throw new Error("Couldn't load "+t+". Status: "+e.status);var r,n=Number(e.getResponseHeader("Content-length")),o=(r=e.getResponseHeader("Accept-Ranges"))&&"bytes"===r,i=(r=e.getResponseHeader("Content-Encoding"))&&"gzip"===r,a=1048576;o||(a=n);var s=this;s.setDataGetter(e=>{var r=e*a,o=(e+1)*a-1;if(o=Math.min(o,n-1),void 0===s.chunks[e]&&(s.chunks[e]=((e,r)=>{if(e>r)throw new Error("invalid range ("+e+", "+r+") or no bytes requested!");if(r>n-1)throw new Error("only "+n+" bytes available! programmer error!");var o=new XMLHttpRequest;if(o.open("GET",t,!1),n!==a&&o.setRequestHeader("Range","bytes="+e+"-"+r),o.responseType="arraybuffer",o.overrideMimeType&&o.overrideMimeType("text/plain; charset=x-user-defined"),o.send(null),!(o.status>=200&&o.status<300||304===o.status))throw new Error("Couldn't load "+t+". Status: "+o.status);return void 0!==o.response?new Uint8Array(o.response||[]):gt(o.responseText||"",!0)})(r,o)),void 0===s.chunks[e])throw new Error("doXHR failed!");return s.chunks[e]}),!i&&n||(a=n=1,n=this.getter(0).length,a=n,b("LazyFiles on gzip forces download of the whole file when length is accessed")),this._length=n,this._chunkSize=a,this.lengthKnown=!0},"undefined"!=typeof XMLHttpRequest){if(!l)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var a=new i;Object.defineProperties(a,{length:{get:function(){return this.lengthKnown||this.cacheLength(),this._length}},chunkSize:{get:function(){return this.lengthKnown||this.cacheLength(),this._chunkSize}}});var s={isDevice:!1,contents:a}}else s={isDevice:!1,url:t};var u=Le.createFile(e,r,s,n,o);s.contents?u.contents=s.contents:s.url&&(u.contents=null,u.url=s.url),Object.defineProperties(u,{usedBytes:{get:function(){return this.contents.length}}});var c={};return Object.keys(u.stream_ops).forEach(e=>{var r=u.stream_ops[e];c[e]=function(){return Le.forceLoadFile(u),r.apply(null,arguments)}}),c.read=(e,r,t,n,o)=>{Le.forceLoadFile(u);var i=e.node.contents;if(o>=i.length)return 0;var a=Math.min(i.length-o,n);if(F(a>=0),i.slice)for(var s=0;s{var c=r?De.resolve(Fe.join2(e,r)):e,d=he("cp "+c);function p(t){function f(t){u&&u(),s||Le.createDataFile(e,r,t,n,o,l),i&&i(),ve(d)}Browser.handledByPreloadPlugin(t,c,f,()=>{a&&a(),ve(d)})||f(t)}ge(d),"string"==typeof t?function(e,r,t,n){var o=n?"":he("al "+e);f(e,function(t){F(t,'Loading data file "'+e+'" failed (no arrayBuffer).'),r(new Uint8Array(t)),o&&ve(o)},function(r){if(!t)throw'Loading data file "'+e+'" failed.';t()}),o&&ge(o)}(t,e=>p(e),a):p(t)},indexedDB:()=>window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB,DB_NAME:()=>"EM_FS_"+window.location.pathname,DB_VERSION:20,DB_STORE_NAME:"FILE_DATA",saveFilesToDB:(e,r,t)=>{r=r||(()=>{}),t=t||(()=>{});var n=Le.indexedDB();try{var o=n.open(Le.DB_NAME(),Le.DB_VERSION)}catch(e){return t(e)}o.onupgradeneeded=()=>{b("creating db"),o.result.createObjectStore(Le.DB_STORE_NAME)},o.onsuccess=()=>{var n=o.result.transaction([Le.DB_STORE_NAME],"readwrite"),i=n.objectStore(Le.DB_STORE_NAME),a=0,s=0,l=e.length;function u(){0==s?r():t()}e.forEach(e=>{var r=i.put(Le.analyzePath(e).object.contents,e);r.onsuccess=()=>{++a+s==l&&u()},r.onerror=()=>{s++,a+s==l&&u()}}),n.onerror=t},o.onerror=t},loadFilesFromDB:(e,r,t)=>{r=r||(()=>{}),t=t||(()=>{});var n=Le.indexedDB();try{var o=n.open(Le.DB_NAME(),Le.DB_VERSION)}catch(e){return t(e)}o.onupgradeneeded=t,o.onsuccess=()=>{var n=o.result;try{var i=n.transaction([Le.DB_STORE_NAME],"readonly")}catch(e){return void t(e)}var a=i.objectStore(Le.DB_STORE_NAME),s=0,l=0,u=e.length;function c(){0==l?r():t()}e.forEach(e=>{var r=a.get(e);r.onsuccess=()=>{Le.analyzePath(e).exists&&Le.unlink(e),Le.createDataFile(Fe.dirname(e),Fe.basename(e),r.result,!0,!0,!0),++s+l==u&&c()},r.onerror=()=>{l++,s+l==u&&c()}}),i.onerror=t},o.onerror=t},absolutePath:()=>{ye("FS.absolutePath has been removed; use PATH_FS.resolve instead")},createFolder:()=>{ye("FS.createFolder has been removed; use FS.mkdir instead")},createLink:()=>{ye("FS.createLink has been removed; use FS.symlink instead")},joinPath:()=>{ye("FS.joinPath has been removed; use PATH.join instead")},mmapAlloc:()=>{ye("FS.mmapAlloc has been replaced by the top level function mmapAlloc")},standardizePath:()=>{ye("FS.standardizePath has been removed; use PATH.normalize instead")}},xe={DEFAULT_POLLMASK:5,calculateAt:function(e,r,t){if(Fe.isAbs(r))return r;var n;if(-100===e)n=Le.cwd();else{var o=Le.getStream(e);if(!o)throw new Le.ErrnoError(8);n=o.path}if(0==r.length){if(!t)throw new Le.ErrnoError(44);return n}return Fe.join2(n,r)},doStat:function(e,r,t){try{var n=e(r)}catch(e){if(e&&e.node&&Fe.normalize(r)!==Fe.normalize(Le.getPath(e.node)))return-54;throw e}return $[t>>2]=n.dev,$[t+4>>2]=0,$[t+8>>2]=n.ino,$[t+12>>2]=n.mode,$[t+16>>2]=n.nlink,$[t+20>>2]=n.uid,$[t+24>>2]=n.gid,$[t+28>>2]=n.rdev,$[t+32>>2]=0,be=[n.size>>>0,(we=n.size,+Math.abs(we)>=1?we>0?(0|Math.min(+Math.floor(we/4294967296),4294967295))>>>0:~~+Math.ceil((we-+(~~we>>>0))/4294967296)>>>0:0)],$[t+40>>2]=be[0],$[t+44>>2]=be[1],$[t+48>>2]=4096,$[t+52>>2]=n.blocks,$[t+56>>2]=n.atime.getTime()/1e3|0,$[t+60>>2]=0,$[t+64>>2]=n.mtime.getTime()/1e3|0,$[t+68>>2]=0,$[t+72>>2]=n.ctime.getTime()/1e3|0,$[t+76>>2]=0,be=[n.ino>>>0,(we=n.ino,+Math.abs(we)>=1?we>0?(0|Math.min(+Math.floor(we/4294967296),4294967295))>>>0:~~+Math.ceil((we-+(~~we>>>0))/4294967296)>>>0:0)],$[t+80>>2]=be[0],$[t+84>>2]=be[1],0},doMsync:function(e,r,t,n,o){var i=U.slice(e,e+t);Le.msync(r,i,o,t,n)},doMknod:function(e,r,t){switch(61440&r){case 32768:case 8192:case 24576:case 4096:case 49152:break;default:return-28}return Le.mknod(e,r,t),0},doReadlink:function(e,r,t){if(t<=0)return-28;var n=Le.readlink(e),o=Math.min(t,I(n)),i=x[r+o];return N(n,r,t+1),x[r+o]=i,o},doAccess:function(e,r){if(-8&r)return-28;var t=Le.lookupPath(e,{follow:!0}).node;if(!t)return-44;var n="";return 4&r&&(n+="r"),2&r&&(n+="w"),1&r&&(n+="x"),n&&Le.nodePermissions(t,n)?-2:0},doReadv:function(e,r,t,n){for(var o=0,i=0;i>2],s=$[r+4>>2];r+=8;var l=Le.read(e,x,a,s,n);if(l<0)return-1;if(o+=l,l>2],s=$[r+4>>2];r+=8;var l=Le.write(e,x,a,s,n);if(l<0)return-1;o+=l}return o},varargs:void 0,get:function(){return F(null!=xe.varargs),xe.varargs+=4,$[xe.varargs-4>>2]},getStr:function(e){return R(e)},getStreamFromFD:function(e){var r=Le.getStream(e);if(!r)throw new Le.ErrnoError(8);return r}};function Ue(e){switch(e){case 1:return 0;case 2:return 1;case 4:return 2;case 8:return 3;default:throw new TypeError("Unknown type size: "+e)}}var Be=void 0;function je(e){for(var r="",t=e;U[t];)r+=Be[U[t++]];return r}var $e={},We={},ze={};function He(e){if(void 0===e)return"_unknown";var r=(e=e.replace(/[^a-zA-Z0-9_]/g,"$")).charCodeAt(0);return r>=48&&r<=57?"_"+e:e}function Ge(e,r){return e=He(e),new Function("body","return function "+e+'() {\n "use strict"; return body.apply(this, arguments);\n};\n')(r)}function Ve(e,r){var t=Ge(r,function(e){this.name=r,this.message=e;var t=new Error(e).stack;void 0!==t&&(this.stack=this.toString()+"\n"+t.replace(/^Error(:[^\n]*)?\n/,""))});return t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.prototype.toString=function(){return void 0===this.message?this.name:this.name+": "+this.message},t}var qe=void 0;function Ye(e){throw new qe(e)}var Xe=void 0;function Ke(e){throw new Xe(e)}function Je(e,r,t){function n(r){var n=t(r);n.length!==e.length&&Ke("Mismatched type converter count");for(var o=0;o{We.hasOwnProperty(e)?o[r]=We[e]:(i.push(e),$e.hasOwnProperty(e)||($e[e]=[]),$e[e].push(()=>{o[r]=We[e],++a===i.length&&n(o)}))}),0===i.length&&n(o)}function Qe(e,r,t={}){if(!("argPackAdvance"in r))throw new TypeError("registerType registeredInstance requires argPackAdvance");var n=r.name;if(e||Ye('type "'+n+'" must have a positive integer typeid pointer'),We.hasOwnProperty(e)){if(t.ignoreDuplicateRegistrations)return;Ye("Cannot register type '"+n+"' twice")}if(We[e]=r,delete ze[e],$e.hasOwnProperty(e)){var o=$e[e];delete $e[e],o.forEach(e=>e())}}function Ze(e){if(!(this instanceof br))return!1;if(!(e instanceof br))return!1;for(var r=this.$$.ptrType.registeredClass,t=this.$$.ptr,n=e.$$.ptrType.registeredClass,o=e.$$.ptr;r.baseClass;)t=r.upcast(t),r=r.baseClass;for(;n.baseClass;)o=n.upcast(o),n=n.baseClass;return r===n&&t===o}function er(e){Ye(e.$$.ptrType.registeredClass.name+" instance already deleted")}var rr=!1;function tr(e){}function nr(e){e.count.value-=1,0===e.count.value&&function(e){e.smartPtr?e.smartPtrType.rawDestructor(e.smartPtr):e.ptrType.registeredClass.rawDestructor(e.ptr)}(e)}function or(e,r,t){if(r===t)return e;if(void 0===t.baseClass)return null;var n=or(e,r,t.baseClass);return null===n?null:t.downcast(n)}var ir={};function ar(){return Object.keys(fr).length}function sr(){var e=[];for(var r in fr)fr.hasOwnProperty(r)&&e.push(fr[r]);return e}var lr=[];function ur(){for(;lr.length;){var e=lr.pop();e.$$.deleteScheduled=!1,e.delete()}}var cr=void 0;function dr(e){cr=e,lr.length&&cr&&cr(ur)}var fr={};function pr(e,r){return r=function(e,r){for(void 0===r&&Ye("ptr should not be undefined");e.baseClass;)r=e.upcast(r),e=e.baseClass;return r}(e,r),fr[r]}function mr(e,r){return r.ptrType&&r.ptr||Ke("makeClassHandle requires ptr and ptrType"),!!r.smartPtrType!==!!r.smartPtr&&Ke("Both smartPtrType and smartPtr must be specified"),r.count={value:1},gr(Object.create(e,{$$:{value:r}}))}function hr(e){var r=this.getPointee(e);if(!r)return this.destructor(e),null;var t=pr(this.registeredClass,r);if(void 0!==t){if(0===t.$$.count.value)return t.$$.ptr=r,t.$$.smartPtr=e,t.clone();var n=t.clone();return this.destructor(e),n}function o(){return this.isSmartPointer?mr(this.registeredClass.instancePrototype,{ptrType:this.pointeeType,ptr:r,smartPtrType:this,smartPtr:e}):mr(this.registeredClass.instancePrototype,{ptrType:this,ptr:e})}var i,a=this.registeredClass.getActualType(r),s=ir[a];if(!s)return o.call(this);i=this.isConst?s.constPointerType:s.pointerType;var l=or(r,this.registeredClass,i.registeredClass);return null===l?o.call(this):this.isSmartPointer?mr(i.registeredClass.instancePrototype,{ptrType:i,ptr:l,smartPtrType:this,smartPtr:e}):mr(i.registeredClass.instancePrototype,{ptrType:i,ptr:l})}function gr(e){return"undefined"==typeof FinalizationRegistry?(gr=e=>e,e):(rr=new FinalizationRegistry(e=>{console.warn(e.leakWarning.stack.replace(/^Error: /,"")),nr(e.$$)}),gr=e=>{var r=e.$$;if(!!r.smartPtr){var t={$$:r},n=r.ptrType.registeredClass;t.leakWarning=new Error("Embind found a leaked C++ instance "+n.name+" <0x"+r.ptr.toString(16)+">.\nWe'll free it automatically in this case, but this functionality is not reliable across various environments.\nMake sure to invoke .delete() manually once you're done with the instance instead.\nOriginally allocated"),"captureStackTrace"in Error&&Error.captureStackTrace(t.leakWarning,hr),rr.register(e,t,e)}return e},tr=e=>rr.unregister(e),gr(e))}function vr(){if(this.$$.ptr||er(this),this.$$.preservePointerOnDelete)return this.$$.count.value+=1,this;var e,r=gr(Object.create(Object.getPrototypeOf(this),{$$:{value:(e=this.$$,{count:e.count,deleteScheduled:e.deleteScheduled,preservePointerOnDelete:e.preservePointerOnDelete,ptr:e.ptr,ptrType:e.ptrType,smartPtr:e.smartPtr,smartPtrType:e.smartPtrType})}}));return r.$$.count.value+=1,r.$$.deleteScheduled=!1,r}function yr(){this.$$.ptr||er(this),this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete&&Ye("Object already scheduled for deletion"),tr(this),nr(this.$$),this.$$.preservePointerOnDelete||(this.$$.smartPtr=void 0,this.$$.ptr=void 0)}function Er(){return!this.$$.ptr}function wr(){return this.$$.ptr||er(this),this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete&&Ye("Object already scheduled for deletion"),lr.push(this),1===lr.length&&cr&&cr(ur),this.$$.deleteScheduled=!0,this}function br(){}function _r(e,r,t){if(void 0===e[r].overloadTable){var n=e[r];e[r]=function(){return e[r].overloadTable.hasOwnProperty(arguments.length)||Ye("Function '"+t+"' called with an invalid number of arguments ("+arguments.length+") - expects one of ("+e[r].overloadTable+")!"),e[r].overloadTable[arguments.length].apply(this,arguments)},e[r].overloadTable=[],e[r].overloadTable[n.argCount]=n}}function Tr(e,r,t,n,o,i,a,s){this.name=e,this.constructor=r,this.instancePrototype=t,this.rawDestructor=n,this.baseClass=o,this.getActualType=i,this.upcast=a,this.downcast=s,this.pureVirtualFunctions=[]}function kr(e,r,t){for(;r!==t;)r.upcast||Ye("Expected null or instance of "+t.name+", got an instance of "+r.name),e=r.upcast(e),r=r.baseClass;return e}function Sr(e,r){if(null===r)return this.isReference&&Ye("null is not a valid "+this.name),0;r.$$||Ye('Cannot pass "'+Xr(r)+'" as a '+this.name),r.$$.ptr||Ye("Cannot pass deleted object as a pointer of type "+this.name);var t=r.$$.ptrType.registeredClass;return kr(r.$$.ptr,t,this.registeredClass)}function Cr(e,r){var t;if(null===r)return this.isReference&&Ye("null is not a valid "+this.name),this.isSmartPointer?(t=this.rawConstructor(),null!==e&&e.push(this.rawDestructor,t),t):0;r.$$||Ye('Cannot pass "'+Xr(r)+'" as a '+this.name),r.$$.ptr||Ye("Cannot pass deleted object as a pointer of type "+this.name),!this.isConst&&r.$$.ptrType.isConst&&Ye("Cannot convert argument of type "+(r.$$.smartPtrType?r.$$.smartPtrType.name:r.$$.ptrType.name)+" to parameter type "+this.name);var n=r.$$.ptrType.registeredClass;if(t=kr(r.$$.ptr,n,this.registeredClass),this.isSmartPointer)switch(void 0===r.$$.smartPtr&&Ye("Passing raw pointer to smart pointer is illegal"),this.sharingPolicy){case 0:r.$$.smartPtrType===this?t=r.$$.smartPtr:Ye("Cannot convert argument of type "+(r.$$.smartPtrType?r.$$.smartPtrType.name:r.$$.ptrType.name)+" to parameter type "+this.name);break;case 1:t=r.$$.smartPtr;break;case 2:if(r.$$.smartPtrType===this)t=r.$$.smartPtr;else{var o=r.clone();t=this.rawShare(t,Yr.toHandle(function(){o.delete()})),null!==e&&e.push(this.rawDestructor,t)}break;default:Ye("Unsupporting sharing policy")}return t}function Pr(e,r){if(null===r)return this.isReference&&Ye("null is not a valid "+this.name),0;r.$$||Ye('Cannot pass "'+Xr(r)+'" as a '+this.name),r.$$.ptr||Ye("Cannot pass deleted object as a pointer of type "+this.name),r.$$.ptrType.isConst&&Ye("Cannot convert argument of type "+r.$$.ptrType.name+" to parameter type "+this.name);var t=r.$$.ptrType.registeredClass;return kr(r.$$.ptr,t,this.registeredClass)}function Ar(e){return this.fromWireType(W[e>>2])}function Fr(e){return this.rawGetPointee&&(e=this.rawGetPointee(e)),e}function Dr(e){this.rawDestructor&&this.rawDestructor(e)}function Or(e){null!==e&&e.delete()}function Rr(e,r,t,n,o,i,a,s,l,u,c){this.name=e,this.registeredClass=r,this.isReference=t,this.isConst=n,this.isSmartPointer=o,this.pointeeType=i,this.sharingPolicy=a,this.rawGetPointee=s,this.rawConstructor=l,this.rawShare=u,this.rawDestructor=c,o||void 0!==r.baseClass?this.toWireType=Cr:n?(this.toWireType=Sr,this.destructorFunction=null):(this.toWireType=Pr,this.destructorFunction=null)}function Mr(e,t,n){return e.includes("j")?function(e,t,n){F("dynCall_"+e in r,"bad function pointer type - no table for sig '"+e+"'"),n&&n.length?F(n.length===e.substring(1).replace(/j/g,"--").length):F(1==e.length);var o=r["dynCall_"+e];return n&&n.length?o.apply(null,[t].concat(n)):o.call(null,t)}(e,t,n):(F(Ae(t),"missing table entry in dynCall: "+t),Ae(t).apply(null,n))}function Nr(e,r){var t=(e=je(e)).includes("j")?function(e,r){F(e.includes("j"),"getDynCaller should only be called with i64 sigs");var t=[];return function(){return t.length=0,Object.assign(t,arguments),Mr(e,r,t)}}(e,r):Ae(r);return"function"!=typeof t&&Ye("unknown function pointer with signature "+e+": "+r),t}var Ir=void 0;function Lr(e){var r=_t(e),t=je(r);return yt(r),t}function xr(e,r){var t=[],n={};throw r.forEach(function e(r){n[r]||We[r]||(ze[r]?ze[r].forEach(e):(t.push(r),n[r]=!0))}),new Ir(e+": "+t.map(Lr).join([", "]))}function Ur(e,r){for(var t=[],n=0;n>2)+n]);return t}function Br(e){for(;e.length;){var r=e.pop();e.pop()(r)}}function jr(e,r){if(!(e instanceof Function))throw new TypeError("new_ called with constructor type "+typeof e+" which is not a function");var t=Ge(e.name||"unknownFunctionName",function(){});t.prototype=e.prototype;var n=new t,o=e.apply(n,r);return o instanceof Object?o:n}function $r(e,r,t,n,o){var i=r.length;i<2&&Ye("argTypes array size mismatch! Must at least get return value and 'this' types!");for(var a=null!==r[1]&&null!==t,s=!1,l=1;l0?", ":"")+d),f+=(u?"var rv = ":"")+"invoker(fn"+(d.length>0?", ":"")+d+");\n",s)f+="runDestructors(destructors);\n";else for(l=a?1:2;l4&&0===--Hr[e].refcount&&(Hr[e]=void 0,zr.push(e))}function Vr(){for(var e=0,r=5;r(e||Ye("Cannot use deleted val. handle = "+e),Hr[e].value),toHandle:e=>{switch(e){case void 0:return 1;case null:return 2;case!0:return 3;case!1:return 4;default:var r=zr.length?zr.pop():Hr.length;return Hr[r]={refcount:1,value:e},r}}};function Xr(e){if(null===e)return"null";var r=typeof e;return"object"===r||"array"===r||"function"===r?e.toString():""+e}function Kr(e,r){switch(r){case 2:return function(e){return this.fromWireType(z[e>>2])};case 3:return function(e){return this.fromWireType(H[e>>3])};default:throw new TypeError("Unknown float type: "+e)}}function Jr(e,r,t){switch(r){case 0:return t?function(e){return x[e]}:function(e){return U[e]};case 1:return t?function(e){return B[e>>1]}:function(e){return j[e>>1]};case 2:return t?function(e){return $[e>>2]}:function(e){return W[e>>2]};default:throw new TypeError("Unknown integer type: "+e)}}function Qr(e,r){var t=We[e];return void 0===t&&Ye(r+" has unknown type "+Lr(e)),t}var Zr={};var et=[];var rt=[];function tt(e){return e<0||0===e&&1/e==-1/0}function nt(e,r){return F(r===(0|r)),(e>>>0)+4294967296*r}function ot(e,r){return(e>>>0)+4294967296*(r>>>0)}function it(e,r){if(e<=0)return e;var t=r<=32?Math.abs(1<=t&&(r<=32||e>t)&&(e=-2*t+e),e}function at(e,r){return e>=0?e:r<=32?2*Math.abs(1<>3]),n+=8):"i64"==e?(r=[$[n>>2],$[n+4>>2]],n+=8):(F(!(3&n)),e="i32",r=$[n>>2],n+=4),r}for(var i,a,s,l=[];;){var u=t;if(0===(i=x[t|0]))break;if(a=x[t+1|0],37==i){var c=!1,d=!1,f=!1,p=!1,m=!1;e:for(;;){switch(a){case 43:c=!0;break;case 45:d=!0;break;case 35:f=!0;break;case 48:if(p)break e;p=!0;break;case 32:m=!0;break;default:break e}t++,a=x[t+1|0]}var h=0;if(42==a)h=o("i32"),t++,a=x[t+1|0];else for(;a>=48&&a<=57;)h=10*h+(a-48),t++,a=x[t+1|0];var g,v=!1,y=-1;if(46==a){if(y=0,v=!0,t++,42==(a=x[t+1|0]))y=o("i32"),t++;else for(;;){var E=x[t+1|0];if(E<48||E>57)break;y=10*y+(E-48),t++}a=x[t+1|0]}switch(y<0&&(y=6,v=!1),String.fromCharCode(a)){case"h":104==x[t+2|0]?(t++,g=1):g=2;break;case"l":108==x[t+2|0]?(t++,g=8):g=4;break;case"L":case"q":case"j":g=8;break;case"z":case"t":case"I":g=4;break;default:g=null}switch(g&&t++,a=x[t+1|0],String.fromCharCode(a)){case"d":case"i":case"u":case"o":case"x":case"X":case"p":var w=100==a||105==a;if(s=o("i"+8*(g=g||4)),8==g&&(s=117==a?ot(s[0],s[1]):nt(s[0],s[1])),g<=4)s=(w?it:at)(s&Math.pow(256,g)-1,8*g);var b=Math.abs(s),_="";if(100==a||105==a)S=it(s,8*g).toString(10);else if(117==a)S=at(s,8*g).toString(10),s=Math.abs(s);else if(111==a)S=(f?"0":"")+b.toString(8);else if(120==a||88==a){if(_=f&&0!=s?"0x":"",s<0){s=-s,S=(b-1).toString(16);for(var T=[],k=0;k=0&&(c?_="+"+_:m&&(_=" "+_)),"-"==S.charAt(0)&&(_="-"+_,S=S.substr(1));_.length+S.lengthA&&A>=-4?(a=(103==a?"f":"F").charCodeAt(0),y-=A+1):(a=(103==a?"e":"E").charCodeAt(0),y--),P=Math.min(y,20)}101==a||69==a?(S=s.toExponential(P),/[eE][-+]\d$/.test(S)&&(S=S.slice(0,-1)+"0"+S.slice(-1))):102!=a&&70!=a||(S=s.toFixed(P),0===s&&tt(s)&&(S="-"+S));var D=S.split("e");if(C&&!f)for(;D[0].length>1&&D[0].includes(".")&&("0"==D[0].slice(-1)||"."==D[0].slice(-1));)D[0]=D[0].slice(0,-1);else for(f&&-1==S.indexOf(".")&&(D[0]+=".");y>P++;)D[0]+="0";S=D[0]+(D.length>1?"e"+D[1]:""),69==a&&(S=S.toUpperCase()),s>=0&&(c?S="+"+S:m&&(S=" "+S))}else S=(s<0?"-":"")+"inf",p=!1;for(;S.length0;)l.push(32);d||l.push(o("i8"));break;case"n":var M=o("i32*");$[M>>2]=l.length;break;case"%":l.push(i);break;default:for(k=u;k=4)){r+=d+"\n";continue}f=g[1],p=g[2],m=g[3],h=0|g[4]}var v=!1;if(8&e){var y=emscripten_source_map.originalPositionFor({line:m,column:h});(v=y&&y.source)&&(64&e&&(y.source=y.source.substring(y.source.replace(/\\/g,"/").lastIndexOf("/")+1)),r+=" at "+f+" ("+y.source+":"+y.line+":"+y.column+")\n")}(16&e||!v)&&(64&e&&(p=p.substring(p.replace(/\\/g,"/").lastIndexOf("/")+1)),r+=(v?" = "+f:" at "+f)+" ("+p+":"+m+":"+h+")\n"),128&e&&i[0]&&(i[1]==f&&i[2].length>0&&(r=r.replace(/\s+$/,""),r+=" with values: "+i[1]+i[2]+"\n"),i=lt(i[0]))}return r=r.replace(/\s+$/,"")}function ct(e){try{return w.grow(e-L.byteLength+65535>>>16),Z(w.buffer),1}catch(r){_("emscripten_realloc_buffer: Attempted to grow heap from "+L.byteLength+" bytes to "+e+" bytes, but got error: "+r)}}var dt={};function ft(){if(!ft.strings){var e={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"==typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:n||"./this.program"};for(var r in dt)void 0===dt[r]?delete e[r]:e[r]=dt[r];var t=[];for(var r in e)t.push(r+"="+e[r]);ft.strings=t}return ft.strings}var pt=function(e,r,t,n){e||(e=this),this.parent=e,this.mount=e.mount,this.mounted=null,this.id=Le.nextInode++,this.name=r,this.mode=t,this.node_ops={},this.stream_ops={},this.rdev=n},mt=365,ht=146;function gt(e,r,t){var n=t>0?t:I(e)+1,o=new Array(n),i=M(e,o,0,o.length);return r&&(o.length=i),o}Object.defineProperties(pt.prototype,{read:{get:function(){return(this.mode&mt)===mt},set:function(e){e?this.mode|=mt:this.mode&=-366}},write:{get:function(){return(this.mode&ht)===ht},set:function(e){e?this.mode|=ht:this.mode&=-147}},isFolder:{get:function(){return Le.isDir(this.mode)}},isDevice:{get:function(){return Le.isChrdev(this.mode)}}}),Le.FSNode=pt,Le.staticInit(),Ie={EPERM:63,ENOENT:44,ESRCH:71,EINTR:27,EIO:29,ENXIO:60,E2BIG:1,ENOEXEC:45,EBADF:8,ECHILD:12,EAGAIN:6,EWOULDBLOCK:6,ENOMEM:48,EACCES:2,EFAULT:21,ENOTBLK:105,EBUSY:10,EEXIST:20,EXDEV:75,ENODEV:43,ENOTDIR:54,EISDIR:31,EINVAL:28,ENFILE:41,EMFILE:33,ENOTTY:59,ETXTBSY:74,EFBIG:22,ENOSPC:51,ESPIPE:70,EROFS:69,EMLINK:34,EPIPE:64,EDOM:18,ERANGE:68,ENOMSG:49,EIDRM:24,ECHRNG:106,EL2NSYNC:156,EL3HLT:107,EL3RST:108,ELNRNG:109,EUNATCH:110,ENOCSI:111,EL2HLT:112,EDEADLK:16,ENOLCK:46,EBADE:113,EBADR:114,EXFULL:115,ENOANO:104,EBADRQC:103,EBADSLT:102,EDEADLOCK:16,EBFONT:101,ENOSTR:100,ENODATA:116,ETIME:117,ENOSR:118,ENONET:119,ENOPKG:120,EREMOTE:121,ENOLINK:47,EADV:122,ESRMNT:123,ECOMM:124,EPROTO:65,EMULTIHOP:36,EDOTDOT:125,EBADMSG:9,ENOTUNIQ:126,EBADFD:127,EREMCHG:128,ELIBACC:129,ELIBBAD:130,ELIBSCN:131,ELIBMAX:132,ELIBEXEC:133,ENOSYS:52,ENOTEMPTY:55,ENAMETOOLONG:37,ELOOP:32,EOPNOTSUPP:138,EPFNOSUPPORT:139,ECONNRESET:15,ENOBUFS:42,EAFNOSUPPORT:5,EPROTOTYPE:67,ENOTSOCK:57,ENOPROTOOPT:50,ESHUTDOWN:140,ECONNREFUSED:14,EADDRINUSE:3,ECONNABORTED:13,ENETUNREACH:40,ENETDOWN:38,ETIMEDOUT:73,EHOSTDOWN:142,EHOSTUNREACH:23,EINPROGRESS:26,EALREADY:7,EDESTADDRREQ:17,EMSGSIZE:35,EPROTONOSUPPORT:66,ESOCKTNOSUPPORT:137,EADDRNOTAVAIL:4,ENETRESET:39,EISCONN:30,ENOTCONN:53,ETOOMANYREFS:141,EUSERS:136,EDQUOT:19,ESTALE:72,ENOTSUP:138,ENOMEDIUM:148,EILSEQ:25,EOVERFLOW:61,ECANCELED:11,ENOTRECOVERABLE:56,EOWNERDEAD:62,ESTRPIPE:135},function(){for(var e=new Array(256),r=0;r<256;++r)e[r]=String.fromCharCode(r);Be=e}(),qe=r.BindingError=Ve(Error,"BindingError"),Xe=r.InternalError=Ve(Error,"InternalError"),br.prototype.isAliasOf=Ze,br.prototype.clone=vr,br.prototype.delete=yr,br.prototype.isDeleted=Er,br.prototype.deleteLater=wr,r.getInheritedInstanceCount=ar,r.getLiveInheritedInstances=sr,r.flushPendingDeletes=ur,r.setDelayFunction=dr,Rr.prototype.getPointee=Fr,Rr.prototype.destructor=Dr,Rr.prototype.argPackAdvance=8,Rr.prototype.readValueFromPointer=Ar,Rr.prototype.deleteObject=Or,Rr.prototype.fromWireType=hr,Ir=r.UnboundTypeError=Ve(Error,"UnboundTypeError"),r.count_emval_handles=Vr,r.get_first_emval=qr;var vt={__syscall_fcntl64:function(e,r,t){xe.varargs=t;try{var n=xe.getStreamFromFD(e);switch(r){case 0:return(o=xe.get())<0?-28:Le.createStream(n,o).fd;case 1:case 2:case 6:case 7:return 0;case 3:return n.flags;case 4:var o=xe.get();return n.flags|=o,0;case 5:o=xe.get();return B[o+0>>1]=2,0;case 16:case 8:default:return-28;case 9:return i=28,$[bt()>>2]=i,-1}}catch(e){if(void 0===Le||!(e instanceof Le.ErrnoError))throw e;return-e.errno}var i},__syscall_openat:function(e,r,t,n){xe.varargs=n;try{r=xe.getStr(r),r=xe.calculateAt(e,r);var o=n?xe.get():0;return Le.open(r,t,o).fd}catch(e){if(void 0===Le||!(e instanceof Le.ErrnoError))throw e;return-e.errno}},_embind_register_bigint:function(e,r,t,n,o){},_embind_register_bool:function(e,r,t,n,o){var i=Ue(t);Qe(e,{name:r=je(r),fromWireType:function(e){return!!e},toWireType:function(e,r){return r?n:o},argPackAdvance:8,readValueFromPointer:function(e){var n;if(1===t)n=x;else if(2===t)n=B;else{if(4!==t)throw new TypeError("Unknown boolean type size: "+r);n=$}return this.fromWireType(n[e>>i])},destructorFunction:null})},_embind_register_class:function(e,t,n,o,i,a,s,l,u,c,d,f,p){d=je(d),a=Nr(i,a),l&&(l=Nr(s,l)),c&&(c=Nr(u,c)),p=Nr(f,p);var m=He(d);!function(e,t,n){r.hasOwnProperty(e)?((void 0===n||void 0!==r[e].overloadTable&&void 0!==r[e].overloadTable[n])&&Ye("Cannot register public name '"+e+"' twice"),_r(r,e,e),r.hasOwnProperty(n)&&Ye("Cannot register multiple overloads of a function with the same number of arguments ("+n+")!"),r[e].overloadTable[n]=t):(r[e]=t,void 0!==n&&(r[e].numArguments=n))}(m,function(){xr("Cannot construct "+d+" due to unbound types",[o])}),Je([e,t,n],o?[o]:[],function(t){var n,i;t=t[0],i=o?(n=t.registeredClass).instancePrototype:br.prototype;var s=Ge(m,function(){if(Object.getPrototypeOf(this)!==u)throw new qe("Use 'new' to construct "+d);if(void 0===f.constructor_body)throw new qe(d+" has no accessible constructor");var e=f.constructor_body[arguments.length];if(void 0===e)throw new qe("Tried to invoke ctor of "+d+" with invalid number of parameters ("+arguments.length+") - expected ("+Object.keys(f.constructor_body).toString()+") parameters instead!");return e.apply(this,arguments)}),u=Object.create(i,{constructor:{value:s}});s.prototype=u;var f=new Tr(d,s,u,p,n,a,l,c),h=new Rr(d,f,!0,!1,!1),g=new Rr(d+"*",f,!1,!1,!1),v=new Rr(d+" const*",f,!1,!0,!1);return ir[e]={pointerType:g,constPointerType:v},function(e,t,n){r.hasOwnProperty(e)||Ke("Replacing nonexistant public symbol"),void 0!==r[e].overloadTable&&void 0!==n?r[e].overloadTable[n]=t:(r[e]=t,r[e].argCount=n)}(m,s),[h,g,v]})},_embind_register_class_constructor:function(e,r,t,n,o,i){F(r>0);var a=Ur(r,t);o=Nr(n,o),Je([],[e],function(e){var t="constructor "+(e=e[0]).name;if(void 0===e.registeredClass.constructor_body&&(e.registeredClass.constructor_body=[]),void 0!==e.registeredClass.constructor_body[r-1])throw new qe("Cannot register multiple constructors with identical number of parameters ("+(r-1)+") for class '"+e.name+"'! Overload resolution is currently only performed using the parameter count, not actual type info!");return e.registeredClass.constructor_body[r-1]=()=>{xr("Cannot construct "+e.name+" due to unbound types",a)},Je([],a,function(n){return n.splice(1,0,null),e.registeredClass.constructor_body[r-1]=$r(t,n,null,o,i),[]}),[]})},_embind_register_class_function:function(e,r,t,n,o,i,a,s){var l=Ur(t,n);r=je(r),i=Nr(o,i),Je([],[e],function(e){var n=(e=e[0]).name+"."+r;function o(){xr("Cannot call "+n+" due to unbound types",l)}r.startsWith("@@")&&(r=Symbol[r.substring(2)]),s&&e.registeredClass.pureVirtualFunctions.push(r);var u=e.registeredClass.instancePrototype,c=u[r];return void 0===c||void 0===c.overloadTable&&c.className!==e.name&&c.argCount===t-2?(o.argCount=t-2,o.className=e.name,u[r]=o):(_r(u,r,n),u[r].overloadTable[t-2]=o),Je([],l,function(o){var s=$r(n,o,e,i,a);return void 0===u[r].overloadTable?(s.argCount=t-2,u[r]=s):u[r].overloadTable[t-2]=s,[]}),[]})},_embind_register_class_property:function(e,r,t,n,o,i,a,s,l,u){r=je(r),o=Nr(n,o),Je([],[e],function(e){var n=(e=e[0]).name+"."+r,c={get:function(){xr("Cannot access "+n+" due to unbound types",[t,a])},enumerable:!0,configurable:!0};return c.set=l?()=>{xr("Cannot access "+n+" due to unbound types",[t,a])}:e=>{Ye(n+" is a read-only property")},Object.defineProperty(e.registeredClass.instancePrototype,r,c),Je([],l?[t,a]:[t],function(t){var a=t[0],c={get:function(){var r=Wr(this,e,n+" getter");return a.fromWireType(o(i,r))},enumerable:!0};if(l){l=Nr(s,l);var d=t[1];c.set=function(r){var t=Wr(this,e,n+" setter"),o=[];l(u,t,d.toWireType(o,r)),Br(o)}}return Object.defineProperty(e.registeredClass.instancePrototype,r,c),[]}),[]})},_embind_register_emval:function(e,r){Qe(e,{name:r=je(r),fromWireType:function(e){var r=Yr.toValue(e);return Gr(e),r},toWireType:function(e,r){return Yr.toHandle(r)},argPackAdvance:8,readValueFromPointer:Ar,destructorFunction:null})},_embind_register_float:function(e,r,t){var n=Ue(t);Qe(e,{name:r=je(r),fromWireType:function(e){return e},toWireType:function(e,r){if("number"!=typeof r&&"boolean"!=typeof r)throw new TypeError('Cannot convert "'+Xr(r)+'" to '+this.name);return r},argPackAdvance:8,readValueFromPointer:Kr(r,n),destructorFunction:null})},_embind_register_integer:function(e,r,t,n,o){r=je(r),-1===o&&(o=4294967295);var i=Ue(t),a=e=>e;if(0===n){var s=32-8*t;a=e=>e<>>s}var l=r.includes("unsigned"),u=(e,t)=>{if("number"!=typeof e&&"boolean"!=typeof e)throw new TypeError('Cannot convert "'+Xr(e)+'" to '+t);if(eo)throw new TypeError('Passing a number "'+Xr(e)+'" from JS side to C/C++ side to an argument of type "'+r+'", which is outside the valid range ['+n+", "+o+"]!")};Qe(e,{name:r,fromWireType:a,toWireType:l?function(e,r){return u(r,this.name),r>>>0}:function(e,r){return u(r,this.name),r},argPackAdvance:8,readValueFromPointer:Jr(r,i,0!==n),destructorFunction:null})},_embind_register_memory_view:function(e,r,t){var n=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array][r];function o(e){var r=W,t=r[e>>=2],o=r[e+1];return new n(L,o,t)}Qe(e,{name:t=je(t),fromWireType:o,argPackAdvance:8,readValueFromPointer:o},{ignoreDuplicateRegistrations:!0})},_embind_register_std_string:function(e,r){var t="std::string"===(r=je(r));Qe(e,{name:r,fromWireType:function(e){var r,n=W[e>>2];if(t)for(var o=e+4,i=0;i<=n;++i){var a=e+4+i;if(i==n||0==U[a]){var s=R(o,a-o);void 0===r?r=s:(r+=String.fromCharCode(0),r+=s),o=a+1}}else{var l=new Array(n);for(i=0;iI(r):()=>r.length)(),i=Et(4+o+1);if(W[i>>2]=o,t&&n)N(r,i+4,o+1);else if(n)for(var a=0;a255&&(yt(i),Ye("String has UTF-16 code units that do not fit in 8 bits")),U[i+4+a]=s}else for(a=0;aj,s=1):4===r&&(n=X,o=K,a=J,i=()=>W,s=2),Qe(e,{name:t,fromWireType:function(e){for(var t,o=W[e>>2],a=i(),l=e+4,u=0;u<=o;++u){var c=e+4+u*r;if(u==o||0==a[c>>s]){var d=n(l,c-l);void 0===t?t=d:(t+=String.fromCharCode(0),t+=d),l=c+r}}return yt(e),t},toWireType:function(e,n){"string"!=typeof n&&Ye("Cannot pass non-string to C++ string type "+t);var i=a(n),l=Et(4+i+r);return W[l>>2]=i>>s,o(n,l+4,i+r),null!==e&&e.push(yt,l),l},argPackAdvance:8,readValueFromPointer:Ar,destructorFunction:function(e){yt(e)}})},_embind_register_void:function(e,r){Qe(e,{isVoid:!0,name:r=je(r),argPackAdvance:0,fromWireType:function(){},toWireType:function(e,r){}})},_emscripten_date_now:function(){return Date.now()},_emval_as:function(e,r,t){e=Yr.toValue(e),r=Qr(r,"emval::as");var n=[],o=Yr.toHandle(n);return $[t>>2]=o,r.toWireType(n,e)},_emval_call_void_method:function(e,r,t,n){var o,i;(e=et[e])(r=Yr.toValue(r),t=void 0===(i=Zr[o=t])?je(o):i,null,n)},_emval_decref:Gr,_emval_get_method_caller:function(e,r){var t=function(e,r){for(var t=new Array(e),n=0;n>2)+n],"parameter "+n);return t}(e,r),n=t[0],o=n.name+"_$"+t.slice(1).map(function(e){return e.name}).join("_")+"$",i=rt[o];if(void 0!==i)return i;for(var a=["retType"],s=[n],l="",u=0;u4&&(Hr[e].refcount+=1)},_emval_run_destructors:function(e){Br(Yr.toValue(e)),Gr(e)},_emval_take_value:function(e,r){var t=(e=Qr(e,"_emval_take_value")).readValueFromPointer(r);return Yr.toHandle(t)},_gmtime_js:function(e,r){var t=new Date(1e3*$[e>>2]);$[r>>2]=t.getUTCSeconds(),$[r+4>>2]=t.getUTCMinutes(),$[r+8>>2]=t.getUTCHours(),$[r+12>>2]=t.getUTCDate(),$[r+16>>2]=t.getUTCMonth(),$[r+20>>2]=t.getUTCFullYear()-1900,$[r+24>>2]=t.getUTCDay();var n=Date.UTC(t.getUTCFullYear(),0,1,0,0,0,0),o=(t.getTime()-n)/864e5|0;$[r+28>>2]=o},_localtime_js:function(e,r){var t=new Date(1e3*$[e>>2]);$[r>>2]=t.getSeconds(),$[r+4>>2]=t.getMinutes(),$[r+8>>2]=t.getHours(),$[r+12>>2]=t.getDate(),$[r+16>>2]=t.getMonth(),$[r+20>>2]=t.getFullYear()-1900,$[r+24>>2]=t.getDay();var n=new Date(t.getFullYear(),0,1),o=(t.getTime()-n.getTime())/864e5|0;$[r+28>>2]=o,$[r+36>>2]=-60*t.getTimezoneOffset();var i=new Date(t.getFullYear(),6,1).getTimezoneOffset(),a=n.getTimezoneOffset(),s=0|(i!=a&&t.getTimezoneOffset()==Math.min(a,i));$[r+32>>2]=s},_mktime_js:function(e){var r=new Date($[e+20>>2]+1900,$[e+16>>2],$[e+12>>2],$[e+8>>2],$[e+4>>2],$[e>>2],0),t=$[e+32>>2],n=r.getTimezoneOffset(),o=new Date(r.getFullYear(),0,1),i=new Date(r.getFullYear(),6,1).getTimezoneOffset(),a=o.getTimezoneOffset(),s=Math.min(a,i);if(t<0)$[e+32>>2]=Number(i!=a&&s==n);else if(t>0!=(s==n)){var l=Math.max(a,i),u=t>0?s:l;r.setTime(r.getTime()+6e4*(u-n))}$[e+24>>2]=r.getDay();var c=(r.getTime()-o.getTime())/864e5|0;return $[e+28>>2]=c,$[e>>2]=r.getSeconds(),$[e+4>>2]=r.getMinutes(),$[e+8>>2]=r.getHours(),$[e+12>>2]=r.getDate(),$[e+16>>2]=r.getMonth(),r.getTime()/1e3|0},_tzset_js:function e(r,t,n){e.called||(e.called=!0,function(e,r,t){var n=(new Date).getFullYear(),o=new Date(n,0,1),i=new Date(n,6,1),a=o.getTimezoneOffset(),s=i.getTimezoneOffset(),l=Math.max(a,s);function u(e){var r=e.toTimeString().match(/\(([A-Za-z ]+)\)$/);return r?r[1]:"GMT"}$[e>>2]=60*l,$[r>>2]=Number(a!=s);var c=u(o),d=u(i),f=Q(c),p=Q(d);s>2]=f,$[t+4>>2]=p):($[t>>2]=p,$[t+4>>2]=f)}(r,t,n))},abort:function(){ye("native code called abort()")},emscripten_log:function(e,r,t){!function(e,r){24&e&&(r=r.replace(/\s+$/,""),r+=(r.length>0?"\n":"")+ut(e)),1&e?4&e?console.error(r):2&e?console.warn(r):512&e?console.info(r):256&e?console.debug(r):console.log(r):6&e?_(r):b(r)}(e,O(st(r,t),0))},emscripten_resize_heap:function(e){var r=U.length;F((e>>>=0)>r);var t=2147483648;if(e>t)return _("Cannot enlarge memory, asked to go up to "+e+" bytes, but the limit is "+t+" bytes!"),!1;let n=(e,r)=>e+(r-e%r)%r;for(var o=1;o<=4;o*=2){var i=r*(1+.2/o);i=Math.min(i,e+100663296);var a=Math.min(t,n(Math.max(e,i),65536));if(ct(a))return!0}return _("Failed to grow the heap from "+r+" bytes to "+a+" bytes, not enough memory!"),!1},environ_get:function(e,r){var t=0;return ft().forEach(function(n,o){var i=r+t;$[e+4*o>>2]=i,function(e,r,t){for(var n=0;n>2]=t.length;var n=0;return t.forEach(function(e){n+=e.length+1}),$[r>>2]=n,0},fd_close:function(e){try{var r=xe.getStreamFromFD(e);return Le.close(r),0}catch(e){if(void 0===Le||!(e instanceof Le.ErrnoError))throw e;return e.errno}},fd_fdstat_get:function(e,r){try{var t=xe.getStreamFromFD(e),n=t.tty?2:Le.isDir(t.mode)?3:Le.isLink(t.mode)?7:4;return x[r|0]=n,0}catch(e){if(void 0===Le||!(e instanceof Le.ErrnoError))throw e;return e.errno}},fd_read:function(e,r,t,n){try{var o=xe.getStreamFromFD(e),i=xe.doReadv(o,r,t);return $[n>>2]=i,0}catch(e){if(void 0===Le||!(e instanceof Le.ErrnoError))throw e;return e.errno}},fd_seek:function(e,r,t,n,o){try{var i=xe.getStreamFromFD(e),a=4294967296*t+(r>>>0),s=9007199254740992;return a<=-s||a>=s?-61:(Le.llseek(i,a,n),be=[i.position>>>0,(we=i.position,+Math.abs(we)>=1?we>0?(0|Math.min(+Math.floor(we/4294967296),4294967295))>>>0:~~+Math.ceil((we-+(~~we>>>0))/4294967296)>>>0:0)],$[o>>2]=be[0],$[o+4>>2]=be[1],i.getdents&&0===a&&0===n&&(i.getdents=null),0)}catch(e){if(void 0===Le||!(e instanceof Le.ErrnoError))throw e;return e.errno}},fd_write:function(e,r,t,n){try{var o=xe.getStreamFromFD(e),i=xe.doWritev(o,r,t);return $[n>>2]=i,0}catch(e){if(void 0===Le||!(e instanceof Le.ErrnoError))throw e;return e.errno}},setTempRet0:function(e){}};!function(){var e={env:vt,wasi_snapshot_preview1:vt};function t(e,t){var n,o=e.exports;r.asm=o,F(w=r.asm.memory,"memory not found in wasm exports"),Z(w.buffer),F(re=r.asm.__indirect_function_table,"table not found in wasm exports"),n=r.asm.__wasm_call_ctors,ae.unshift(n),ve("wasm-instantiate")}ge("wasm-instantiate");var n=r;function o(e){F(r===n,"the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?"),n=null,t(e.instance)}function i(r){return function(){if(!E&&(s||l)){if("function"==typeof fetch&&!Te(Ee))return fetch(Ee,{credentials:"same-origin"}).then(function(e){if(!e.ok)throw"failed to load wasm binary file at '"+Ee+"'";return e.arrayBuffer()}).catch(function(){return Se(Ee)});if(f)return new Promise(function(e,r){f(Ee,function(r){e(new Uint8Array(r))},r)})}return Promise.resolve().then(function(){return Se(Ee)})}().then(function(r){return WebAssembly.instantiate(r,e)}).then(function(e){return e}).then(r,function(e){_("failed to asynchronously prepare wasm: "+e),Te(Ee)&&_("warning: Loading from a file URI ("+Ee+") is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing"),ye(e)})}if(r.instantiateWasm)try{return r.instantiateWasm(e,t)}catch(e){return _("Module.instantiateWasm callback failed with error: "+e),!1}E||"function"!=typeof WebAssembly.instantiateStreaming||_e(Ee)||Te(Ee)||"function"!=typeof fetch?i(o):fetch(Ee,{credentials:"same-origin"}).then(function(r){return WebAssembly.instantiateStreaming(r,e).then(o,function(e){return _("wasm streaming compile failed: "+e),_("falling back to ArrayBuffer instantiation"),i(o)})})}(),r.___wasm_call_ctors=ke("__wasm_call_ctors");var yt=r._free=ke("free"),Et=r._malloc=ke("malloc"),wt=r._strlen=ke("strlen"),bt=r.___errno_location=ke("__errno_location"),_t=r.___getTypeName=ke("__getTypeName");r.___embind_register_native_and_builtin_types=ke("__embind_register_native_and_builtin_types");var Tt=r.___stdio_exit=ke("__stdio_exit"),kt=r._emscripten_builtin_memalign=ke("emscripten_builtin_memalign"),St=r._emscripten_stack_init=function(){return(St=r._emscripten_stack_init=r.asm.emscripten_stack_init).apply(null,arguments)};r._emscripten_stack_get_free=function(){return(r._emscripten_stack_get_free=r.asm.emscripten_stack_get_free).apply(null,arguments)},r._emscripten_stack_get_base=function(){return(r._emscripten_stack_get_base=r.asm.emscripten_stack_get_base).apply(null,arguments)};var Ct,Pt=r._emscripten_stack_get_end=function(){return(Pt=r._emscripten_stack_get_end=r.asm.emscripten_stack_get_end).apply(null,arguments)};function At(e){this.name="ExitStatus",this.message="Program terminated with exit("+e+")",this.status=e}function Ft(e){function t(){Ct||(Ct=!0,r.calledRun=!0,A||(oe(),F(!le),le=!0,r.noFSInit||Le.init.initialized||Le.init(),Le.ignorePermissions=!1,Ce(ae),r.onRuntimeInitialized&&r.onRuntimeInitialized(),F(!r._main,'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]'),function(){if(oe(),r.postRun)for("function"==typeof r.postRun&&(r.postRun=[r.postRun]);r.postRun.length;)ce(r.postRun.shift());Ce(se)}()))}de>0||(St(),ne(),function(){if(r.preRun)for("function"==typeof r.preRun&&(r.preRun=[r.preRun]);r.preRun.length;)ue(r.preRun.shift());Ce(ie)}(),de>0||(r.setStatus?(r.setStatus("Running..."),setTimeout(function(){setTimeout(function(){r.setStatus("")},1),t()},1)):t(),oe()))}if(r.stackSave=ke("stackSave"),r.stackRestore=ke("stackRestore"),r.stackAlloc=ke("stackAlloc"),r.dynCall_ijiii=ke("dynCall_ijiii"),r.dynCall_viiijj=ke("dynCall_viiijj"),r.dynCall_jij=ke("dynCall_jij"),r.dynCall_jii=ke("dynCall_jii"),r.dynCall_jiji=ke("dynCall_jiji"),r._ff_h264_cabac_tables=112940,P("intArrayFromString",!1),P("intArrayToString",!1),P("ccall",!1),P("cwrap",!1),P("setValue",!1),P("getValue",!1),P("allocate",!1),P("UTF8ArrayToString",!1),P("UTF8ToString",!1),P("stringToUTF8Array",!1),P("stringToUTF8",!1),P("lengthBytesUTF8",!1),P("stackTrace",!1),P("addOnPreRun",!1),P("addOnInit",!1),P("addOnPreMain",!1),P("addOnExit",!1),P("addOnPostRun",!1),P("writeStringToMemory",!1),P("writeArrayToMemory",!1),P("writeAsciiToMemory",!1),P("addRunDependency",!0),P("removeRunDependency",!0),P("FS_createFolder",!1),P("FS_createPath",!0),P("FS_createDataFile",!0),P("FS_createPreloadedFile",!0),P("FS_createLazyFile",!0),P("FS_createLink",!1),P("FS_createDevice",!0),P("FS_unlink",!0),P("getLEB",!1),P("getFunctionTables",!1),P("alignFunctionTables",!1),P("registerFunctions",!1),P("addFunction",!1),P("removeFunction",!1),P("prettyPrint",!1),P("dynCall",!1),P("getCompilerSetting",!1),P("print",!1),P("printErr",!1),P("getTempRet0",!1),P("setTempRet0",!1),P("callMain",!1),P("abort",!1),P("keepRuntimeAlive",!1),P("ptrToString",!1),P("zeroMemory",!1),P("stringToNewUTF8",!1),P("emscripten_realloc_buffer",!1),P("ENV",!1),P("ERRNO_CODES",!1),P("ERRNO_MESSAGES",!1),P("setErrNo",!1),P("inetPton4",!1),P("inetNtop4",!1),P("inetPton6",!1),P("inetNtop6",!1),P("readSockaddr",!1),P("writeSockaddr",!1),P("DNS",!1),P("getHostByName",!1),P("Protocols",!1),P("Sockets",!1),P("getRandomDevice",!1),P("traverseStack",!1),P("UNWIND_CACHE",!1),P("convertPCtoSourceLocation",!1),P("readAsmConstArgsArray",!1),P("readAsmConstArgs",!1),P("mainThreadEM_ASM",!1),P("jstoi_q",!1),P("jstoi_s",!1),P("getExecutableName",!1),P("listenOnce",!1),P("autoResumeAudioContext",!1),P("dynCallLegacy",!1),P("getDynCaller",!1),P("dynCall",!1),P("setWasmTableEntry",!1),P("getWasmTableEntry",!1),P("handleException",!1),P("runtimeKeepalivePush",!1),P("runtimeKeepalivePop",!1),P("callUserCallback",!1),P("maybeExit",!1),P("safeSetTimeout",!1),P("asmjsMangle",!1),P("asyncLoad",!1),P("alignMemory",!1),P("mmapAlloc",!1),P("reallyNegative",!1),P("unSign",!1),P("reSign",!1),P("formatString",!1),P("PATH",!1),P("PATH_FS",!1),P("SYSCALLS",!1),P("getSocketFromFD",!1),P("getSocketAddress",!1),P("JSEvents",!1),P("registerKeyEventCallback",!1),P("specialHTMLTargets",!1),P("maybeCStringToJsString",!1),P("findEventTarget",!1),P("findCanvasEventTarget",!1),P("getBoundingClientRect",!1),P("fillMouseEventData",!1),P("registerMouseEventCallback",!1),P("registerWheelEventCallback",!1),P("registerUiEventCallback",!1),P("registerFocusEventCallback",!1),P("fillDeviceOrientationEventData",!1),P("registerDeviceOrientationEventCallback",!1),P("fillDeviceMotionEventData",!1),P("registerDeviceMotionEventCallback",!1),P("screenOrientation",!1),P("fillOrientationChangeEventData",!1),P("registerOrientationChangeEventCallback",!1),P("fillFullscreenChangeEventData",!1),P("registerFullscreenChangeEventCallback",!1),P("registerRestoreOldStyle",!1),P("hideEverythingExceptGivenElement",!1),P("restoreHiddenElements",!1),P("setLetterbox",!1),P("currentFullscreenStrategy",!1),P("restoreOldWindowedStyle",!1),P("softFullscreenResizeWebGLRenderTarget",!1),P("doRequestFullscreen",!1),P("fillPointerlockChangeEventData",!1),P("registerPointerlockChangeEventCallback",!1),P("registerPointerlockErrorEventCallback",!1),P("requestPointerLock",!1),P("fillVisibilityChangeEventData",!1),P("registerVisibilityChangeEventCallback",!1),P("registerTouchEventCallback",!1),P("fillGamepadEventData",!1),P("registerGamepadEventCallback",!1),P("registerBeforeUnloadEventCallback",!1),P("fillBatteryEventData",!1),P("battery",!1),P("registerBatteryEventCallback",!1),P("setCanvasElementSize",!1),P("getCanvasElementSize",!1),P("demangle",!1),P("demangleAll",!1),P("jsStackTrace",!1),P("stackTrace",!1),P("getEnvStrings",!1),P("checkWasiClock",!1),P("writeI53ToI64",!1),P("writeI53ToI64Clamped",!1),P("writeI53ToI64Signaling",!1),P("writeI53ToU64Clamped",!1),P("writeI53ToU64Signaling",!1),P("readI53FromI64",!1),P("readI53FromU64",!1),P("convertI32PairToI53",!1),P("convertU32PairToI53",!1),P("dlopenMissingError",!1),P("setImmediateWrapped",!1),P("clearImmediateWrapped",!1),P("polyfillSetImmediate",!1),P("uncaughtExceptionCount",!1),P("exceptionLast",!1),P("exceptionCaught",!1),P("ExceptionInfo",!1),P("exception_addRef",!1),P("exception_decRef",!1),P("Browser",!1),P("setMainLoop",!1),P("wget",!1),P("FS",!1),P("MEMFS",!1),P("TTY",!1),P("PIPEFS",!1),P("SOCKFS",!1),P("_setNetworkCallback",!1),P("tempFixedLengthArray",!1),P("miniTempWebGLFloatBuffers",!1),P("heapObjectForWebGLType",!1),P("heapAccessShiftForWebGLHeap",!1),P("GL",!1),P("emscriptenWebGLGet",!1),P("computeUnpackAlignedImageSize",!1),P("emscriptenWebGLGetTexPixelData",!1),P("emscriptenWebGLGetUniform",!1),P("webglGetUniformLocation",!1),P("webglPrepareUniformLocationsBeforeFirstUse",!1),P("webglGetLeftBracePos",!1),P("emscriptenWebGLGetVertexAttrib",!1),P("writeGLArray",!1),P("AL",!1),P("SDL_unicode",!1),P("SDL_ttfContext",!1),P("SDL_audio",!1),P("SDL",!1),P("SDL_gfx",!1),P("GLUT",!1),P("EGL",!1),P("GLFW_Window",!1),P("GLFW",!1),P("GLEW",!1),P("IDBStore",!1),P("runAndAbortIfError",!1),P("InternalError",!1),P("BindingError",!1),P("UnboundTypeError",!1),P("PureVirtualError",!1),P("init_embind",!1),P("throwInternalError",!1),P("throwBindingError",!1),P("throwUnboundTypeError",!1),P("ensureOverloadTable",!1),P("exposePublicSymbol",!1),P("replacePublicSymbol",!1),P("extendError",!1),P("createNamedFunction",!1),P("registeredInstances",!1),P("getBasestPointer",!1),P("registerInheritedInstance",!1),P("unregisterInheritedInstance",!1),P("getInheritedInstance",!1),P("getInheritedInstanceCount",!1),P("getLiveInheritedInstances",!1),P("registeredTypes",!1),P("awaitingDependencies",!1),P("typeDependencies",!1),P("registeredPointers",!1),P("registerType",!1),P("whenDependentTypesAreResolved",!1),P("embind_charCodes",!1),P("embind_init_charCodes",!1),P("readLatin1String",!1),P("getTypeName",!1),P("heap32VectorToArray",!1),P("requireRegisteredType",!1),P("getShiftFromSize",!1),P("integerReadValueFromPointer",!1),P("enumReadValueFromPointer",!1),P("floatReadValueFromPointer",!1),P("simpleReadValueFromPointer",!1),P("runDestructors",!1),P("new_",!1),P("craftInvokerFunction",!1),P("embind__requireFunction",!1),P("tupleRegistrations",!1),P("structRegistrations",!1),P("genericPointerToWireType",!1),P("constNoSmartPtrRawPointerToWireType",!1),P("nonConstNoSmartPtrRawPointerToWireType",!1),P("init_RegisteredPointer",!1),P("RegisteredPointer",!1),P("RegisteredPointer_getPointee",!1),P("RegisteredPointer_destructor",!1),P("RegisteredPointer_deleteObject",!1),P("RegisteredPointer_fromWireType",!1),P("runDestructor",!1),P("releaseClassHandle",!1),P("finalizationRegistry",!1),P("detachFinalizer_deps",!1),P("detachFinalizer",!1),P("attachFinalizer",!1),P("makeClassHandle",!1),P("init_ClassHandle",!1),P("ClassHandle",!1),P("ClassHandle_isAliasOf",!1),P("throwInstanceAlreadyDeleted",!1),P("ClassHandle_clone",!1),P("ClassHandle_delete",!1),P("deletionQueue",!1),P("ClassHandle_isDeleted",!1),P("ClassHandle_deleteLater",!1),P("flushPendingDeletes",!1),P("delayFunction",!1),P("setDelayFunction",!1),P("RegisteredClass",!1),P("shallowCopyInternalPointer",!1),P("downcastPointer",!1),P("upcastPointer",!1),P("validateThis",!1),P("char_0",!1),P("char_9",!1),P("makeLegalFunctionName",!1),P("emval_handle_array",!1),P("emval_free_list",!1),P("emval_symbols",!1),P("init_emval",!1),P("count_emval_handles",!1),P("get_first_emval",!1),P("getStringOrSymbol",!1),P("Emval",!1),P("emval_newers",!1),P("craftEmvalAllocator",!1),P("emval_get_global",!1),P("emval_methodCallers",!1),P("emval_registeredMethods",!1),P("warnOnce",!1),P("stackSave",!1),P("stackRestore",!1),P("stackAlloc",!1),P("AsciiToString",!1),P("stringToAscii",!1),P("UTF16ToString",!1),P("stringToUTF16",!1),P("lengthBytesUTF16",!1),P("UTF32ToString",!1),P("stringToUTF32",!1),P("lengthBytesUTF32",!1),P("allocateUTF8",!1),P("allocateUTF8OnStack",!1),r.writeStackCookie=ne,r.checkStackCookie=oe,C("ALLOC_NORMAL",!1),C("ALLOC_STACK",!1),pe=function e(){Ct||Ft(),Ct||(pe=e)},r.run=Ft,r.preInit)for("function"==typeof r.preInit&&(r.preInit=[r.preInit]);r.preInit.length>0;)r.preInit.pop()();Ft(),e.exports=r});const u=1e3,c=1e3,d=!1,f=!1,p=!1,m=!1,h="initVideo",g="render",v="playAudio",y="initAudio",E="audioCode",w="videoCode",b=1,_=2,T="init",k="decode",S="audioDecode",C="videoDecode",P="close",A="updateConfig",F="key",D="delta";s(function(e){!function(){var r="undefined"!=typeof window&&void 0!==window.document?window.document:{},t=e.exports,n=function(){for(var e,t=[["requestFullscreen","exitFullscreen","fullscreenElement","fullscreenEnabled","fullscreenchange","fullscreenerror"],["webkitRequestFullscreen","webkitExitFullscreen","webkitFullscreenElement","webkitFullscreenEnabled","webkitfullscreenchange","webkitfullscreenerror"],["webkitRequestFullScreen","webkitCancelFullScreen","webkitCurrentFullScreenElement","webkitCancelFullScreen","webkitfullscreenchange","webkitfullscreenerror"],["mozRequestFullScreen","mozCancelFullScreen","mozFullScreenElement","mozFullScreenEnabled","mozfullscreenchange","mozfullscreenerror"],["msRequestFullscreen","msExitFullscreen","msFullscreenElement","msFullscreenEnabled","MSFullscreenChange","MSFullscreenError"]],n=0,o=t.length,i={};n{try{if("object"==typeof WebAssembly&&"function"==typeof WebAssembly.instantiate){const e=new WebAssembly.Module(Uint8Array.of(0,97,115,109,1,0,0,0));if(e instanceof WebAssembly.Module)return new WebAssembly.Instance(e)instanceof WebAssembly.Instance}}catch(e){}})(),Date.now||(Date.now=function(){return(new Date).getTime()}),l.postRun=function(){var e=[],r=[],t={};"VideoEncoder"in self&&(t={hasInit:!1,isEmitInfo:!1,offscreenCanvas:null,offscreenCanvasCtx:null,decoder:new VideoDecoder({output:function(e){if(n.isDestroyed)return;t.isEmitInfo||(n.opt.debug&&console.log("Jb: [worker] Webcodecs Video Decoder initSize"),postMessage({cmd:h,w:e.codedWidth,h:e.codedHeight}),t.isEmitInfo=!0,t.offscreenCanvas=new OffscreenCanvas(e.codedWidth,e.codedHeight),t.offscreenCanvasCtx=t.offscreenCanvas.getContext("2d")),t.offscreenCanvasCtx.drawImage(e,0,0,e.codedWidth,e.codedHeight);let r=t.offscreenCanvas.transferToImageBitmap();postMessage({cmd:g,buffer:r,delay:n.delay,ts:0},[r]),setTimeout(function(){e.close?e.close():e.destroy()},100)},error:function(e){console.error(e)}}),decode:function(e,r){const o=e[0]>>4==1;if(t.hasInit){const n=new EncodedVideoChunk({data:e.slice(5),timestamp:r,type:o?F:D});t.decoder.decode(n)}else if(o&&0===e[1]){const r=15&e[0];n.setVideoCodec(r);const o=function(e){let r=e.subarray(1,4),t="avc1.";for(let e=0;e<3;e++){let n=r[e].toString(16);n.length<2&&(n="0"+n),t+=n}return{codec:t,description:e}}(e.slice(5));t.decoder.configure(o),t.hasInit=!0}},reset(){t.hasInit=!1,t.isEmitInfo=!1,t.offscreenCanvas=null,t.offscreenCanvasCtx=null}});var n={isDestroyed:!1,opt:{debug:d,useOffscreen:p,useWCS:f,videoBuffer:u,openWebglAlignment:m,videoBufferDelay:c},useOffscreen:function(){return n.opt.useOffscreen&&"undefined"!=typeof OffscreenCanvas},initAudioPlanar:function(e,t){postMessage({cmd:y,sampleRate:t,channels:e});var n=[],o=0;this.playAudioPlanar=function(t,i,a){for(var s=i,u=[],c=0,d=0;d<2;d++){var f=l.HEAPU32[(t>>2)+d]>>2;u[d]=l.HEAPF32.subarray(f,f+s)}if(o){if(!(s>=(i=1024-o)))return o+=s,r[0]=Float32Array.of(...r[0],...u[0]),void(2==e&&(r[1]=Float32Array.of(...r[1],...u[1])));n[0]=Float32Array.of(...r[0],...u[0].subarray(0,i)),2==e&&(n[1]=Float32Array.of(...r[1],...u[1].subarray(0,i))),postMessage({cmd:v,buffer:n,ts:a},n.map(e=>e.buffer)),c=i,s-=i}for(o=s;o>=1024;o-=1024)n[0]=u[0].slice(c,c+=1024),2==e&&(n[1]=u[1].slice(c-1024,c)),postMessage({cmd:v,buffer:n,ts:a},n.map(e=>e.buffer));o&&(r[0]=u[0].slice(c),2==e&&(r[1]=u[1].slice(c)))}},setVideoCodec:function(e){postMessage({cmd:w,code:e})},setAudioCodec:function(e){postMessage({cmd:E,code:e})},setVideoSize:function(e,r){postMessage({cmd:h,w:e,h:r});var t=e*r,o=t>>2;n.useOffscreen()?(this.offscreenCanvas=new OffscreenCanvas(e,r),this.offscreenCanvasGL=this.offscreenCanvas.getContext("webgl"),this.webglObj=((e,r)=>{var t=["attribute vec4 vertexPos;","attribute vec4 texturePos;","varying vec2 textureCoord;","void main()","{","gl_Position = vertexPos;","textureCoord = texturePos.xy;","}"].join("\n"),n=["precision highp float;","varying highp vec2 textureCoord;","uniform sampler2D ySampler;","uniform sampler2D uSampler;","uniform sampler2D vSampler;","const mat4 YUV2RGB = mat4","(","1.1643828125, 0, 1.59602734375, -.87078515625,","1.1643828125, -.39176171875, -.81296875, .52959375,","1.1643828125, 2.017234375, 0, -1.081390625,","0, 0, 0, 1",");","void main(void) {","highp float y = texture2D(ySampler, textureCoord).r;","highp float u = texture2D(uSampler, textureCoord).r;","highp float v = texture2D(vSampler, textureCoord).r;","gl_FragColor = vec4(y, u, v, 1) * YUV2RGB;","}"].join("\n");r&&e.pixelStorei(e.UNPACK_ALIGNMENT,1);var o=e.createShader(e.VERTEX_SHADER);e.shaderSource(o,t),e.compileShader(o),e.getShaderParameter(o,e.COMPILE_STATUS)||console.log("Vertex shader failed to compile: "+e.getShaderInfoLog(o));var i=e.createShader(e.FRAGMENT_SHADER);e.shaderSource(i,n),e.compileShader(i),e.getShaderParameter(i,e.COMPILE_STATUS)||console.log("Fragment shader failed to compile: "+e.getShaderInfoLog(i));var a=e.createProgram();e.attachShader(a,o),e.attachShader(a,i),e.linkProgram(a),e.getProgramParameter(a,e.LINK_STATUS)||console.log("Program failed to compile: "+e.getProgramInfoLog(a)),e.useProgram(a);var s=e.createBuffer();e.bindBuffer(e.ARRAY_BUFFER,s),e.bufferData(e.ARRAY_BUFFER,new Float32Array([1,1,-1,1,1,-1,-1,-1]),e.STATIC_DRAW);var l=e.getAttribLocation(a,"vertexPos");e.enableVertexAttribArray(l),e.vertexAttribPointer(l,2,e.FLOAT,!1,0,0);var u=e.createBuffer();e.bindBuffer(e.ARRAY_BUFFER,u),e.bufferData(e.ARRAY_BUFFER,new Float32Array([1,0,0,0,1,1,0,1]),e.STATIC_DRAW);var c=e.getAttribLocation(a,"texturePos");function d(r,t){var n=e.createTexture();return e.bindTexture(e.TEXTURE_2D,n),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.bindTexture(e.TEXTURE_2D,null),e.uniform1i(e.getUniformLocation(a,r),t),n}e.enableVertexAttribArray(c),e.vertexAttribPointer(c,2,e.FLOAT,!1,0,0);var f=d("ySampler",0),p=d("uSampler",1),m=d("vSampler",2);return{render:function(r,t,n,o,i){e.viewport(0,0,r,t),e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,f),e.texImage2D(e.TEXTURE_2D,0,e.LUMINANCE,r,t,0,e.LUMINANCE,e.UNSIGNED_BYTE,n),e.activeTexture(e.TEXTURE1),e.bindTexture(e.TEXTURE_2D,p),e.texImage2D(e.TEXTURE_2D,0,e.LUMINANCE,r/2,t/2,0,e.LUMINANCE,e.UNSIGNED_BYTE,o),e.activeTexture(e.TEXTURE2),e.bindTexture(e.TEXTURE_2D,m),e.texImage2D(e.TEXTURE_2D,0,e.LUMINANCE,r/2,t/2,0,e.LUMINANCE,e.UNSIGNED_BYTE,i),e.drawArrays(e.TRIANGLE_STRIP,0,4)},destroy:function(){try{e.deleteProgram(a),e.deleteBuffer(s),e.deleteBuffer(u),e.deleteTexture(f),e.deleteTexture(p),e.deleteTexture(m)}catch(e){}}}})(this.offscreenCanvasGL,n.opt.openWebglAlignment),this.draw=function(n,i,a,s){const u=l.HEAPU8.subarray(i,i+t),c=l.HEAPU8.subarray(a,a+o),d=l.HEAPU8.subarray(s,s+o);this.webglObj.render(e,r,u,c,d);let f=this.offscreenCanvas.transferToImageBitmap();postMessage({cmd:g,buffer:f,delay:this.delay,ts:n},[f])}):this.draw=function(e,r,n,i){const a=[Uint8Array.from(l.HEAPU8.subarray(r,r+t)),Uint8Array.from(l.HEAPU8.subarray(n,n+o)),Uint8Array.from(l.HEAPU8.subarray(i,i+o))];postMessage({cmd:g,output:a,delay:this.delay,ts:e},a.map(e=>e.buffer))}},getDelay:function(e){if(!e)return-1;if(this.firstTimestamp){if(e){const r=Date.now()-this.startTimestamp,t=e-this.firstTimestamp;this.delay=r>=t?r-t:t-r}}else this.firstTimestamp=e,this.startTimestamp=Date.now(),this.delay=-1;return this.delay},resetDelay:function(){this.firstTimestamp=null,this.startTimestamp=null,this.delay=-1},init:function(){n.opt.debug&&console.log("Jb: [worker] init");const r=e=>{n.opt.useWCS&&n.useOffscreen()&&e.type===_&&t.decode?t.decode(e.payload,e.ts):e.decoder.decode(e.payload,e.ts)};this.stopId=setInterval(()=>{if(!n.isDestroyed&&e.length)if(this.dropping){for((t=e.shift()).type===b&&0===t.payload[1]&&r(t);!t.isIFrame&&e.length;)(t=e.shift()).type===b&&0===t.payload[1]&&r(t);t.isIFrame&&(this.dropping=!1,r(t))}else{var t=e[0];if(-1===this.getDelay(t.ts))e.shift(),r(t);else if(this.delay>n.opt.videoBuffer+n.opt.videoBufferDelay)this.resetDelay(),this.dropping=!0;else for(;e.length&&(t=e[0],this.getDelay(t.ts)>n.opt.videoBuffer);)e.shift(),r(t)}},10)},close:function(){n.isDestroyed=!0,n.opt.debug&&console.log("Jb: [worker]: close"),clearInterval(this.stopId),this.stopId=null,o.clear&&o.clear(),i.clear&&i.clear(),t.reset&&t.reset(),this.firstTimestamp=null,this.startTimestamp=null,this.delay=-1,this.dropping=!1,this.webglObj&&(this.webglObj.destroy(),this.offscreenCanvas=null,this.offscreenCanvasGL=null,this.offscreenCanvasCtx=null),e=[],r=[],delete this.playAudioPlanar,delete this.draw},pushBuffer:function(r,t){t.type===b?e.push({ts:t.ts,payload:r,decoder:o,type:b}):t.type===_&&e.push({ts:t.ts,payload:r,decoder:i,type:_,isIFrame:t.isIFrame})}},o=new l.AudioDecoder(n),i=new l.VideoDecoder(n);postMessage({cmd:T}),self.onmessage=function(e){var r=e.data;switch(r.cmd){case T:try{n.opt=Object.assign(n.opt,JSON.parse(r.opt))}catch(e){}o.sample_rate=r.sampleRate,n.init();break;case k:n.pushBuffer(r.buffer,r.options);break;case S:o.decode(r.buffer,r.ts);break;case C:i.decode(r.buffer,r.ts);break;case P:n.close();break;case A:n.opt[r.key]=r.value}}}}); diff --git a/web/public/static/js/jessibuca/decoder.wasm b/web/public/static/js/jessibuca/decoder.wasm new file mode 100755 index 0000000..89c8185 Binary files /dev/null and b/web/public/static/js/jessibuca/decoder.wasm differ diff --git a/web/public/static/js/jessibuca/jessibuca.d.ts b/web/public/static/js/jessibuca/jessibuca.d.ts new file mode 100644 index 0000000..09a1ac5 --- /dev/null +++ b/web/public/static/js/jessibuca/jessibuca.d.ts @@ -0,0 +1,683 @@ +declare namespace Jessibuca { + + /** 超时信息 */ + enum TIMEOUT { + /** 当play()的时候,如果没有数据返回 */ + loadingTimeout = 'loadingTimeout', + /** 当播放过程中,如果超过timeout之后没有数据渲染 */ + delayTimeout = 'delayTimeout', + } + + /** 错误信息 */ + enum ERROR { + /** 播放错误,url 为空的时候,调用 play 方法 */ + playError = 'playError', + /** http 请求失败 */ + fetchError = 'fetchError', + /** websocket 请求失败 */ + websocketError = 'websocketError', + /** webcodecs 解码 h265 失败 */ + webcodecsH265NotSupport = 'webcodecsH265NotSupport', + /** mediaSource 解码 h265 失败 */ + mediaSourceH265NotSupport = 'mediaSourceH265NotSupport', + /** wasm 解码失败 */ + wasmDecodeError = 'wasmDecodeError', + } + + interface Config { + /** + * 播放器容器 + * * 若为 string ,则底层调用的是 document.getElementById('id') + * */ + container: HTMLElement | string; + /** + * 设置最大缓冲时长,单位秒,播放器会自动消除延迟 + */ + videoBuffer?: number; + /** + * worker地址 + * * 默认引用的是根目录下面的decoder.js文件 ,decoder.js 与 decoder.wasm文件必须是放在同一个目录下面。 */ + decoder?: string; + /** + * 是否不使用离屏模式(提升渲染能力) + */ + forceNoOffscreen?: boolean; + /** + * 是否开启当页面的'visibilityState'变为'hidden'的时候,自动暂停播放。 + */ + hiddenAutoPause?: boolean; + /** + * 是否有音频,如果设置`false`,则不对音频数据解码,提升性能。 + */ + hasAudio?: boolean; + /** + * 设置旋转角度,只支持,0(默认),180,270 三个值 + */ + rotate?: boolean; + /** + * 1. 当为`true`的时候:视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边。 等同于 `setScaleMode(1)` + * 2. 当为`false`的时候:视频画面完全填充canvas区域,画面会被拉伸。等同于 `setScaleMode(0)` + */ + isResize?: boolean; + /** + * 1. 当为`true`的时候:视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全。等同于 `setScaleMode(2)` + */ + isFullResize?: boolean; + /** + * 1. 当为`true`的时候:ws协议不检验是否以.flv为依据,进行协议解析。 + */ + isFlv?: boolean; + /** + * 是否开启控制台调试打 + */ + debug?: boolean; + /** + * 1. 设置超时时长, 单位秒 + * 2. 在连接成功之前(loading)和播放中途(heart),如果超过设定时长无数据返回,则回调timeout事件 + */ + timeout?: number; + /** + * 1. 设置超时时长, 单位秒 + * 2. 在连接成功之前,如果超过设定时长无数据返回,则回调timeout事件 + */ + heartTimeout?: number; + /** + * 1. 设置超时时长, 单位秒 + * 2. 在连接成功之前,如果超过设定时长无数据返回,则回调timeout事件 + */ + loadingTimeout?: number; + /** + * 是否支持屏幕的双击事件,触发全屏,取消全屏事件 + */ + supportDblclickFullscreen?: boolean; + /** + * 是否显示网 + */ + showBandwidth?: boolean; + /** + * 配置操作按钮 + */ + operateBtns?: { + /** 是否显示全屏按钮 */ + fullscreen?: boolean; + /** 是否显示截图按钮 */ + screenshot?: boolean; + /** 是否显示播放暂停按钮 */ + play?: boolean; + /** 是否显示声音按钮 */ + audio?: boolean; + /** 是否显示录制按 */ + record?: boolean; + }; + /** + * 开启屏幕常亮,在手机浏览器上, canvas标签渲染视频并不会像video标签那样保持屏幕常亮 + */ + keepScreenOn?: boolean; + /** + * 是否开启声音,默认是关闭声音播放的 + */ + isNotMute?: boolean; + /** + * 加载过程中文案 + */ + loadingText?: string; + /** + * 背景图片 + */ + background?: string; + /** + * 是否开启MediaSource硬解码 + * * 视频编码只支持H.264视频(Safari on iOS不支持) + * * 不支持 forceNoOffscreen 为 false (开启离屏渲染) + */ + useMSE?: boolean; + /** + * 是否开启Webcodecs硬解码 + * * 视频编码只支持H.264视频 (需在chrome 94版本以上,需要https或者localhost环境) + * * 支持 forceNoOffscreen 为 false (开启离屏渲染) + * */ + useWCS?: boolean; + /** + * 是否开启键盘快捷键 + * 目前支持的键盘快捷键有:esc -> 退出全屏;arrowUp -> 声音增加;arrowDown -> 声音减少; + */ + hotKey?: boolean; + /** + * 在使用MSE或者Webcodecs 播放H265的时候,是否自动降级到wasm模式。 + * 设置为false 则直接关闭播放,抛出Error 异常,设置为true 则会自动切换成wasm模式播放。 + */ + autoWasm?: boolean; + /** + * heartTimeout 心跳超时之后自动再播放,不再抛出异常,而直接重新播放视频地址。 + */ + heartTimeoutReplay?: boolean, + /** + * heartTimeoutReplay 从试次数,超过之后,不再自动播放 + */ + heartTimeoutReplayTimes?: number, + /** + * loadingTimeout loading之后自动再播放,不再抛出异常,而直接重新播放视频地址。 + */ + loadingTimeoutReplay?: boolean, + /** + * heartTimeoutReplay 从试次数,超过之后,不再自动播放 + */ + loadingTimeoutReplayTimes?: number + /** + * wasm解码报错之后,不再抛出异常,而是直接重新播放视频地址。 + */ + wasmDecodeErrorReplay?: boolean, + /** + * https://github.com/langhuihui/jessibuca/issues/152 解决方案 + * 例如:WebGL图像预处理默认每次取4字节的数据,但是540x960分辨率下的U、V分量宽度是540/2=270不能被4整除,导致绿屏。 + */ + openWebglAlignment?: boolean, + + /** + * webcodecs硬解码是否通过video标签渲染 + */ + wcsUseVideoRender?: boolean, + + /** + * 底部控制台是否自动隐藏 + */ + controlAutoHide?: boolean, + + /** + * 录制的视频格式 + */ + recordType?: 'webm' | 'mp4', + + /** + * 是否使用web全屏(旋转90度)(只会在移动端生效)。 + */ + useWebFullScreen?: boolean, + + /** + * 是否自动使用系统全屏 + */ + autoUseSystemFullScreen?: boolean, + } +} + + +declare class Jessibuca { + + constructor(config?: Jessibuca.Config); + + /** + * 是否开启控制台调试打印 + @example + // 开启 + jessibuca.setDebug(true) + // 关闭 + jessibuca.setDebug(false) + */ + setDebug(flag: boolean): void; + + /** + * 静音 + @example + jessibuca.mute() + */ + mute(): void; + + /** + * 取消静音 + @example + jessibuca.cancelMute() + */ + cancelMute(): void; + + /** + * 留给上层用户操作来触发音频恢复的方法。 + * + * iPhone,chrome等要求自动播放时,音频必须静音,需要由一个真实的用户交互操作来恢复,不能使用代码。 + * + * https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + */ + audioResume(): void; + + /** + * + * 设置超时时长, 单位秒 + * 在连接成功之前和播放中途,如果超过设定时长无数据返回,则回调timeout事件 + + @example + jessibuca.setTimeout(10) + + jessibuca.on('timeout',function(){ + // + }); + */ + setTimeout(): void; + + /** + * @param mode + * 0 视频画面完全填充canvas区域,画面会被拉伸 等同于参数 `isResize` 为false + * + * 1 视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边 等同于参数 `isResize` 为true + * + * 2 视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全 等同于参数 `isFullResize` 为true + @example + jessibuca.setScaleMode(0) + + jessibuca.setScaleMode(1) + + jessibuca.setScaleMode(2) + */ + setScaleMode(mode: number): void; + + /** + * 暂停播放 + * + * 可以在pause 之后,再调用 `play()`方法就继续播放之前的流。 + @example + jessibuca.pause().then(()=>{ + console.log('pause success') + + jessibuca.play().then(()=>{ + + }).catch((e)=>{ + + }) + + }).catch((e)=>{ + console.log('pause error',e); + }) + */ + pause(): Promise; + + /** + * 关闭视频,不释放底层资源 + @example + jessibuca.close(); + */ + close(): void; + + /** + * 关闭视频,释放底层资源 + @example + jessibuca.destroy() + */ + destroy(): void; + + /** + * 清理画布为黑色背景 + @example + jessibuca.clearView() + */ + clearView(): void; + + /** + * 播放视频 + @example + + jessibuca.play('url').then(()=>{ + console.log('play success') + }).catch((e)=>{ + console.log('play error',e) + }) + // 添加请求头 + jessibuca.play('url',{headers:{'Authorization':'test111'}}).then(()=>{ + console.log('play success') + }).catch((e)=>{ + console.log('play error',e) + }) + */ + play(url?: string, options?: { + headers: Object + }): Promise; + + /** + * 重新调整视图大小 + */ + resize(): void; + + /** + * 设置最大缓冲时长,单位秒,播放器会自动消除延迟。 + * + * 等同于 `videoBuffer` 参数。 + * + @example + // 设置 200ms 缓冲 + jessibuca.setBufferTime(0.2) + */ + setBufferTime(time: number): void; + + /** + * 设置旋转角度,只支持,0(默认) ,180,270 三个值。 + * + * > 可用于实现监控画面小窗和全屏效果,由于iOS没有全屏API,此方法可以模拟页面内全屏效果而且多端效果一致。 * + @example + jessibuca.setRotate(0) + + jessibuca.setRotate(90) + + jessibuca.setRotate(270) + */ + setRotate(deg: number): void; + + /** + * + * 设置音量大小,取值0 — 1 + * + * > 区别于 mute 和 cancelMute 方法,虽然设置setVolume(0) 也能达到 mute方法,但是mute 方法是不调用底层播放音频的,能提高性能。而setVolume(0)只是把声音设置为0 ,以达到效果。 + * @param volume 当为0时,完全无声;当为1时,最大音量,默认值 + @example + jessibuca.setVolume(0.2) + + jessibuca.setVolume(0) + + jessibuca.setVolume(1) + */ + setVolume(volume: number): void; + + /** + * 返回是否加载完毕 + @example + var result = jessibuca.hasLoaded() + console.log(result) // true + */ + hasLoaded(): boolean; + + /** + * 开启屏幕常亮,在手机浏览器上, canvas标签渲染视频并不会像video标签那样保持屏幕常亮。 + * H5目前在chrome\edge 84, android chrome 84及以上有原生亮屏API, 需要是https页面 + * 其余平台为模拟实现,此时为兼容实现,并不保证所有浏览器都支持 + @example + jessibuca.setKeepScreenOn() + */ + setKeepScreenOn(): boolean; + + /** + * 全屏(取消全屏)播放视频 + @example + jessibuca.setFullscreen(true) + // + jessibuca.setFullscreen(false) + */ + setFullscreen(flag: boolean): void; + + /** + * + * 截图,调用后弹出下载框保存截图 + * @param filename 可选参数, 保存的文件名, 默认 `时间戳` + * @param format 可选参数, 截图的格式,可选png或jpeg或者webp ,默认 `png` + * @param quality 可选参数, 当格式是jpeg或者webp时,压缩质量,取值0 ~ 1 ,默认 `0.92` + * @param type 可选参数, 可选download或者base64或者blob,默认`download` + + @example + + jessibuca.screenshot("test","png",0.5) + + const base64 = jessibuca.screenshot("test","png",0.5,'base64') + + const fileBlob = jessibuca.screenshot("test",'blob') + */ + screenshot(filename?: string, format?: string, quality?: number, type?: string): void; + + /** + * 开始录制。 + * @param fileName 可选,默认时间戳 + * @param fileType 可选,默认webm,支持webm 和mp4 格式 + + @example + jessibuca.startRecord('xxx','webm') + */ + startRecord(fileName: string, fileType: string): void; + + /** + * 暂停录制并下载。 + @example + jessibuca.stopRecordAndSave() + */ + stopRecordAndSave(): void; + + /** + * 返回是否正在播放中状态。 + @example + var result = jessibuca.isPlaying() + console.log(result) // true + */ + isPlaying(): boolean; + + /** + * 返回是否静音。 + @example + var result = jessibuca.isMute() + console.log(result) // true + */ + isMute(): boolean; + + /** + * 返回是否正在录制。 + @example + var result = jessibuca.isRecording() + console.log(result) // true + */ + isRecording(): boolean; + + /** + * 切换底部控制条 隐藏/显示 + * @param isShow + * + * @example + * jessibuca.toggleControlBar(true) // 显示 + * jessibuca.toggleControlBar(false) // 隐藏 + * jessibuca.toggleControlBar() // 切换 隐藏/显示 + */ + toggleControlBar(isShow:boolean): void; + + /** + * 获取底部控制条是否显示 + */ + getControlBarShow(): boolean; + + /** + * 监听 jessibuca 初始化事件 + * @example + * jessibuca.on("load",function(){console.log('load')}) + */ + on(event: 'load', callback: () => void): void; + + /** + * 视频播放持续时间,单位ms + * @example + * jessibuca.on('timeUpdate',function (ts) {console.log('timeUpdate',ts);}) + */ + on(event: 'timeUpdate', callback: () => void): void; + + /** + * 当解析出视频信息时回调,2个回调参数 + * @example + * jessibuca.on("videoInfo",function(data){console.log('width:',data.width,'height:',data.width)}) + */ + on(event: 'videoInfo', callback: (data: { + /** 视频宽 */ + width: number; + /** 视频高 */ + height: number; + }) => void): void; + + /** + * 当解析出音频信息时回调,2个回调参数 + * @example + * jessibuca.on("audioInfo",function(data){console.log('numOfChannels:',data.numOfChannels,'sampleRate',data.sampleRate)}) + */ + on(event: 'audioInfo', callback: (data: { + /** 声频通道 */ + numOfChannels: number; + /** 采样率 */ + sampleRate: number; + }) => void): void; + + /** + * 信息,包含错误信息 + * @example + * jessibuca.on("log",function(data){console.log('data:',data)}) + */ + on(event: 'log', callback: () => void): void; + + /** + * 错误信息 + * @example + * jessibuca.on("error",function(error){ + if(error === Jessibuca.ERROR.fetchError){ + // + } + else if(error === Jessibuca.ERROR.webcodecsH265NotSupport){ + // + } + console.log('error:',error) + }) + */ + on(event: 'error', callback: (err: Jessibuca.ERROR) => void): void; + + /** + * 当前网速, 单位KB 每秒1次, + * @example + * jessibuca.on("kBps",function(data){console.log('kBps:',data)}) + */ + on(event: 'kBps', callback: (value: number) => void): void; + + /** + * 渲染开始 + * @example + * jessibuca.on("start",function(){console.log('start render')}) + */ + on(event: 'start', callback: () => void): void; + + /** + * 当设定的超时时间内无数据返回,则回调 + * @example + * jessibuca.on("timeout",function(error){console.log('timeout:',error)}) + */ + on(event: 'timeout', callback: (error: Jessibuca.TIMEOUT) => void): void; + + /** + * 当play()的时候,如果没有数据返回,则回调 + * @example + * jessibuca.on("loadingTimeout",function(){console.log('timeout')}) + */ + on(event: 'loadingTimeout', callback: () => void): void; + + /** + * 当播放过程中,如果超过timeout之后没有数据渲染,则抛出异常。 + * @example + * jessibuca.on("delayTimeout",function(){console.log('timeout')}) + */ + on(event: 'delayTimeout', callback: () => void): void; + + /** + * 当前是否全屏 + * @example + * jessibuca.on("fullscreen",function(flag){console.log('is fullscreen',flag)}) + */ + on(event: 'fullscreen', callback: () => void): void; + + /** + * 触发播放事件 + * @example + * jessibuca.on("play",function(flag){console.log('play')}) + */ + on(event: 'play', callback: () => void): void; + + /** + * 触发暂停事件 + * @example + * jessibuca.on("pause",function(flag){console.log('pause')}) + */ + on(event: 'pause', callback: () => void): void; + + /** + * 触发声音事件,返回boolean值 + * @example + * jessibuca.on("mute",function(flag){console.log('is mute',flag)}) + */ + on(event: 'mute', callback: () => void): void; + + /** + * 流状态统计,流开始播放后回调,每秒1次。 + * @example + * jessibuca.on("stats",function(s){console.log("stats is",s)}) + */ + on(event: 'stats', callback: (stats: { + /** 当前缓冲区时长,单位毫秒 */ + buf: number; + /** 当前视频帧率 */ + fps: number; + /** 当前音频码率,单位byte */ + abps: number; + /** 当前视频码率,单位byte */ + vbps: number; + /** 当前视频帧pts,单位毫秒 */ + ts: number; + }) => void): void; + + /** + * 渲染性能统计,流开始播放后回调,每秒1次。 + * @param performance 0: 表示卡顿,1: 表示流畅,2: 表示非常流程 + * @example + * jessibuca.on("performance",function(performance){console.log("performance is",performance)}) + */ + on(event: 'performance', callback: (performance: 0 | 1 | 2) => void): void; + + /** + * 录制开始的事件 + + * @example + * jessibuca.on("recordStart",function(){console.log("record start")}) + */ + on(event: 'recordStart', callback: () => void): void; + + /** + * 录制结束的事件 + + * @example + * jessibuca.on("recordEnd",function(){console.log("record end")}) + */ + on(event: 'recordEnd', callback: () => void): void; + + /** + * 录制的时候,返回的录制时长,1s一次 + + * @example + * jessibuca.on("recordingTimestamp",function(timestamp){console.log("recordingTimestamp is",timestamp)}) + */ + on(event: 'recordingTimestamp', callback: (timestamp: number) => void): void; + + /** + * 监听调用play方法 经过 初始化-> 网络请求-> 解封装 -> 解码 -> 渲染 一系列过程的时间消耗 + * @param event + * @param callback + */ + on(event: 'playToRenderTimes', callback: (times: { + playInitStart: number, // 1 初始化 + playStart: number, // 2 初始化 + streamStart: number, // 3 网络请求 + streamResponse: number, // 4 网络请求 + demuxStart: number, // 5 解封装 + decodeStart: number, // 6 解码 + videoStart: number, // 7 渲染 + playTimestamp: number,// playStart- playInitStart + streamTimestamp: number,// streamStart - playStart + streamResponseTimestamp: number,// streamResponse - streamStart + demuxTimestamp: number, // demuxStart - streamResponse + decodeTimestamp: number, // decodeStart - demuxStart + videoTimestamp: number,// videoStart - decodeStart + allTimestamp: number // videoStart - playInitStart + }) => void): void + + /** + * 监听方法 + * + @example + + jessibuca.on("load",function(){console.log('load')}) + */ + on(event: string, callback: Function): void; + +} + +export default Jessibuca; diff --git a/web/public/static/js/jessibuca/jessibuca.js b/web/public/static/js/jessibuca/jessibuca.js new file mode 100644 index 0000000..e9ac986 --- /dev/null +++ b/web/public/static/js/jessibuca/jessibuca.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).jessibuca=t()}(this,function(){"use strict";var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}function i(e,t){return e(t={exports:{}},t.exports),t.exports}var o=i(function(e){function t(i){return e.exports=t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e.exports.__esModule=!0,e.exports.default=e.exports,t(i)}e.exports=t,e.exports.__esModule=!0,e.exports.default=e.exports});t(o);var r=i(function(e){var t=o.default;e.exports=function(e,i){if("object"!=t(e)||!e)return e;var o=e[Symbol.toPrimitive];if(void 0!==o){var r=o.call(e,i||"default");if("object"!=t(r))return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===i?String:Number)(e)},e.exports.__esModule=!0,e.exports.default=e.exports});t(r);var s=i(function(e){var t=o.default;e.exports=function(e){var i=r(e,"string");return"symbol"==t(i)?i:i+""},e.exports.__esModule=!0,e.exports.default=e.exports});t(s);var a=t(i(function(e){e.exports=function(e,t,i){return(t=s(t))in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e},e.exports.__esModule=!0,e.exports.default=e.exports}));const n=0,A=1,d="flv",c="m7s",l="mp4",u="webm",h="jessibuca",p='"3.3.21"',g={videoBuffer:1e3,videoBufferDelay:1e3,isResize:!0,isFullResize:!1,isFlv:!1,debug:!1,hotKey:!1,loadingTimeout:10,heartTimeout:5,timeout:10,loadingTimeoutReplay:!0,heartTimeoutReplay:!0,loadingTimeoutReplayTimes:3,heartTimeoutReplayTimes:3,supportDblclickFullscreen:!1,showBandwidth:!1,keepScreenOn:!1,isNotMute:!1,hasAudio:!0,hasVideo:!0,operateBtns:{fullscreen:!1,screenshot:!1,play:!1,audio:!1,record:!1},controlAutoHide:!1,hasControl:!1,loadingText:"",background:"",decoder:"decoder.js",url:"",rotate:0,forceNoOffscreen:!0,hiddenAutoPause:!1,protocol:A,demuxType:d,useWCS:!1,wcsUseVideoRender:!1,useMSE:!1,useOffscreen:!1,autoWasm:!0,wasmDecodeErrorReplay:!0,openWebglAlignment:!1,wasmDecodeAudioSyncVideo:!1,recordType:u,useWebFullScreen:!1,loadingDecoderWorkerTimeout:10,autoUseSystemFullScreen:!0},m="init",f="initVideo",b="render",y="playAudio",v="initAudio",w="audioCode",S="videoCode",E="wasmError",B="Invalid NAL unit size",C=1,R=2,k=8,T=9,I="init",x="decode",D="audioDecode",j="close",L="updateConfig",F={fullscreen:"fullscreen$2",webFullscreen:"webFullscreen",decoderWorkerInit:"decoderWorkerInit",play:"play",playing:"playing",pause:"pause",mute:"mute",load:"load",loading:"loading",videoInfo:"videoInfo",timeUpdate:"timeUpdate",audioInfo:"audioInfo",log:"log",error:"error",kBps:"kBps",timeout:"timeout",delayTimeout:"delayTimeout",loadingTimeout:"loadingTimeout",stats:"stats",performance:"performance",record:"record",recording:"recording",recordingTimestamp:"recordingTimestamp",recordStart:"recordStart",recordEnd:"recordEnd",recordCreateError:"recordCreateError",buffer:"buffer",videoFrame:"videoFrame",start:"start",metadata:"metadata",resize:"resize",streamEnd:"streamEnd",streamSuccess:"streamSuccess",streamMessage:"streamMessage",streamError:"streamError",volumechange:"volumechange",volume:"volume",destroy:"destroy",mseSourceOpen:"mseSourceOpen",mseSourceClose:"mseSourceClose",mseSourceBufferError:"mseSourceBufferError",mseSourceBufferBusy:"mseSourceBufferBusy",mseSourceBufferFull:"mseSourceBufferFull",videoWaiting:"videoWaiting",videoTimeUpdate:"videoTimeUpdate",videoSyncAudio:"videoSyncAudio",playToRenderTimes:"playToRenderTimes"},M={load:F.load,timeUpdate:F.timeUpdate,videoInfo:F.videoInfo,audioInfo:F.audioInfo,error:F.error,kBps:F.kBps,log:F.log,start:F.start,timeout:F.timeout,loadingTimeout:F.loadingTimeout,delayTimeout:F.delayTimeout,fullscreen:"fullscreen",webFullscreen:F.webFullscreen,play:F.play,pause:F.pause,mute:F.mute,stats:F.stats,volumechange:F.volumechange,performance:F.performance,recordingTimestamp:F.recordingTimestamp,recordStart:F.recordStart,recordEnd:F.recordEnd,playToRenderTimes:F.playToRenderTimes,volume:F.volume},O={playError:"playIsNotPauseOrUrlIsNull",fetchError:"fetchError",websocketError:"websocketError",webcodecsH265NotSupport:"webcodecsH265NotSupport",webcodecsConfigureError:"webcodecsConfigureError",webcodecsDecodeError:"webcodecsDecodeError",webcodecsWidthOrHeightChange:"webcodecsWidthOrHeightChange",mediaSourceH265NotSupport:"mediaSourceH265NotSupport",mediaSourceFull:F.mseSourceBufferFull,mseSourceBufferError:F.mseSourceBufferError,mediaSourceAppendBufferError:"mediaSourceAppendBufferError",mediaSourceBufferListLarge:"mediaSourceBufferListLarge",mediaSourceAppendBufferEndTimeout:"mediaSourceAppendBufferEndTimeout",wasmDecodeError:"wasmDecodeError",webglAlignmentError:"webglAlignmentError",webglContextLostError:"webglContextLostError",webglInitError:"webglInitError"},V="notConnect",U="open",Q="close",W="error",J={download:"download",base64:"base64",blob:"blob"},G={7:"H264(AVC)",12:"H265(HEVC)"},P=12,N={10:"AAC",7:"ALAW",8:"MULAW"},H=38,z=0,Y=1,X=2,q="webcodecs",Z="webgl",K="offscreen",_="key",$="delta",ee='video/mp4; codecs="avc1.64002A"',te="ended",ie="open",oe="closed",re=1e3,se=27,ae=38,ne=40,Ae="A key frame is required after configure() or flush()",de="Cannot call 'decode' on a closed codec",ce="The user aborted a request",le="AbortError",ue="AbortError",he=0,pe=1,ge=3,me=16;class fe{constructor(e){this.log=(t,...i)=>{e._opt&&e._opt.debug&&console.log(`Jb: [${t}]`,...i)},this.warn=(t,...i)=>{e._opt&&e._opt.debug&&console.warn(`Jb: [${t}]`,...i)},this.error=(e,...t)=>{console.error(`Jb: [${e}]`,...t)}}}class be{constructor(e){this.destroys=[],this.proxy=this.proxy.bind(this),this.master=e}proxy(e,t,i,o={}){if(!e)return;if(Array.isArray(t))return t.map(t=>this.proxy(e,t,i,o));e.addEventListener(t,i,o);const r=()=>e.removeEventListener(t,i,o);return this.destroys.push(r),r}destroy(){this.master.debug&&this.master.debug.log("Events","destroy"),this.destroys.forEach(e=>e())}}var ye=i(function(e){!function(){var t="undefined"!=typeof window&&void 0!==window.document?window.document:{},i=e.exports,o=function(){for(var e,i=[["requestFullscreen","exitFullscreen","fullscreenElement","fullscreenEnabled","fullscreenchange","fullscreenerror"],["webkitRequestFullscreen","webkitExitFullscreen","webkitFullscreenElement","webkitFullscreenEnabled","webkitfullscreenchange","webkitfullscreenerror"],["webkitRequestFullScreen","webkitCancelFullScreen","webkitCurrentFullScreenElement","webkitCancelFullScreen","webkitfullscreenchange","webkitfullscreenerror"],["mozRequestFullScreen","mozCancelFullScreen","mozFullScreenElement","mozFullScreenEnabled","mozfullscreenchange","mozfullscreenerror"],["msRequestFullscreen","msExitFullscreen","msFullscreenElement","msFullscreenEnabled","MSFullscreenChange","MSFullscreenError"]],o=0,r=i.length,s={};o{Be(e,i,t[i])}),e.style[t]=i,e}function Ce(e,t,i=!0){if(!e)return 0;const o=getComputedStyle(e,null).getPropertyValue(t);return i?parseFloat(o):o}function Re(){return performance&&"function"==typeof performance.now?performance.now():Date.now()}function ke(e){let t=0,i=Re();return o=>{t+=o;const r=Re(),s=r-i;s>=1e3&&(e(t/s*1e3),i=r,t=0)}}function Te(){return/iphone|ipod|android.*mobile|windows.*phone|blackberry.*mobile/i.test(window.navigator.userAgent.toLowerCase())}function Ie(e){return null==e}function xe(e){return!0===e||!1===e}function De(e){return!Ie(e)}function je(e){var t;if(e>-1){var i=Math.floor(e/3600),o=Math.floor(e/60)%60,r=e%60;t=i<10?"0"+i+":":i+":",o<10&&(t+="0"),t+=o+":",(r=Math.round(r))<10&&(t+="0"),t+=r.toFixed(0)}return t}function Le(e){const t=e||window.event;return t.target||t.srcElement}function Fe(e){let t=!1;return e&&(e.innerHTML="",e.parentNode&&(e.parentNode.removeChild(e),t=!0)),t}function Me(e,t){let i=[];i[0]=t?28:44,i[1]=1,i[2]=0,i[3]=0,i[4]=0;const o=new Uint8Array(i.length+e.byteLength);return o.set(i,0),o.set(e,i.length),o}function Oe(e){return!0!==e&&"true"!==e}ye.isEnabled,(()=>{try{if("object"==typeof WebAssembly&&"function"==typeof WebAssembly.instantiate){const e=new WebAssembly.Module(Uint8Array.of(0,97,115,109,1,0,0,0));if(e instanceof WebAssembly.Module)return new WebAssembly.Instance(e)instanceof WebAssembly.Instance}}catch(e){}})();class Ve{on(e,t,i){const o=this.e||(this.e={});return(o[e]||(o[e]=[])).push({fn:t,ctx:i}),this}once(e,t,i){const o=this;function r(...s){o.off(e,r),t.apply(i,s)}return r._=t,this.on(e,r,i)}emit(e,...t){const i=((this.e||(this.e={}))[e]||[]).slice();for(let e=0;e{delete i[e]}),void delete this.e;const o=i[e],r=[];if(o&&t)for(let e=0,i=o.length;e=200&&t.status<=299}function Ge(e){try{e.dispatchEvent(new MouseEvent("click"))}catch(i){var t=document.createEvent("MouseEvents");t.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),e.dispatchEvent(t)}}var Pe=Qe.navigator&&/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),Ne="object"!=typeof window||window!==Qe?function(){}:"download"in HTMLAnchorElement.prototype&&!Pe?function(e,t,i){var o=Qe.URL||Qe.webkitURL,r=document.createElementNS("http://www.w3.org/1999/xhtml","a");t=t||e.name||"download",r.download=t,r.rel="noopener","string"==typeof e?(r.href=e,r.origin!==location.origin?Je(r.href)?We(e,t,i):Ge(r,r.target="_blank"):Ge(r)):(r.href=o.createObjectURL(e),setTimeout(function(){o.revokeObjectURL(r.href)},4e4),setTimeout(function(){Ge(r)},0))}:"msSaveOrOpenBlob"in navigator?function(e,t,i){if(t=t||e.name||"download","string"==typeof e)if(Je(e))We(e,t,i);else{var o=document.createElement("a");o.href=e,o.target="_blank",setTimeout(function(){Ge(o)})}else navigator.msSaveOrOpenBlob(function(e,t){return void 0===t?t={autoBom:!1}:"object"!=typeof t&&(console.warn("Deprecated: Expected third argument to be a object"),t={autoBom:!t}),t.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(e.type)?new Blob([String.fromCharCode(65279),e],{type:e.type}):e}(e,i),t)}:function(e,t,i,o){if((o=o||open("","_blank"))&&(o.document.title=o.document.body.innerText="downloading..."),"string"==typeof e)return We(e,t,i);var r="application/octet-stream"===e.type,s=/constructor/i.test(Qe.HTMLElement)||Qe.safari,a=/CriOS\/[\d]+/.test(navigator.userAgent);if((a||r&&s||Pe)&&"undefined"!=typeof FileReader){var n=new FileReader;n.onloadend=function(){var e=n.result;e=a?e:e.replace(/^data:[^;]*;/,"data:attachment/file;"),o?o.location.href=e:location=e,o=null},n.readAsDataURL(e)}else{var A=Qe.URL||Qe.webkitURL,d=A.createObjectURL(e);o?o.location=d:location.href=d,o=null,setTimeout(function(){A.revokeObjectURL(d)},4e4)}};class He extends Ue{constructor(e){super(),this.player=e;const t=document.createElement("canvas");t.style.position="absolute",t.style.top=0,t.style.left=0,this.$videoElement=t,e.$container.appendChild(this.$videoElement),this.context2D=null,this.contextGl=null,this.contextGlRender=null,this.contextGlDestroy=null,this.bitmaprenderer=null,this.renderType=null,this.isContextGlRenderLost=!1,this.videoInfo={width:"",height:"",encType:""},this._initCanvasRender(),this.player.debug.log("CanvasVideo","init")}async destroy(){super.destroy(),this.contextGl&&(this.contextGl=null),this.context2D&&(this.context2D=null),this.contextGlRender&&(this.contextGlDestroy&&this.contextGlDestroy(),this.contextGlDestroy=null,this.contextGlRender=null),this.bitmaprenderer&&(this.bitmaprenderer=null),this.renderType=null,this.isContextGlRenderLost=!1,this.player.debug.log("CanvasVideoLoader","destroy")}_initContextGl(){if(this.contextGl=function(e){let t=null;const i=["webgl","experimental-webgl","moz-webgl","webkit-3d"];let o=0;for(;!t&&o{var i=["attribute vec4 vertexPos;","attribute vec4 texturePos;","varying vec2 textureCoord;","void main()","{","gl_Position = vertexPos;","textureCoord = texturePos.xy;","}"].join("\n"),o=["precision highp float;","varying highp vec2 textureCoord;","uniform sampler2D ySampler;","uniform sampler2D uSampler;","uniform sampler2D vSampler;","const mat4 YUV2RGB = mat4","(","1.1643828125, 0, 1.59602734375, -.87078515625,","1.1643828125, -.39176171875, -.81296875, .52959375,","1.1643828125, 2.017234375, 0, -1.081390625,","0, 0, 0, 1",");","void main(void) {","highp float y = texture2D(ySampler, textureCoord).r;","highp float u = texture2D(uSampler, textureCoord).r;","highp float v = texture2D(vSampler, textureCoord).r;","gl_FragColor = vec4(y, u, v, 1) * YUV2RGB;","}"].join("\n");t&&e.pixelStorei(e.UNPACK_ALIGNMENT,1);var r=e.createShader(e.VERTEX_SHADER);e.shaderSource(r,i),e.compileShader(r),e.getShaderParameter(r,e.COMPILE_STATUS)||console.log("Vertex shader failed to compile: "+e.getShaderInfoLog(r));var s=e.createShader(e.FRAGMENT_SHADER);e.shaderSource(s,o),e.compileShader(s),e.getShaderParameter(s,e.COMPILE_STATUS)||console.log("Fragment shader failed to compile: "+e.getShaderInfoLog(s));var a=e.createProgram();e.attachShader(a,r),e.attachShader(a,s),e.linkProgram(a),e.getProgramParameter(a,e.LINK_STATUS)||console.log("Program failed to compile: "+e.getProgramInfoLog(a)),e.useProgram(a);var n=e.createBuffer();e.bindBuffer(e.ARRAY_BUFFER,n),e.bufferData(e.ARRAY_BUFFER,new Float32Array([1,1,-1,1,1,-1,-1,-1]),e.STATIC_DRAW);var A=e.getAttribLocation(a,"vertexPos");e.enableVertexAttribArray(A),e.vertexAttribPointer(A,2,e.FLOAT,!1,0,0);var d=e.createBuffer();e.bindBuffer(e.ARRAY_BUFFER,d),e.bufferData(e.ARRAY_BUFFER,new Float32Array([1,0,0,0,1,1,0,1]),e.STATIC_DRAW);var c=e.getAttribLocation(a,"texturePos");function l(t,i){var o=e.createTexture();return e.bindTexture(e.TEXTURE_2D,o),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.bindTexture(e.TEXTURE_2D,null),e.uniform1i(e.getUniformLocation(a,t),i),o}e.enableVertexAttribArray(c),e.vertexAttribPointer(c,2,e.FLOAT,!1,0,0);var u=l("ySampler",0),h=l("uSampler",1),p=l("vSampler",2);return{render:function(t,i,o,r,s){e.viewport(0,0,t,i),e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,u),e.texImage2D(e.TEXTURE_2D,0,e.LUMINANCE,t,i,0,e.LUMINANCE,e.UNSIGNED_BYTE,o),e.activeTexture(e.TEXTURE1),e.bindTexture(e.TEXTURE_2D,h),e.texImage2D(e.TEXTURE_2D,0,e.LUMINANCE,t/2,i/2,0,e.LUMINANCE,e.UNSIGNED_BYTE,r),e.activeTexture(e.TEXTURE2),e.bindTexture(e.TEXTURE_2D,p),e.texImage2D(e.TEXTURE_2D,0,e.LUMINANCE,t/2,i/2,0,e.LUMINANCE,e.UNSIGNED_BYTE,s),e.drawArrays(e.TRIANGLE_STRIP,0,4)},destroy:function(){try{e.deleteProgram(a),e.deleteBuffer(n),e.deleteBuffer(d),e.deleteTexture(u),e.deleteTexture(h),e.deleteTexture(p)}catch(e){}}}})(this.contextGl,this.player._opt.openWebglAlignment);this.contextGlRender=e.render,this.contextGlDestroy=e.destroy}else this.player.debug.error("CanvasVideoLoader","init webgl fail"),this.player.emitError(O.webglInitError)}_initContext2D(){this.context2D=this.$videoElement.getContext("2d")}_initCanvasRender(){this.player._opt.useWCS&&!this._supportOffscreen()?(this.renderType=q,this._initContext2D()):this._supportOffscreen()?(this.renderType=K,this._bindOffscreen()):(this.renderType=Z,this._initContextGl())}_supportOffscreen(){return"function"==typeof this.$videoElement.transferControlToOffscreen&&this.player._opt.useOffscreen}_bindOffscreen(){this.bitmaprenderer=this.$videoElement.getContext("bitmaprenderer")}initCanvasViewSize(){this.$videoElement.width=this.videoInfo.width,this.$videoElement.height=this.videoInfo.height,this.resize()}render(e){switch(this.player.videoTimestamp=e.ts,this.renderType){case K:this.bitmaprenderer.transferFromImageBitmap(e.buffer);break;case Z:if(this.isContextGlRenderLost)return;try{this.contextGlRender(this.$videoElement.width,this.$videoElement.height,e.output[0],e.output[1],e.output[2])}catch(e){this.player.debug.error("CanvasVideoLoader","webgl render error and emit webglContextLostError",e),this.isContextGlRenderLost=!0,this.player.emitError(O.webglContextLostError)}break;case q:this.context2D.drawImage(e.videoFrame,0,0,this.$videoElement.width,this.$videoElement.height),(t=e.videoFrame).close?t.close():t.destroy&&t.destroy()}var t}screenshot(e,t,i,o){e=e||Se(),o=o||J.download;const r={png:"image/png",jpeg:"image/jpeg",webp:"image/webp"};let s=.92;!r[t]&&J[t]&&(o=t,t="png",i=void 0),"string"==typeof i&&(o=i,i=void 0),void 0!==i&&(s=Number(i));const a=this.$videoElement.toDataURL(r[t]||r.png,s);if(o===J.base64)return a;{const t=we(a);if(o===J.blob)return t;o===J.download&&Ne(t,e)}}clearView(){switch(this.renderType){case K:(function(e,t){const i=document.createElement("canvas");return i.width=e,i.height=t,window.createImageBitmap(i,0,0,e,t)})(this.$videoElement.width,this.$videoElement.height).then(e=>{this.bitmaprenderer.transferFromImageBitmap(e)});break;case Z:this.contextGl.clear(this.contextGl.COLOR_BUFFER_BIT);break;case q:this.context2D.clearRect(0,0,this.$videoElement.width,this.$videoElement.height)}}resize(){this.player.debug.log("canvasVideo","resize");const e=this.player._opt;let t=this.player.width,i=this.player.height;this.player.isControlBarShow()&&(Te()&&this.player.fullscreen&&e.useWebFullScreen?t-=H:i-=H);let o=this.$videoElement.width,r=this.$videoElement.height;const s=e.rotate;let a=(t-o)/2,n=(i-r)/2;270!==s&&90!==s||(o=this.$videoElement.height,r=this.$videoElement.width);const A=t/o,d=i/r;let c=A>d?d:A;e.isResize||A!==d&&(c=A+","+d),e.isFullResize&&(c=A>d?A:d);let l="scale("+c+")";s&&(l+=" rotate("+s+"deg)"),this.$videoElement.style.transform=l,this.$videoElement.style.left=a+"px",this.$videoElement.style.top=n+"px"}}class ze extends Ue{constructor(e){super(),this.player=e;const t=document.createElement("video"),i=document.createElement("canvas");t.muted=!0,t.disablePictureInPicture=!0,function(){const e=window.navigator.userAgent.toLowerCase();return/android/i.test(e)}()&&(t.poster="noposter"),t.style.position="absolute",t.style.top=0,t.style.left=0,this._delayPlay=!1,e.$container.appendChild(t),this.videoInfo={width:"",height:"",encType:""};const o=this.player._opt;o.useWCS&&o.wcsUseVideoRender&&(this.trackGenerator=new MediaStreamTrackGenerator({kind:"video"}),t.srcObject=new MediaStream([this.trackGenerator]),this.vwriter=this.trackGenerator.writable.getWriter()),this.$videoElement=t,this.$canvasElement=i,this.canvasContext=i.getContext("2d"),this.fixChromeVideoFlashBug(),this.resize();const{proxy:r}=this.player.events;r(this.$videoElement,"canplay",()=>{this.player.debug.log("Video","canplay"),this._delayPlay&&(this.player.debug.log("Video","canplay and _delayPlay is true and next play()"),this._play())}),r(this.$videoElement,"waiting",()=>{this.player.debug.log("Video","waiting")}),r(this.$videoElement,"timeupdate",e=>{const t=parseInt(e.timeStamp,10);this.player.emit(F.timeUpdate,t),!this.isPlaying()&&this.init&&(this.player.debug.log("Video","timeupdate and this.isPlaying is false and retry play"),this.$videoElement.play())}),this.player.debug.log("Video","init")}async destroy(){super.destroy(),this.$canvasElement=null,this.canvasContext=null,this.$videoElement&&(this.$videoElement.pause(),this.$videoElement.currentTime=0,this.$videoElement.src="",this.$videoElement.removeAttribute("src"),this.$videoElement=null),this.trackGenerator&&(this.trackGenerator.stop(),this.trackGenerator=null),this.vwriter&&(await this.vwriter.close(),this.vwriter=null),this.player.debug.log("Video","destroy")}fixChromeVideoFlashBug(){const e=function(){const e=navigator.userAgent.toLowerCase(),t={},i={IE:window.ActiveXObject||"ActiveXObject"in window,Chrome:e.indexOf("chrome")>-1&&e.indexOf("safari")>-1,Firefox:e.indexOf("firefox")>-1,Opera:e.indexOf("opera")>-1,Safari:e.indexOf("safari")>-1&&-1==e.indexOf("chrome"),Edge:e.indexOf("edge")>-1,QQBrowser:/qqbrowser/.test(e),WeixinBrowser:/MicroMessenger/i.test(e)};for(let o in i)if(i[o]){let i="";if("IE"===o)i=e.match(/(msie\s|trident.*rv:)([\w.]+)/)[2];else if("Chrome"===o){for(let e in navigator.mimeTypes)"application/360softmgrplugin"===navigator.mimeTypes[e].type&&(o="360");i=e.match(/chrome\/([\d.]+)/)[1]}else"Firefox"===o?i=e.match(/firefox\/([\d.]+)/)[1]:"Opera"===o?i=e.match(/opera\/([\d.]+)/)[1]:"Safari"===o?i=e.match(/version\/([\d.]+)/)[1]:"Edge"===o?i=e.match(/edge\/([\d.]+)/)[1]:"QQBrowser"===o&&(i=e.match(/qqbrowser\/([\d.]+)/)[1]);t.type=o,t.version=parseInt(i)}return t}().type.toLowerCase();if("chrome"===e||"edge"===e){const e=this.player.$container;e.style.backdropFilter="blur(0px)",e.style.translateZ="0"}}play(){if(this.$videoElement){const e=this._getVideoReadyState();if(this.player.debug.log("Video",`play and readyState: ${e}`),0===e)return this.player.debug.warn("Video","readyState is 0 and set _delayPlay to true"),void(this._delayPlay=!0);this._play()}}_getVideoReadyState(){let e=0;return this.$videoElement&&(e=this.$videoElement.readyState),e}_play(){this.$videoElement&&this.$videoElement.play().then(()=>{this._delayPlay=!1,this.player.debug.log("Video","_play success"),setTimeout(()=>{this.isPlaying()||(this.player.debug.warn("Video","play failed and retry play"),this._play())},100)}).catch(e=>{this.player.debug.error("Video","_play error",e)})}pause(e){e?this.$videoElement&&this.$videoElement.pause():setTimeout(()=>{this.$videoElement&&this.$videoElement.pause()},100)}clearView(){}screenshot(e,t,i,o){e=e||Se(),o=o||J.download;const r={png:"image/png",jpeg:"image/jpeg",webp:"image/webp"};let s=.92;!r[t]&&J[t]&&(o=t,t="png",i=void 0),"string"==typeof i&&(o=i,i=void 0),void 0!==i&&(s=Number(i));const a=this.$videoElement;let n=this.$canvasElement;n.width=a.videoWidth,n.height=a.videoHeight,this.canvasContext.drawImage(a,0,0,n.width,n.height);const A=n.toDataURL(r[t]||r.png,s);if(this.canvasContext.clearRect(0,0,n.width,n.height),n.width=0,n.height=0,o===J.base64)return A;{const t=we(A);if(o===J.blob)return t;o===J.download&&Ne(t,e)}}initCanvasViewSize(){this.resize()}render(e){this.vwriter&&(this.vwriter.write(e.videoFrame),e.videoFrame.close())}resize(){let e=this.player.width,t=this.player.height;const i=this.player._opt,o=i.rotate;this.player.isControlBarShow()&&(Te()&&this.player.fullscreen&&i.useWebFullScreen?e-=H:t-=H),this.$videoElement.width=e,this.$videoElement.height=t,270!==o&&90!==o||(this.$videoElement.width=t,this.$videoElement.height=e);let r=(e-this.$videoElement.width)/2,s=(t-this.$videoElement.height)/2,a="contain";i.isResize||(a="fill"),i.isFullResize&&(a="none"),this.$videoElement.style.objectFit=a,this.$videoElement.style.transform="rotate("+o+"deg)",this.$videoElement.style.left=r+"px",this.$videoElement.style.top=s+"px"}isPlaying(){return this.$videoElement&&!this.$videoElement.paused}}class Ye{constructor(e){return new(Ye.getLoaderFactory(e._opt))(e)}static getLoaderFactory(e){return e.useMSE||e.useWCS&&!e.useOffscreen&&e.wcsUseVideoRender?ze:He}}class Xe extends Ve{constructor(e){super(),this.bufferList=[],this.player=e,this.scriptNode=null,this.hasInitScriptNode=!1,this.audioContextChannel=null,this.audioContext=new(window.AudioContext||window.webkitAudioContext),this.gainNode=this.audioContext.createGain();const t=this.audioContext.createBufferSource();t.buffer=this.audioContext.createBuffer(1,1,22050),t.connect(this.audioContext.destination),t.noteOn?t.noteOn(0):t.start(0),this.audioBufferSourceNode=t,this.mediaStreamAudioDestinationNode=this.audioContext.createMediaStreamDestination(),this.audioEnabled(!0),this.gainNode.gain.value=0,this._prevVolume=null,this.playing=!1,this.audioSyncVideoOption={diff:null},this.audioInfo={encType:"",channels:"",sampleRate:""},this.init=!1,this.hasAudio=!1,this.on(F.videoSyncAudio,e=>{this.audioSyncVideoOption=e}),this.player.debug.log("AudioContext","init")}resetInit(){this.init=!1,this.audioInfo={encType:"",channels:"",sampleRate:""}}async destroy(){this.closeAudio(),this.resetInit(),this.audioContext&&(await this.audioContext.close(),this.audioContext=null),this.gainNode=null,this.hasAudio=!1,this.playing=!1,this.scriptNode&&(this.scriptNode.onaudioprocess=ve,this.scriptNode=null),this.audioBufferSourceNode=null,this.mediaStreamAudioDestinationNode=null,this.hasInitScriptNode=!1,this.audioSyncVideoOption={diff:null},this._prevVolume=null,this.off(),this.player.debug.log("AudioContext","destroy")}updateAudioInfo(e){e.encTypeCode&&(this.audioInfo.encType=N[e.encTypeCode],this.audioInfo.encTypeCode=e.encTypeCode),e.channels&&(this.audioInfo.channels=e.channels),e.sampleRate&&(this.audioInfo.sampleRate=e.sampleRate),this.audioInfo.sampleRate&&this.audioInfo.channels&&this.audioInfo.encType&&!this.init&&(this.player.emit(F.audioInfo,this.audioInfo),this.init=!0)}get isPlaying(){return this.playing}get isMute(){return 0===this.gainNode.gain.value}get volume(){return this.gainNode.gain.value}get bufferSize(){return this.bufferList.length}initScriptNode(){if(this.playing=!0,this.hasInitScriptNode)return;const e=this.audioInfo.channels,t=this.audioContext.createScriptProcessor(1024,0,e);t.onaudioprocess=t=>{const i=t.outputBuffer;if(this.bufferList.length&&this.playing){if(!this.player._opt.useWCS&&!this.player._opt.useMSE&&this.player._opt.wasmDecodeAudioSyncVideo){if(this.audioSyncVideoOption.diff>re)return void this.player.debug.warn("AudioContext",`audioSyncVideoOption more than diff :${this.audioSyncVideoOption.diff}, waiting`);if(this.audioSyncVideoOption.diff<-1e3){this.player.debug.warn("AudioContext",`audioSyncVideoOption less than diff :${this.audioSyncVideoOption.diff}, dropping`);let e=this.bufferList.shift();for(;e.ts-this.player.videoTimestamp<-1e3&&this.bufferList.length>0;)e=this.bufferList.shift();if(0===this.bufferList.length)return}}if(0===this.bufferList.length)return;const t=this.bufferList.shift();t&&t.ts&&(this.player.audioTimestamp=t.ts);for(let o=0;o0?this.player.emit(F.mute,!1):this._prevVolume>0&&0===e&&this.player.emit(F.mute,!0),this.gainNode.gain.value=e,this.gainNode.gain.setValueAtTime(e,this.audioContext.currentTime),this.player.emit(F.volumechange,this.player.volume),this.player.emit(F.volume,this.player.volume),this._prevVolume=e)}closeAudio(){this.hasInitScriptNode&&(this.scriptNode&&this.scriptNode.disconnect(this.gainNode),this.gainNode&&this.gainNode.disconnect(this.audioContext.destination),this.gainNode&&this.gainNode.disconnect(this.mediaStreamAudioDestinationNode)),this.clear()}audioEnabled(e){e?"suspended"===this.audioContext.state&&this.audioContext.resume():"running"===this.audioContext.state&&this.audioContext.suspend()}isStateRunning(){return"running"===this.audioContext.state}isStateSuspended(){return"suspended"===this.audioContext.state}clear(){this.bufferList=[]}play(e,t){this.isMute||(this.hasAudio=!0,this.bufferList.push({buffer:e,ts:t}),this.bufferList.length>20&&(this.player.debug.warn("AudioContext",`bufferList is large: ${this.bufferList.length}`),this.bufferList.length>50&&this.bufferList.shift()))}pause(){this.audioSyncVideoOption={diff:null},this.playing=!1,this.clear()}resume(){this.playing=!0}getLastVolume(){return this._prevVolume}}class qe{constructor(e){return new(qe.getLoaderFactory())(e)}static getLoaderFactory(){return Xe}}class Ze extends Ve{constructor(e){super(),this.player=e,this.playing=!1,this.abortController=new AbortController,this.streamRate=ke(t=>{e.emit(F.kBps,(t/1e3).toFixed(2))}),e.debug.log("FetchStream","init")}async destroy(){this.abort(),this.off(),this.streamRate=null,this.player.debug.log("FetchStream","destroy")}fetchStream(e,t={}){const{demux:i}=this.player;this.player.debug.log("FetchStream","fetchStream",e,JSON.stringify(t)),this.player._times.streamStart=Se(),this.abortController||(this.abortController=new AbortController);const o=Object.assign({signal:this.abortController.signal},{headers:t.headers||{}});fetch(e,o).then(e=>{if(Oe(function(e){return e.ok&&e.status>=200&&e.status<=299}(e)))return this.player.debug.log("FetchStream",`fetch response status is ${e.status} and ok is ${e.ok} and emit error and next abort()`),this.abort(),this.emit(O.fetchError,`fetch response status is ${e.status} and ok is ${e.ok}`),void this.player.emit(F.error,O.fetchError);const t=e.body.getReader();this.emit(F.streamSuccess);const o=()=>{t.read().then(({done:e,value:t})=>{e?i.close():(this.streamRate&&this.streamRate(8*t.byteLength),i.dispatch(t),o())}).catch(e=>{i.close();const t=e.toString();-1===t.indexOf(ce)&&-1===t.indexOf(le)&&e.name!==ue&&(this.abort(),this.emit(O.fetchError,e),this.player.emit(F.error,O.fetchError))})};o()}).catch(e=>{"AbortError"!==e.name&&(i.close(),this.abort(),this.emit(O.fetchError,e),this.player.emit(F.error,O.fetchError))})}abort(){this.abortController&&(this.abortController.abort(),this.abortController=null)}}class Ke extends Ve{constructor(e){super(),this.player=e,this.socket=null,this.socketStatus=V,this.wsUrl=null,this.streamRate=ke(t=>{e.emit(F.kBps,(t/1e3).toFixed(2))}),e.debug.log("WebsocketLoader","init")}async destroy(){this.socket&&(this.socket.close(1e3,"Client disconnecting"),this.socket=null),this.socketStatus=V,this.streamRate=null,this.wsUrl=null,this.off(),this.player.debug.log("websocketLoader","destroy")}_createWebSocket(e={}){const t=this.player,{debug:i,events:{proxy:o},demux:r}=t,s=e.protocols||[];this.socket=new WebSocket(this.wsUrl,s),this.socket.binaryType="arraybuffer",o(this.socket,"open",()=>{this.emit(F.streamSuccess),i.log("websocketLoader","socket open"),this.socketStatus=U}),o(this.socket,"message",e=>{this.streamRate&&this.streamRate(8*e.data.byteLength),this._handleMessage(e.data)}),o(this.socket,"close",()=>{i.log("websocketLoader","socket close"),this.emit(F.streamEnd),this.socketStatus=Q}),o(this.socket,"error",e=>{i.log("websocketLoader","socket error"),this.emit(O.websocketError,e),this.player.emit(F.error,O.websocketError),this.socketStatus=W,r.close(),i.log("websocketLoader","socket error:",e)})}_handleMessage(e){const{demux:t}=this.player;t?t.dispatch(e):this.player.debug.warn("websocketLoader","websocket handle message demux is null")}fetchStream(e,t){this.player._times.streamStart=Se(),this.wsUrl=e,this._createWebSocket(t)}}class _e{constructor(e){return new(_e.getLoaderFactory(e._opt.protocol))(e)}static getLoaderFactory(e){return e===A?Ze:e===n?Ke:void 0}}var $e=i(function(t){function i(e,t){if(!e)throw"First parameter is required.";t=new o(e,t=t||{type:"video"});var s=this;function a(i){i&&(t.initCallback=function(){i(),i=t.initCallback=null});var o=new r(e,t);(h=new o(e,t)).record(),u("recording"),t.disableLogs||console.log("Initialized recorderType:",h.constructor.name,"for output-type:",t.type)}function n(e){if(e=e||function(){},h){if("paused"===s.state)return s.resumeRecording(),void setTimeout(function(){n(e)},1);"recording"===s.state||t.disableLogs||console.warn('Recording state should be: "recording", however current state is: ',s.state),t.disableLogs||console.log("Stopped recording "+t.type+" stream."),"gif"!==t.type?h.stop(i):(h.stop(),i()),u("stopped")}else g();function i(i){if(h){Object.keys(h).forEach(function(e){"function"!=typeof h[e]&&(s[e]=h[e])});var o=h.blob;if(!o){if(!i)throw"Recording failed.";h.blob=o=i}if(o&&!t.disableLogs&&console.log(o.type,"->",b(o.size)),e){var r;try{r=l.createObjectURL(o)}catch(e){}"function"==typeof e.call?e.call(s,r):e(r)}t.autoWriteToDisk&&d(function(e){var i={};i[t.type+"Blob"]=e,x.Store(i)})}else"function"==typeof e.call?e.call(s,""):e("")}}function A(e){postMessage((new FileReaderSync).readAsDataURL(e))}function d(e,i){if(!e)throw"Pass a callback function over getDataURL.";var o=i?i.blob:(h||{}).blob;if(!o)return t.disableLogs||console.warn("Blob encoder did not finish its job yet."),void setTimeout(function(){d(e,i)},1e3);if("undefined"==typeof Worker||navigator.mozGetUserMedia){var r=new FileReader;r.readAsDataURL(o),r.onload=function(t){e(t.target.result)}}else{var s=function(e){try{var t=l.createObjectURL(new Blob([e.toString(),"this.onmessage = function (eee) {"+e.name+"(eee.data);}"],{type:"application/javascript"})),i=new Worker(t);return l.revokeObjectURL(t),i}catch(e){}}(A);s.onmessage=function(t){e(t.data)},s.postMessage(o)}}function c(e){e=e||0,"paused"!==s.state?"stopped"!==s.state&&(e>=s.recordingDuration?n(s.onRecordingStopped):(e+=1e3,setTimeout(function(){c(e)},1e3))):setTimeout(function(){c(e)},1e3)}function u(e){s&&(s.state=e,"function"==typeof s.onStateChanged.call?s.onStateChanged.call(s,e):s.onStateChanged(e))}var h,p='It seems that recorder is destroyed or "startRecording" is not invoked for '+t.type+" recorder.";function g(){!0!==t.disableLogs&&console.warn(p)}var m={startRecording:function(i){return t.disableLogs||console.log("RecordRTC version: ",s.version),i&&(t=new o(e,i)),t.disableLogs||console.log("started recording "+t.type+" stream."),h?(h.clearRecordedData(),h.record(),u("recording"),s.recordingDuration&&c(),s):(a(function(){s.recordingDuration&&c()}),s)},stopRecording:n,pauseRecording:function(){h?"recording"===s.state?(u("paused"),h.pause(),t.disableLogs||console.log("Paused recording.")):t.disableLogs||console.warn("Unable to pause the recording. Recording state: ",s.state):g()},resumeRecording:function(){h?"paused"===s.state?(u("recording"),h.resume(),t.disableLogs||console.log("Resumed recording.")):t.disableLogs||console.warn("Unable to resume the recording. Recording state: ",s.state):g()},initRecorder:a,setRecordingDuration:function(e,t){if(void 0===e)throw"recordingDuration is required.";if("number"!=typeof e)throw"recordingDuration must be a number.";return s.recordingDuration=e,s.onRecordingStopped=t||function(){},{onRecordingStopped:function(e){s.onRecordingStopped=e}}},clearRecordedData:function(){h?(h.clearRecordedData(),t.disableLogs||console.log("Cleared old recorded data.")):g()},getBlob:function(){if(h)return h.blob;g()},getDataURL:d,toURL:function(){if(h)return l.createObjectURL(h.blob);g()},getInternalRecorder:function(){return h},save:function(e){h?y(h.blob,e):g()},getFromDisk:function(e){h?i.getFromDisk(t.type,e):g()},setAdvertisementArray:function(e){t.advertisement=[];for(var i=e.length,o=0;o-1&&"netscape"in window&&/ rv:/.test(navigator.userAgent),g=!h&&!u&&!!navigator.webkitGetUserMedia||v()||-1!==navigator.userAgent.toLowerCase().indexOf("chrome/"),m=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);m&&!g&&-1!==navigator.userAgent.indexOf("CriOS")&&(m=!1,g=!0);var f=window.MediaStream;function b(e){if(0===e)return"0 Bytes";var t=parseInt(Math.floor(Math.log(e)/Math.log(1e3)),10);return(e/Math.pow(1e3,t)).toPrecision(3)+" "+["Bytes","KB","MB","GB","TB"][t]}function y(e,t){if(!e)throw"Blob object is required.";if(!e.type)try{e.type="video/webm"}catch(e){}var i=(e.type||"video/webm").split("/")[1];if(-1!==i.indexOf(";")&&(i=i.split(";")[0]),t&&-1!==t.indexOf(".")){var o=t.split(".");t=o[0],i=o[1]}var r=(t||Math.round(9999999999*Math.random())+888888888)+"."+i;if(void 0!==navigator.msSaveOrOpenBlob)return navigator.msSaveOrOpenBlob(e,r);if(void 0!==navigator.msSaveBlob)return navigator.msSaveBlob(e,r);var s=document.createElement("a");s.href=l.createObjectURL(e),s.download=r,s.style="display:none;opacity:0;color:transparent;",(document.body||document.documentElement).appendChild(s),"function"==typeof s.click?s.click():(s.target="_blank",s.dispatchEvent(new MouseEvent("click",{view:window,bubbles:!0,cancelable:!0}))),l.revokeObjectURL(s.href)}function v(){return"undefined"!=typeof window&&"object"==typeof window.process&&"renderer"===window.process.type||(!("undefined"==typeof process||"object"!=typeof process.versions||!process.versions.electron)||"object"==typeof navigator&&"string"==typeof navigator.userAgent&&navigator.userAgent.indexOf("Electron")>=0)}function w(e,t){return e&&e.getTracks?e.getTracks().filter(function(e){return e.kind===(t||"audio")}):[]}function S(e,t){"srcObject"in t?t.srcObject=e:"mozSrcObject"in t?t.mozSrcObject=e:t.srcObject=e}void 0===f&&"undefined"!=typeof webkitMediaStream&&(f=webkitMediaStream),void 0!==f&&void 0===f.prototype.stop&&(f.prototype.stop=function(){this.getTracks().forEach(function(e){e.stop()})}),void 0!==i&&(i.invokeSaveAsDialog=y,i.getTracks=w,i.getSeekableBlob=function(e,t){if("undefined"==typeof EBML)throw new Error("Please link: https://www.webrtc-experiment.com/EBML.js");var i=new EBML.Reader,o=new EBML.Decoder,r=EBML.tools,s=new FileReader;s.onload=function(e){o.decode(this.result).forEach(function(e){i.read(e)}),i.stop();var s=r.makeMetadataSeekable(i.metadatas,i.duration,i.cues),a=this.result.slice(i.metadataSize),n=new Blob([s,a],{type:"video/webm"});t(n)},s.readAsArrayBuffer(e)},i.bytesToSize=b,i.isElectron=v);var E={};function B(){if(p||m||u)return!0;var e,t,i=navigator.userAgent,o=""+parseFloat(navigator.appVersion),r=parseInt(navigator.appVersion,10);return(g||h)&&(e=i.indexOf("Chrome"),o=i.substring(e+7)),-1!==(t=o.indexOf(";"))&&(o=o.substring(0,t)),-1!==(t=o.indexOf(" "))&&(o=o.substring(0,t)),r=parseInt(""+o,10),isNaN(r)&&(o=""+parseFloat(navigator.appVersion),r=parseInt(navigator.appVersion,10)),r>=49}function C(e,t){var i=this;if(void 0===e)throw'First argument "MediaStream" is required.';if("undefined"==typeof MediaRecorder)throw"Your browser does not support the Media Recorder API. Please try other modules e.g. WhammyRecorder or StereoAudioRecorder.";if("audio"===(t=t||{mimeType:"video/webm"}).type){var o;if(w(e,"video").length&&w(e,"audio").length)navigator.mozGetUserMedia?(o=new f).addTrack(w(e,"audio")[0]):o=new f(w(e,"audio")),e=o;t.mimeType&&-1!==t.mimeType.toString().toLowerCase().indexOf("audio")||(t.mimeType=g?"audio/webm":"audio/ogg"),t.mimeType&&"audio/ogg"!==t.mimeType.toString().toLowerCase()&&navigator.mozGetUserMedia&&(t.mimeType="audio/ogg")}var r,s=[];function a(){i.timestamps.push((new Date).getTime()),"function"==typeof t.onTimeStamp&&t.onTimeStamp(i.timestamps[i.timestamps.length-1],i.timestamps)}function n(e){return r&&r.mimeType?r.mimeType:e.mimeType||"video/webm"}function A(){s=[],r=null,i.timestamps=[]}this.getArrayOfBlobs=function(){return s},this.record=function(){i.blob=null,i.clearRecordedData(),i.timestamps=[],d=[],s=[];var o=t;t.disableLogs||console.log("Passing following config over MediaRecorder API.",o),r&&(r=null),g&&!B()&&(o="video/vp8"),"function"==typeof MediaRecorder.isTypeSupported&&o.mimeType&&(MediaRecorder.isTypeSupported(o.mimeType)||(t.disableLogs||console.warn("MediaRecorder API seems unable to record mimeType:",o.mimeType),o.mimeType="audio"===t.type?"audio/webm":"video/webm"));try{r=new MediaRecorder(e,o),t.mimeType=o.mimeType}catch(t){r=new MediaRecorder(e)}o.mimeType&&!MediaRecorder.isTypeSupported&&"canRecordMimeType"in r&&!1===r.canRecordMimeType(o.mimeType)&&(t.disableLogs||console.warn("MediaRecorder API seems unable to record mimeType:",o.mimeType)),r.ondataavailable=function(e){if(e.data&&d.push("ondataavailable: "+b(e.data.size)),"number"!=typeof t.timeSlice)!e.data||!e.data.size||e.data.size<100||i.blob?i.recordingCallback&&(i.recordingCallback(new Blob([],{type:n(o)})),i.recordingCallback=null):(i.blob=t.getNativeBlob?e.data:new Blob([e.data],{type:n(o)}),i.recordingCallback&&(i.recordingCallback(i.blob),i.recordingCallback=null));else if(e.data&&e.data.size&&(s.push(e.data),a(),"function"==typeof t.ondataavailable)){var r=t.getNativeBlob?e.data:new Blob([e.data],{type:n(o)});t.ondataavailable(r)}},r.onstart=function(){d.push("started")},r.onpause=function(){d.push("paused")},r.onresume=function(){d.push("resumed")},r.onstop=function(){d.push("stopped")},r.onerror=function(e){e&&(e.name||(e.name="UnknownError"),d.push("error: "+e),t.disableLogs||(-1!==e.name.toString().toLowerCase().indexOf("invalidstate")?console.error("The MediaRecorder is not in a state in which the proposed operation is allowed to be executed.",e):-1!==e.name.toString().toLowerCase().indexOf("notsupported")?console.error("MIME type (",o.mimeType,") is not supported.",e):-1!==e.name.toString().toLowerCase().indexOf("security")?console.error("MediaRecorder security error",e):"OutOfMemory"===e.name?console.error("The UA has exhaused the available memory. User agents SHOULD provide as much additional information as possible in the message attribute.",e):"IllegalStreamModification"===e.name?console.error("A modification to the stream has occurred that makes it impossible to continue recording. An example would be the addition of a Track while recording is occurring. User agents SHOULD provide as much additional information as possible in the message attribute.",e):"OtherRecordingError"===e.name?console.error("Used for an fatal error other than those listed above. User agents SHOULD provide as much additional information as possible in the message attribute.",e):"GenericError"===e.name?console.error("The UA cannot provide the codec or recording option that has been requested.",e):console.error("MediaRecorder Error",e)),function(){if(!i.manuallyStopped&&r&&"inactive"===r.state)return delete t.timeslice,void r.start(6e5);setTimeout(void 0,1e3)}(),"inactive"!==r.state&&"stopped"!==r.state&&r.stop())},"number"==typeof t.timeSlice?(a(),r.start(t.timeSlice)):r.start(36e5),t.initCallback&&t.initCallback()},this.timestamps=[],this.stop=function(e){e=e||function(){},i.manuallyStopped=!0,r&&(this.recordingCallback=e,"recording"===r.state&&r.stop(),"number"==typeof t.timeSlice&&setTimeout(function(){i.blob=new Blob(s,{type:n(t)}),i.recordingCallback(i.blob)},100))},this.pause=function(){r&&"recording"===r.state&&r.pause()},this.resume=function(){r&&"paused"===r.state&&r.resume()},this.clearRecordedData=function(){r&&"recording"===r.state&&i.stop(A),A()},this.getInternalRecorder=function(){return r},this.blob=null,this.getState=function(){return r&&r.state||"inactive"};var d=[];this.getAllStates=function(){return d},void 0===t.checkForInactiveTracks&&(t.checkForInactiveTracks=!1);i=this;!function o(){if(r&&!1!==t.checkForInactiveTracks)return!1===function(){if("active"in e){if(!e.active)return!1}else if("ended"in e&&e.ended)return!1;return!0}()?(t.disableLogs||console.log("MediaStream seems stopped."),void i.stop()):void setTimeout(o,1e3)}(),this.name="MediaStreamRecorder",this.toString=function(){return this.name}}function R(e,t){if(!w(e,"audio").length)throw"Your stream has no audio tracks.";var o,r=this,s=[],a=[],n=!1,A=0,d=2,c=(t=t||{}).desiredSampRate;function u(){if(!1===t.checkForInactiveTracks)return!0;if("active"in e){if(!e.active)return!1}else if("ended"in e&&e.ended)return!1;return!0}function h(e,t){function i(e,t){var i,o=e.numberOfAudioChannels,r=e.leftBuffers.slice(0),s=e.rightBuffers.slice(0),a=e.sampleRate,n=e.internalInterleavedLength,A=e.desiredSampRate;function d(e,t,i){var o=Math.round(e.length*(t/i)),r=[],s=Number((e.length-1)/(o-1));r[0]=e[0];for(var a=1;a96e3)&&(t.disableLogs||console.log("sample-rate must be under range 22050 and 96000.")),t.disableLogs||t.desiredSampRate&&console.log("Desired sample-rate: "+t.desiredSampRate);var y=!1;function v(){s=[],a=[],A=0,E=!1,n=!1,y=!1,p=null,r.leftchannel=s,r.rightchannel=a,r.numberOfAudioChannels=d,r.desiredSampRate=c,r.sampleRate=b,r.recordingLength=A,B={left:[],right:[],recordingLength:0}}function S(){o&&(o.onaudioprocess=null,o.disconnect(),o=null),g&&(g.disconnect(),g=null),v()}this.pause=function(){y=!0},this.resume=function(){if(!1===u())throw"Please make sure MediaStream is active.";if(!n)return t.disableLogs||console.log("Seems recording has been restarted."),void this.record();y=!1},this.clearRecordedData=function(){t.checkForInactiveTracks=!1,n&&this.stop(S),S()},this.name="StereoAudioRecorder",this.toString=function(){return this.name};var E=!1;o.onaudioprocess=function(e){if(!y)if(!1===u()&&(t.disableLogs||console.log("MediaStream seems stopped."),o.disconnect(),n=!1),n){E||(E=!0,t.onAudioProcessStarted&&t.onAudioProcessStarted(),t.initCallback&&t.initCallback());var i=e.inputBuffer.getChannelData(0),c=new Float32Array(i);if(s.push(c),2===d){var l=e.inputBuffer.getChannelData(1),h=new Float32Array(l);a.push(h)}A+=f,r.recordingLength=A,void 0!==t.timeSlice&&(B.recordingLength+=f,B.left.push(c),2===d&&B.right.push(h))}else g&&(g.disconnect(),g=null)},p.createMediaStreamDestination?o.connect(p.createMediaStreamDestination()):o.connect(p.destination),this.leftchannel=s,this.rightchannel=a,this.numberOfAudioChannels=d,this.desiredSampRate=c,this.sampleRate=b,r.recordingLength=A;var B={left:[],right:[],recordingLength:0};function C(){n&&"function"==typeof t.ondataavailable&&void 0!==t.timeSlice&&(B.left.length?(h({desiredSampRate:c,sampleRate:b,numberOfAudioChannels:d,internalInterleavedLength:B.recordingLength,leftBuffers:B.left,rightBuffers:1===d?[]:B.right},function(e,i){var o=new Blob([i],{type:"audio/wav"});t.ondataavailable(o),setTimeout(C,t.timeSlice)}),B={left:[],right:[],recordingLength:0}):setTimeout(C,t.timeSlice))}}function k(e,t){if("undefined"==typeof html2canvas)throw"Please link: https://www.webrtc-experiment.com/screenshot.js";(t=t||{}).frameInterval||(t.frameInterval=10);var i=!1;["captureStream","mozCaptureStream","webkitCaptureStream"].forEach(function(e){e in document.createElement("canvas")&&(i=!0)});var o,r,s,a=!(!window.webkitRTCPeerConnection&&!window.webkitGetUserMedia||!window.chrome),n=50,A=navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);if(a&&A&&A[2]&&(n=parseInt(A[2],10)),a&&n<52&&(i=!1),t.useWhammyRecorder&&(i=!1),i)if(t.disableLogs||console.log("Your browser supports both MediRecorder API and canvas.captureStream!"),e instanceof HTMLCanvasElement)o=e;else{if(!(e instanceof CanvasRenderingContext2D))throw"Please pass either HTMLCanvasElement or CanvasRenderingContext2D.";o=e.canvas}else navigator.mozGetUserMedia&&(t.disableLogs||console.error("Canvas recording is NOT supported in Firefox."));this.record=function(){if(s=!0,i&&!t.useWhammyRecorder){var e;"captureStream"in o?e=o.captureStream(25):"mozCaptureStream"in o?e=o.mozCaptureStream(25):"webkitCaptureStream"in o&&(e=o.webkitCaptureStream(25));try{var a=new f;a.addTrack(w(e,"video")[0]),e=a}catch(e){}if(!e)throw"captureStream API are NOT available.";(r=new C(e,{mimeType:t.mimeType||"video/webm"})).record()}else h.frames=[],u=(new Date).getTime(),l();t.initCallback&&t.initCallback()},this.getWebPImages=function(i){if("canvas"===e.nodeName.toLowerCase()){var o=h.frames.length;h.frames.forEach(function(e,i){var r=o-i;t.disableLogs||console.log(r+"/"+o+" frames remaining"),t.onEncodingCallback&&t.onEncodingCallback(r,o);var s=e.image.toDataURL("image/webp",1);h.frames[i].image=s}),t.disableLogs||console.log("Generating WebM"),i()}else i()},this.stop=function(e){s=!1;var o=this;i&&r?r.stop(e):this.getWebPImages(function(){h.compile(function(i){t.disableLogs||console.log("Recording finished!"),o.blob=i,o.blob.forEach&&(o.blob=new Blob([],{type:"video/webm"})),e&&e(o.blob),h.frames=[]})})};var d=!1;function c(){h.frames=[],s=!1,d=!1}function l(){if(d)return u=(new Date).getTime(),setTimeout(l,500);if("canvas"===e.nodeName.toLowerCase()){var i=(new Date).getTime()-u;return u=(new Date).getTime(),h.frames.push({image:(o=document.createElement("canvas"),r=o.getContext("2d"),o.width=e.width,o.height=e.height,r.drawImage(e,0,0),o),duration:i}),void(s&&setTimeout(l,t.frameInterval))}var o,r;html2canvas(e,{grabMouse:void 0===t.showMousePointer||t.showMousePointer,onrendered:function(e){var i=(new Date).getTime()-u;if(!i)return setTimeout(l,t.frameInterval);u=(new Date).getTime(),h.frames.push({image:e.toDataURL("image/webp",1),duration:i}),s&&setTimeout(l,t.frameInterval)}})}this.pause=function(){d=!0,r instanceof C&&r.pause()},this.resume=function(){d=!1,r instanceof C?r.resume():s||this.record()},this.clearRecordedData=function(){s&&this.stop(c),c()},this.name="CanvasRecorder",this.toString=function(){return this.name};var u=(new Date).getTime(),h=new I.Video(100)}function T(e,t){function i(e){e=void 0!==e?e:10;var t=(new Date).getTime()-A;return t?s?(A=(new Date).getTime(),setTimeout(i,100)):(A=(new Date).getTime(),n.paused&&n.play(),l.drawImage(n,0,0,c.width,c.height),d.frames.push({duration:t,image:c.toDataURL("image/webp")}),void(r||setTimeout(i,e,e))):setTimeout(i,e,e)}function o(e,t,i,o,r){var s=document.createElement("canvas");s.width=c.width,s.height=c.height;var a,n,A,d=s.getContext("2d"),l=[],u=-1===t,h=t&&t>0&&t<=e.length?t:e.length,p=0,g=0,m=0,f=Math.sqrt(Math.pow(255,2)+Math.pow(255,2)+Math.pow(255,2)),b=i&&i>=0&&i<=1?i:0,y=o&&o>=0&&o<=1?o:0,v=!1;n=-1,A=(a={length:h,functionToLoop:function(t,i){var o,r,s,a=function(){!v&&s-o<=s*y||(u&&(v=!0),l.push(e[i])),t()};if(v)a();else{var n=new Image;n.onload=function(){d.drawImage(n,0,0,c.width,c.height);var e=d.getImageData(0,0,c.width,c.height);o=0,r=e.data.length,s=e.data.length/4;for(var t=0;t127)throw"TrackNumber > 127 not supported";return[128|e.trackNum,e.timecode>>8,255&e.timecode,t].map(function(e){return String.fromCharCode(e)}).join("")+e.frame}({discardable:0,frame:e.data.slice(4),invisible:0,keyframe:1,lacing:0,trackNum:1,timecode:Math.round(t)});return t+=e.duration,{data:i,id:163}}))}function i(e){for(var t=[];e>0;)t.push(255&e),e>>=8;return new Uint8Array(t.reverse())}function o(e){return new Uint8Array(e.split("").map(function(e){return e.charCodeAt(0)}))}function r(e){var t=[];e=(e.length%8?new Array(9-e.length%8).join("0"):"")+e;for(var i=0;i1?2*i[0].width:i[0].width;var o=1;3!==e&&4!==e||(o=2),5!==e&&6!==e||(o=3),7!==e&&8!==e||(o=4),9!==e&&10!==e||(o=5),a.height=i[0].height*o}else a.width=A.width||360,a.height=A.height||240;t&&t instanceof HTMLVideoElement&&p(t),i.forEach(function(e,t){p(e,t)}),setTimeout(h,A.frameInterval)}}function p(e,t){if(!s){var i=0,o=0,r=e.width,a=e.height;1===t&&(i=e.width),2===t&&(o=e.height),3===t&&(i=e.width,o=e.height),4===t&&(o=2*e.height),5===t&&(i=e.width,o=2*e.height),6===t&&(o=3*e.height),7===t&&(i=e.width,o=3*e.height),void 0!==e.stream.left&&(i=e.stream.left),void 0!==e.stream.top&&(o=e.stream.top),void 0!==e.stream.width&&(r=e.stream.width),void 0!==e.stream.height&&(a=e.stream.height),n.drawImage(e,i,o,r,a),"function"==typeof e.stream.onRender&&e.stream.onRender(n,i,o,r,a,t)}}function g(e){var t=document.createElement("video");return function(e,t){"srcObject"in t?t.srcObject=e:"mozSrcObject"in t?t.mozSrcObject=e:t.srcObject=e}(e,t),t.className=o,t.muted=!0,t.volume=0,t.width=e.width||A.width||360,t.height=e.height||A.height||240,t.play(),t}function m(e){r=[],(e=e||t).forEach(function(e){if(e.getTracks().filter(function(e){return"video"===e.kind}).length){var t=g(e);t.stream=e,r.push(t)}})}void 0!==d?u.AudioContext=d:"undefined"!=typeof webkitAudioContext&&(u.AudioContext=webkitAudioContext),this.startDrawingFrames=function(){h()},this.appendStreams=function(e){if(!e)throw"First parameter is required.";e instanceof Array||(e=[e]),e.forEach(function(e){var i=new l;if(e.getTracks().filter(function(e){return"video"===e.kind}).length){var o=g(e);o.stream=e,r.push(o),i.addTrack(e.getTracks().filter(function(e){return"video"===e.kind})[0])}if(e.getTracks().filter(function(e){return"audio"===e.kind}).length){var s=A.audioContext.createMediaStreamSource(e);A.audioDestination=A.audioContext.createMediaStreamDestination(),s.connect(A.audioDestination),i.addTrack(A.audioDestination.stream.getTracks().filter(function(e){return"audio"===e.kind})[0])}t.push(i)})},this.releaseStreams=function(){r=[],s=!0,A.gainNode&&(A.gainNode.disconnect(),A.gainNode=null),A.audioSources.length&&(A.audioSources.forEach(function(e){e.disconnect()}),A.audioSources=[]),A.audioDestination&&(A.audioDestination.disconnect(),A.audioDestination=null),A.audioContext&&A.audioContext.close(),A.audioContext=null,n.clearRect(0,0,a.width,a.height),a.stream&&(a.stream.stop(),a.stream=null)},this.resetVideoStreams=function(e){!e||e instanceof Array||(e=[e]),m(e)},this.name="MultiStreamsMixer",this.toString=function(){return this.name},this.getMixedStream=function(){s=!1;var e=function(){var e;m(),"captureStream"in a?e=a.captureStream():"mozCaptureStream"in a?e=a.mozCaptureStream():A.disableLogs||console.error("Upgrade to latest Chrome or otherwise enable this flag: chrome://flags/#enable-experimental-web-platform-features");var t=new l;return e.getTracks().filter(function(e){return"video"===e.kind}).forEach(function(e){t.addTrack(e)}),a.stream=t,t}(),i=function(){u.AudioContextConstructor||(u.AudioContextConstructor=new u.AudioContext);A.audioContext=u.AudioContextConstructor,A.audioSources=[],!0===A.useGainNode&&(A.gainNode=A.audioContext.createGain(),A.gainNode.connect(A.audioContext.destination),A.gainNode.gain.value=0);var e=0;if(t.forEach(function(t){if(t.getTracks().filter(function(e){return"audio"===e.kind}).length){e++;var i=A.audioContext.createMediaStreamSource(t);!0===A.useGainNode&&i.connect(A.gainNode),A.audioSources.push(i)}}),!e)return;return A.audioDestination=A.audioContext.createMediaStreamDestination(),A.audioSources.forEach(function(e){e.connect(A.audioDestination)}),A.audioDestination.stream}();return i&&i.getTracks().filter(function(e){return"audio"===e.kind}).forEach(function(t){e.addTrack(t)}),t.forEach(function(e){e.fullcanvas}),e}}function L(e,t){e=e||[];var i,o,r=this;(t=t||{elementClass:"multi-streams-mixer",mimeType:"video/webm",video:{width:360,height:240}}).frameInterval||(t.frameInterval=10),t.video||(t.video={}),t.video.width||(t.video.width=360),t.video.height||(t.video.height=240),this.record=function(){var r;i=new j(e,t.elementClass||"multi-streams-mixer"),(r=[],e.forEach(function(e){w(e,"video").forEach(function(e){r.push(e)})}),r).length&&(i.frameInterval=t.frameInterval||10,i.width=t.video.width||360,i.height=t.video.height||240,i.startDrawingFrames()),t.previewStream&&"function"==typeof t.previewStream&&t.previewStream(i.getMixedStream()),(o=new C(i.getMixedStream(),t)).record()},this.stop=function(e){o&&o.stop(function(t){r.blob=t,e(t),r.clearRecordedData()})},this.pause=function(){o&&o.pause()},this.resume=function(){o&&o.resume()},this.clearRecordedData=function(){o&&(o.clearRecordedData(),o=null),i&&(i.releaseStreams(),i=null)},this.addStreams=function(r){if(!r)throw"First parameter is required.";r instanceof Array||(r=[r]),e.concat(r),o&&i&&(i.appendStreams(r),t.previewStream&&"function"==typeof t.previewStream&&t.previewStream(i.getMixedStream()))},this.resetVideoStreams=function(e){i&&(!e||e instanceof Array||(e=[e]),i.resetVideoStreams(e))},this.getMixer=function(){return i},this.name="MultiStreamRecorder",this.toString=function(){return this.name}}function F(e,t){var i,o,r;function s(){return new ReadableStream({start:function(o){var r=document.createElement("canvas"),s=document.createElement("video"),a=!0;s.srcObject=e,s.muted=!0,s.height=t.height,s.width=t.width,s.volume=0,s.onplaying=function(){r.width=t.width,r.height=t.height;var e=r.getContext("2d"),n=1e3/t.frameRate,A=setInterval(function(){if(i&&(clearInterval(A),o.close()),a&&(a=!1,t.onVideoProcessStarted&&t.onVideoProcessStarted()),e.drawImage(s,0,0),"closed"!==o._controlledReadableStream.state)try{o.enqueue(e.getImageData(0,0,t.width,t.height))}catch(e){}},n)},s.play()}})}function a(e,A){if(!t.workerPath&&!A)return i=!1,void fetch("https://unpkg.com/webm-wasm@latest/dist/webm-worker.js").then(function(t){t.arrayBuffer().then(function(t){a(e,t)})});if(!t.workerPath&&A instanceof ArrayBuffer){var d=new Blob([A],{type:"text/javascript"});t.workerPath=l.createObjectURL(d)}t.workerPath||console.error("workerPath parameter is missing."),(o=new Worker(t.workerPath)).postMessage(t.webAssemblyPath||"https://unpkg.com/webm-wasm@latest/dist/webm-wasm.wasm"),o.addEventListener("message",function(e){"READY"===e.data?(o.postMessage({width:t.width,height:t.height,bitrate:t.bitrate||1200,timebaseDen:t.frameRate||30,realtime:t.realtime}),s().pipeTo(new WritableStream({write:function(e){i?console.error("Got image, but recorder is finished!"):o.postMessage(e.data.buffer,[e.data.buffer])}}))):e.data&&(r||n.push(e.data))})}"undefined"!=typeof ReadableStream&&"undefined"!=typeof WritableStream||console.error("Following polyfill is strongly recommended: https://unpkg.com/@mattiasbuelens/web-streams-polyfill/dist/polyfill.min.js"),(t=t||{}).width=t.width||640,t.height=t.height||480,t.frameRate=t.frameRate||30,t.bitrate=t.bitrate||1200,t.realtime=t.realtime||!0,this.record=function(){n=[],r=!1,this.blob=null,a(e),"function"==typeof t.initCallback&&t.initCallback()},this.pause=function(){r=!0},this.resume=function(){r=!1};var n=[];this.stop=function(e){i=!0;var t=this;!function(e){o?(o.addEventListener("message",function(t){null===t.data&&(o.terminate(),o=null,e&&e())}),o.postMessage(null)):e&&e()}(function(){t.blob=new Blob(n,{type:"video/webm"}),e(t.blob)})},this.name="WebAssemblyRecorder",this.toString=function(){return this.name},this.clearRecordedData=function(){n=[],r=!1,this.blob=null},this.blob=null}void 0!==i&&(i.DiskStorage=x),void 0!==i&&(i.GifRecorder=D),void 0===i&&(t.exports=j),void 0!==i&&(i.MultiStreamRecorder=L),void 0!==i&&(i.RecordRTCPromisesHandler=function(e,t){if(!this)throw'Use "new RecordRTCPromisesHandler()"';if(void 0===e)throw'First argument "MediaStream" is required.';var o=this;o.recordRTC=new i(e,t),this.startRecording=function(){return new Promise(function(e,t){try{o.recordRTC.startRecording(),e()}catch(e){t(e)}})},this.stopRecording=function(){return new Promise(function(e,t){try{o.recordRTC.stopRecording(function(i){o.blob=o.recordRTC.getBlob(),o.blob&&o.blob.size?e(i):t("Empty blob.",o.blob)})}catch(e){t(e)}})},this.pauseRecording=function(){return new Promise(function(e,t){try{o.recordRTC.pauseRecording(),e()}catch(e){t(e)}})},this.resumeRecording=function(){return new Promise(function(e,t){try{o.recordRTC.resumeRecording(),e()}catch(e){t(e)}})},this.getDataURL=function(e){return new Promise(function(e,t){try{o.recordRTC.getDataURL(function(t){e(t)})}catch(e){t(e)}})},this.getBlob=function(){return new Promise(function(e,t){try{e(o.recordRTC.getBlob())}catch(e){t(e)}})},this.getInternalRecorder=function(){return new Promise(function(e,t){try{e(o.recordRTC.getInternalRecorder())}catch(e){t(e)}})},this.reset=function(){return new Promise(function(e,t){try{e(o.recordRTC.reset())}catch(e){t(e)}})},this.destroy=function(){return new Promise(function(e,t){try{e(o.recordRTC.destroy())}catch(e){t(e)}})},this.getState=function(){return new Promise(function(e,t){try{e(o.recordRTC.getState())}catch(e){t(e)}})},this.blob=null,this.version="5.6.2"}),void 0!==i&&(i.WebAssemblyRecorder=F)});class et extends Ve{constructor(e){super(),this.player=e,this.fileName="",this.fileType=e._opt.recordType||u,this.isRecording=!1,this.recordingTimestamp=0,this.recordingInterval=null,this.recorder=null,e.debug.log("Recorder","init")}destroy(){this._reset(),this.player.debug.log("Recorder","destroy")}setFileName(e,t){this.fileName=e,l!==t&&u!==t||(this.fileType=t)}get recording(){return this.isRecording}get recordTime(){return this.recordingTimestamp}startRecord(){const e=this.player.debug,t={type:"video",mimeType:"video/webm;codecs=h264",onTimeStamp:t=>{e.log("Recorder","record timestamp :"+t)},disableLogs:!this.player._opt.debug};try{const e=this.player.video.$videoElement.captureStream(25);if(this.player.audio&&this.player.audio.mediaStreamAudioDestinationNode&&this.player.audio.mediaStreamAudioDestinationNode.stream&&!this.player.audio.isStateSuspended()&&this.player.audio.hasAudio&&this.player._opt.hasAudio){const t=this.player.audio.mediaStreamAudioDestinationNode.stream;if(t.getAudioTracks().length>0){const i=t.getAudioTracks()[0];i&&i.enabled&&e.addTrack(i)}}this.recorder=$e(e,t)}catch(t){e.error("Recorder","startRecord error",t),this.emit(F.recordCreateError)}this.recorder&&(this.isRecording=!0,this.player.emit(F.recording,!0),this.recorder.startRecording(),e.log("Recorder","start recording"),this.player.emit(F.recordStart),this.recordingInterval=window.setInterval(()=>{this.recordingTimestamp+=1,this.player.emit(F.recordingTimestamp,this.recordingTimestamp)},1e3))}stopRecordAndSave(){this.recorder&&this.isRecording&&this.recorder.stopRecording(()=>{this.player.debug.log("Recorder","stop recording"),this.player.emit(F.recordEnd);const e=(this.fileName||Se())+"."+(this.fileType||u);Ne(this.recorder.getBlob(),e),this._reset(),this.player.emit(F.recording,!1)})}_reset(){this.isRecording=!1,this.recordingTimestamp=0,this.recorder&&(this.recorder.destroy(),this.recorder=null),this.fileName=null,this.recordingInterval&&clearInterval(this.recordingInterval),this.recordingInterval=null}}class tt{constructor(e){return new(tt.getLoaderFactory())(e)}static getLoaderFactory(){return et}}class it{constructor(e){this.player=e,this.decoderWorker=new Worker(e._opt.decoder),this._initDecoderWorker(),e.debug.log("decoderWorker","init")}async destroy(){this.decoderWorker&&(this.decoderWorker.postMessage({cmd:j}),this.decoderWorker.terminate(),this.decoderWorker=null),this.player.debug.log("decoderWorker","destroy")}_initDecoderWorker(){const{debug:e,events:{proxy:t}}=this.player;this.decoderWorker.onmessage=t=>{const i=t.data;switch(i.cmd){case m:e.log("decoderWorker","onmessage:",m),this.player.loaded||this.player.emit(F.load),this.player.emit(F.decoderWorkerInit),this._initWork();break;case S:e.log("decoderWorker","onmessage:",S,i.code),this.player._times.decodeStart||(this.player._times.decodeStart=Se()),this.player.video.updateVideoInfo({encTypeCode:i.code});break;case w:e.log("decoderWorker","onmessage:",w,i.code),this.player.audio&&this.player.audio.updateAudioInfo({encTypeCode:i.code});break;case f:if(e.log("decoderWorker","onmessage:",f,`width:${i.w},height:${i.h}`),this.player.video.updateVideoInfo({width:i.w,height:i.h}),!this.player._opt.openWebglAlignment&&i.w/2%4!=0)return void this.player.emit(O.webglAlignmentError);this.player.video.initCanvasViewSize();break;case v:e.log("decoderWorker","onmessage:",v,`channels:${i.channels},sampleRate:${i.sampleRate}`),this.player.audio&&(this.player.audio.updateAudioInfo(i),this.player.audio.initScriptNode(i));break;case b:this.player.handleRender(),this.player.video.render(i),this.player.emit(F.timeUpdate,i.ts),this.player.updateStats({fps:!0,ts:i.ts,buf:i.delay}),this.player._times.videoStart||(this.player._times.videoStart=Se(),this.player.handlePlayToRenderTimes());break;case y:this.player.playing&&this.player.audio&&this.player.audio.play(i.buffer,i.ts);break;case E:i.message&&-1!==i.message.indexOf(B)&&this.player.emitError(O.wasmDecodeError);break;default:this.player[i.cmd]&&this.player[i.cmd](i)}}}_initWork(){const e={debug:this.player._opt.debug,useOffscreen:this.player._opt.useOffscreen,useWCS:this.player._opt.useWCS,videoBuffer:this.player._opt.videoBuffer,videoBufferDelay:this.player._opt.videoBufferDelay,openWebglAlignment:this.player._opt.openWebglAlignment};this.decoderWorker.postMessage({cmd:I,opt:JSON.stringify(e),sampleRate:this.player.audio&&this.player.audio.audioContext.sampleRate||0})}decodeVideo(e,t,i){const o={type:R,ts:Math.max(t,0),isIFrame:i};this.decoderWorker.postMessage({cmd:x,buffer:e,options:o},[e.buffer])}decodeAudio(e,t){this.player._opt.useWCS||this.player._opt.useMSE?this._decodeAudioNoDelay(e,t):this._decodeAudio(e,t)}_decodeAudio(e,t){const i={type:C,ts:Math.max(t,0)};this.decoderWorker.postMessage({cmd:x,buffer:e,options:i},[e.buffer])}_decodeAudioNoDelay(e,t){this.decoderWorker.postMessage({cmd:D,buffer:e,ts:Math.max(t,0)},[e.buffer])}updateWorkConfig(e){this.decoderWorker.postMessage({cmd:L,key:e.key,value:e.value})}}class ot extends Ve{constructor(e){super(),this.player=e,this.stopId=null,this.firstTimestamp=null,this.startTimestamp=null,this.delay=-1,this.bufferList=[],this.dropping=!1,this.initInterval()}destroy(){this.stopId&&(clearInterval(this.stopId),this.stopId=null),this.firstTimestamp=null,this.startTimestamp=null,this.delay=-1,this.bufferList=[],this.dropping=!1,this.off(),this.player.debug.log("CommonDemux","destroy")}getDelay(e){if(!e)return-1;if(this.firstTimestamp){if(e){const t=Date.now()-this.startTimestamp,i=e-this.firstTimestamp;this.delay=t>=i?t-i:i-t}}else this.firstTimestamp=e,this.startTimestamp=Date.now(),this.delay=-1;return this.delay}resetDelay(){this.firstTimestamp=null,this.startTimestamp=null,this.delay=-1,this.dropping=!1}initInterval(){this.player.debug.log("common dumex","init Interval");let e=()=>{let e;const t=this.player._opt.videoBuffer,i=this.player._opt.videoBufferDelay;if(!this.player.isDestroyedOrClosed())if(this.player._opt.useMSE&&this.player.mseDecoder&&this.player.mseDecoder.getSourceBufferUpdating())this.player.debug.warn("CommonDemux",`_loop getSourceBufferUpdating is true and bufferList length is ${this.bufferList.length}`);else if(this.bufferList.length)if(this.dropping){for(e=this.bufferList.shift(),e.type===C&&0===e.payload[1]&&this._doDecoderDecode(e);!e.isIFrame&&this.bufferList.length;)e=this.bufferList.shift(),e.type===C&&0===e.payload[1]&&this._doDecoderDecode(e);e.isIFrame&&this.getDelay(e.ts)<=Math.min(t,200)&&(this.dropping=!1,this._doDecoderDecode(e))}else e=this.bufferList[0],-1===this.getDelay(e.ts)?(this.bufferList.shift(),this._doDecoderDecode(e)):this.delay>t+i?(this.resetDelay(),this.dropping=!0):(e=this.bufferList[0],this.getDelay(e.ts)>t&&(this.bufferList.shift(),this._doDecoderDecode(e)))};e(),this.stopId=setInterval(e,10)}_doDecode(e,t,i,o,r){const s=this.player;let a={ts:i,cts:r,type:t,isIFrame:!1};s._opt.useWCS&&!s._opt.useOffscreen||s._opt.useMSE?(t===R&&(a.isIFrame=o),this.pushBuffer(e,a)):t===R?s.decoderWorker&&s.decoderWorker.decodeVideo(e,i,o):t===C&&s._opt.hasAudio&&s.decoderWorker&&s.decoderWorker.decodeAudio(e,i)}_doDecoderDecode(e){const t=this.player,{webcodecsDecoder:i,mseDecoder:o}=t;e.type===C?t._opt.hasAudio&&t.decoderWorker&&t.decoderWorker.decodeAudio(e.payload,e.ts):e.type===R&&(t._opt.useWCS&&!t._opt.useOffscreen?i.decodeVideo(e.payload,e.ts,e.isIFrame):t._opt.useMSE&&o.decodeVideo(e.payload,e.ts,e.isIFrame,e.cts))}pushBuffer(e,t){t.type===C?this.bufferList.push({ts:t.ts,payload:e,type:C}):t.type===R&&this.bufferList.push({ts:t.ts,cts:t.cts,payload:e,type:R,isIFrame:t.isIFrame})}close(){}_decodeEnhancedH265Video(e,t){const i=e[0],o=48&i,r=15&i,s=e.slice(1,5),a=new ArrayBuffer(4),n=new Uint32Array(a),A="a"==String.fromCharCode(s[0]);if(r===he){if(o===me){const t=e.slice(5);if(!A){const e=new Uint8Array(5+t.length);e.set([28,0,0,0,0],0),e.set(t,5),this._doDecode(e,R,0,!0,0)}}}else if(r===pe){let i=e,r=0;const s=o===me;if(!A){n[0]=e[4],n[1]=e[3],n[2]=e[2],n[3]=0,r=n[0];i=Me(e.slice(8),s),this._doDecode(i,R,t,s,r)}}else if(r===ge){const i=o===me;let r=Me(e.slice(5),i);this._doDecode(r,R,t,i,0)}}_isEnhancedH265Header(e){return!(128&~e)}}class rt extends ot{constructor(e){super(e),this.input=this._inputFlv(),this.flvDemux=this.dispatchFlvData(this.input),e.debug.log("FlvDemux","init")}destroy(){super.destroy(),this.input=null,this.flvDemux=null,this.player.debug.log("FlvDemux","destroy")}dispatch(e){this.flvDemux(e)}*_inputFlv(){yield 9;const e=new ArrayBuffer(4),t=new Uint8Array(e),i=new Uint32Array(e),o=this.player;for(;;){t[3]=0;const e=yield 15,r=e[4];t[0]=e[7],t[1]=e[6],t[2]=e[5];const s=i[0];t[0]=e[10],t[1]=e[9],t[2]=e[8];let a=i[0];16777215===a&&(t[3]=e[11],a=i[0]);const n=yield s;switch(r){case k:o._opt.hasAudio&&(o.updateStats({abps:n.byteLength}),n.byteLength>0&&this._doDecode(n,C,a));break;case T:if(o._times.demuxStart||(o._times.demuxStart=Se()),o._opt.hasVideo){o.updateStats({vbps:n.byteLength});const e=n[0];if(this._isEnhancedH265Header(e))this._decodeEnhancedH265Video(n,a);else{const e=n[0]>>4==1;if(n.byteLength>0){i[0]=n[4],i[1]=n[3],i[2]=n[2],i[3]=0;let t=i[0];this._doDecode(n,R,a,e,t)}}}}}}dispatchFlvData(e){let t=e.next(),i=null;return o=>{let r=new Uint8Array(o);if(i){let e=new Uint8Array(i.length+r.length);e.set(i),e.set(r,i.length),r=e,i=null}for(;r.length>=t.value;){let i=r.slice(t.value);t=e.next(r.slice(0,t.value)),r=i}r.length>0&&(i=r)}}close(){this.input&&this.input.return(null)}}class st extends ot{constructor(e){super(e),e.debug.log("M7sDemux","init")}destroy(){super.destroy(),this.player.debug.log("M7sDemux","destroy")}dispatch(e){const t=this.player,i=new DataView(e),o=i.getUint8(0),r=i.getUint32(1,!1),s=new ArrayBuffer(4),a=new Uint32Array(s);switch(o){case C:if(t._opt.hasAudio){const i=new Uint8Array(e,5);t.updateStats({abps:i.byteLength}),i.byteLength>0&&this._doDecode(i,o,r)}break;case R:if(t._opt.hasVideo)if(t._times.demuxStart||(t._times.demuxStart=Se()),i.byteLength>5){const s=new Uint8Array(e,5),n=s[0];if(this._isEnhancedH265Header(n))this._decodeEnhancedH265Video(s,r);else{const e=i.getUint8(5)>>4==1;t.updateStats({vbps:s.byteLength}),a[0]=s[4],a[1]=s[3],a[2]=s[2],a[3]=0;let n=a[0];this._doDecode(s,o,r,e,n)}}else this.player.debug.warn("M7sDemux","dispatch","dv byteLength is",i.byteLength)}}}class at{constructor(e){return new(at.getLoaderFactory(e._opt.demuxType))(e)}static getLoaderFactory(e){return e===c?st:e===d?rt:void 0}}class nt{constructor(e){this.TAG="ExpGolomb",this._buffer=e,this._buffer_index=0,this._total_bytes=e.byteLength,this._total_bits=8*e.byteLength,this._current_word=0,this._current_word_bits_left=0}destroy(){this._buffer=null}_fillCurrentWord(){let e=this._total_bytes-this._buffer_index,t=Math.min(4,e),i=new Uint8Array(4);i.set(this._buffer.subarray(this._buffer_index,this._buffer_index+t)),this._current_word=new DataView(i.buffer).getUint32(0,!1),this._buffer_index+=t,this._current_word_bits_left=8*t}readBits(e){if(e<=this._current_word_bits_left){let t=this._current_word>>>32-e;return this._current_word<<=e,this._current_word_bits_left-=e,t}let t=this._current_word_bits_left?this._current_word:0;t>>>=32-this._current_word_bits_left;let i=e-this._current_word_bits_left;this._fillCurrentWord();let o=Math.min(i,this._current_word_bits_left),r=this._current_word>>>32-o;return this._current_word<<=o,this._current_word_bits_left-=o,t=t<>>e)return this._current_word<<=e,this._current_word_bits_left-=e,e;return this._fillCurrentWord(),e+this._skipLeadingZero()}readUEG(){let e=this._skipLeadingZero();return this.readBits(e+1)-1}readSEG(){let e=this.readUEG();return 1&e?e+1>>>1:-1*(e>>>1)}}class At{static _ebsp2rbsp(e){let t=e,i=t.byteLength,o=new Uint8Array(i),r=0;for(let e=0;e=2&&3===t[e]&&0===t[e-1]&&0===t[e-2]||(o[r]=t[e],r++);return new Uint8Array(o.buffer,0,r)}static parseSPS(e){let t=At._ebsp2rbsp(e),i=new nt(t);i.readByte();let o=i.readByte();i.readByte();let r=i.readByte();i.readUEG();let s=At.getProfileString(o),a=At.getLevelString(r),n=1,A=420,d=[0,420,422,444],c=8;if((100===o||110===o||122===o||244===o||44===o||83===o||86===o||118===o||128===o||138===o||144===o)&&(n=i.readUEG(),3===n&&i.readBits(1),n<=3&&(A=d[n]),c=i.readUEG()+8,i.readUEG(),i.readBits(1),i.readBool())){let e=3!==n?8:12;for(let t=0;t0&&e<16?(v=[1,12,10,16,40,24,20,32,80,18,15,64,160,4,3,2][e-1],w=[1,11,11,11,33,11,11,11,33,11,11,33,99,3,2,1][e-1]):255===e&&(v=i.readByte()<<8|i.readByte(),w=i.readByte()<<8|i.readByte())}if(i.readBool()&&i.readBool(),i.readBool()&&(i.readBits(4),i.readBool()&&i.readBits(24)),i.readBool()&&(i.readUEG(),i.readUEG()),i.readBool()){let e=i.readBits(32),t=i.readBits(32);E=i.readBool(),B=t,C=2*e,S=B/C}}let R=1;1===v&&1===w||(R=v/w);let k=0,T=0;if(0===n)k=1,T=2-g;else{k=3===n?1:2,T=(1===n?2:1)*(2-g)}let I=16*(h+1),x=16*(p+1)*(2-g);I-=(m+f)*k,x-=(b+y)*T;let D=Math.ceil(I*R);return i.destroy(),i=null,{profile_string:s,level_string:a,bit_depth:c,ref_frames:u,chroma_format:A,chroma_format_string:At.getChromaFormatString(A),frame_rate:{fixed:E,fps:S,fps_den:C,fps_num:B},sar_ratio:{width:v,height:w},codec_size:{width:I,height:x},present_size:{width:D,height:x}}}static _skipScalingList(e,t){let i=8,o=8,r=0;for(let s=0;s ${t.codecWidth}, height ${i.height}-> ${t.codecHeight}`),void this.player.emit(O.webcodecsWidthOrHeightChange)}if(!this.isDecodeFirstIIframe&&i&&(this.isDecodeFirstIIframe=!0),this.isDecodeFirstIIframe){const o=new EncodedVideoChunk({data:e.slice(5),timestamp:t,type:i?_:$});this.player.emit(F.timeUpdate,t);try{if(this.isDecodeStateClosed())return void this.player.debug.warn("Webcodecs","VideoDecoder isDecodeStateClosed true");this.decoder.decode(o)}catch(e){this.player.debug.error("Webcodecs","VideoDecoder",e),(-1!==e.toString().indexOf(Ae)||-1!==e.toString().indexOf(de))&&this.player.emitError(O.webcodecsDecodeError)}}else this.player.debug.warn("Webcodecs","VideoDecoder isDecodeFirstIIframe false")}else if(i&&0===e[1]){const t=15&e[0];if(this.player.video.updateVideoInfo({encTypeCode:t}),t===P)return void this.emit(O.webcodecsH265NotSupport);this.player._times.decodeStart||(this.player._times.decodeStart=Se());const i=function(e){let t=e.subarray(1,4),i="avc1.";for(let e=0;e<3;e++){let o=t[e].toString(16);o.length<2&&(o="0"+o),i+=o}return{codec:i,description:e}}(e.slice(5));this.player.debug.log("Webcodecs","VideoDecoder configure",i);try{this.decoder.configure(i)}catch(e){return this.player.debug.error("Webcodecs","VideoDecoder configure",e),void this.player.emit(O.webcodecsConfigureError)}this.hasInit=!0}}isDecodeStateClosed(){return"closed"===this.decoder.state}}const lt={play:"播放",pause:"暂停",audio:"",mute:"",screenshot:"截图",loading:"加载",fullscreen:"全屏",fullscreenExit:"退出全屏",record:"录制",recordStop:"停止录制"};var ut=Object.keys(lt).reduce((e,t)=>(e[t]=`\n \n ${lt[t]?`${lt[t]}`:""}\n`,e),{}),ht=(e,t)=>{const{events:{proxy:i}}=e,o=document.createElement("object");o.setAttribute("aria-hidden","true"),o.setAttribute("tabindex",-1),o.type="text/html",o.data="about:blank",Be(o,{display:"block",position:"absolute",top:"0",left:"0",height:"100%",width:"100%",overflow:"hidden",pointerEvents:"none",zIndex:"-1"});let r=e.width,s=e.height;i(o,"load",()=>{i(o.contentDocument.defaultView,"resize",()=>{e.width===r&&e.height===s||(r=e.width,s=e.height,e.emit(F.resize),n())})}),e.$container.appendChild(o),e.on(F.destroy,()=>{e.$container.removeChild(o)}),e.on(F.volumechange,()=>{!function(e){if(0===e)Be(t.$volumeOn,"display","none"),Be(t.$volumeOff,"display","flex"),Be(t.$volumeHandle,"top","48px");else if(t.$volumeHandle&&t.$volumePanel){const i=Ce(t.$volumePanel,"height")||60,o=Ce(t.$volumeHandle,"height"),r=i-(i-o)*e-o;Be(t.$volumeHandle,"top",`${r}px`),Be(t.$volumeOn,"display","flex"),Be(t.$volumeOff,"display","none")}t.$volumePanelText&&(t.$volumePanelText.innerHTML=parseInt(100*e))}(e.volume)}),e.on(F.loading,e=>{Be(t.$loading,"display",e?"flex":"none"),Be(t.$poster,"display","none"),e&&Be(t.$playBig,"display","none")});const a=i=>{let o=xe(i)?i:e.fullscreen;Be(t.$fullscreenExit,"display",o?"flex":"none"),Be(t.$fullscreen,"display",o?"none":"flex")},n=()=>{Te()&&t.$controls&&e._opt.useWebFullScreen&&setTimeout(()=>{if(e.fullscreen){let i=e.height/2-e.width+19,o=e.height/2-19;t.$controls.style.transform=`translateX(${-i}px) translateY(-${o}px) rotate(-90deg)`}else t.$controls.style.transform="translateX(0) translateY(0) rotate(0)"},10)};try{ye.on("change",a),e.events.destroys.push(()=>{ye.off("change",a)})}catch(e){}e.on(F.webFullscreen,e=>{a(e),n()}),e.on(F.recording,()=>{Be(t.$record,"display",e.recording?"none":"flex"),Be(t.$recordStop,"display",e.recording?"flex":"none"),Be(t.$recording,"display",e.recording?"flex":"none"),!e.recording&&t.$recordingTime&&(t.$recordingTime.innerHTML=je(0))}),e.on(F.recordingTimestamp,e=>{t.$recordingTime&&(t.$recordingTime.innerHTML=je(e))}),e.on(F.playing,e=>{Be(t.$play,"display",e?"none":"flex"),Be(t.$playBig,"display",e?"none":"block"),Be(t.$pause,"display",e?"flex":"none"),Be(t.$screenshot,"display",e?"flex":"none"),Be(t.$record,"display",e?"flex":"none"),Be(t.$qualityMenu,"display",e?"flex":"none"),Be(t.$volume,"display",e?"flex":"none"),a(),e||t.$speed&&(t.$speed.innerHTML=function(e){if(null==e||""===e||0===parseInt(e)||isNaN(parseInt(e)))return"0KB/s";let t=parseFloat(e);return t=t.toFixed(2),t+"KB/s"}(""))}),e.on(F.kBps,e=>{const i=function(e){if(null==e||""===e||0===parseFloat(e)||"NaN"===e)return"0 KB/s";const t=["B/s","KB/s","MB/s","GB/s","TB/s","PB/s","EB/s","ZB/s","YB/s"];let i=0;const o=parseFloat(e/8);i=Math.floor(Math.log(o)/Math.log(1024));let r=o/Math.pow(1024,i);return r=r.toFixed(2),r+(t[i]||t[0])}(1e3*e);t.$speed&&(t.$speed.innerHTML=i)})};function pt(e,t){void 0===t&&(t={});var i=t.insertAt;if(e&&"undefined"!=typeof document){var o=document.head||document.getElementsByTagName("head")[0],r=document.createElement("style");r.type="text/css","top"===i&&o.firstChild?o.insertBefore(r,o.firstChild):o.appendChild(r),r.styleSheet?r.styleSheet.cssText=e:r.appendChild(document.createTextNode(e))}}pt('@keyframes rotation{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(1turn)}}@keyframes magentaPulse{0%{background-color:#630030;-webkit-box-shadow:0 0 9px #333}50%{background-color:#a9014b;-webkit-box-shadow:0 0 18px #a9014b}to{background-color:#630030;-webkit-box-shadow:0 0 9px #333}}.jessibuca-container .jessibuca-icon{cursor:pointer;width:16px;height:16px}.jessibuca-container .jessibuca-poster{position:absolute;z-index:10;left:0;top:0;right:0;bottom:0;height:100%;width:100%;background-position:50%;background-repeat:no-repeat;background-size:contain;pointer-events:none}.jessibuca-container .jessibuca-play-big{position:absolute;display:none;height:100%;width:100%;background:rgba(0,0,0,.4)}.jessibuca-container .jessibuca-play-big:after{cursor:pointer;content:"";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:block;width:48px;height:48px;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACgklEQVRoQ+3ZPYsTQRjA8eeZZCFlWttAwCIkZOaZJt8hlvkeHrlccuAFT6wEG0FQOeQQLCIWih6chQgKgkkKIyqKCVYip54IWmiQkTmyYhFvd3Zn3yDb7szu/7cv7GaDkPEFM94PK0DSZ9DzDAyHw7uI2HRDlVJX5/N5r9FoHCYdr/fvCRiNRmpJ6AEidoUQ15NG+AH8BgD2n9AHANAmohdJQfwAfgGA4xF4bjabnW21Whob62ILoKNfAsAGEd2PU2ATcNSNiDf0/cE5/xAHxDpgEf0NADaJ6HLUiKgAbvcjpdSGlPJZVJCoAUfdSqkLxWLxTLlc/mkbEgtgET1TSnWklLdtIuIEuN23crlcp16vv7cBSQKgu38AwBYRXQyLSArg3hsjRDxNRE+CQhIF/BN9qVAobFYqle+mkLQAdLd+8K0T0U0TRJoAbvc9fVkJId75gaQRoLv1C2STiPTb7rFLWgE6+g0RncwyYEJEtawCvjDGmpzzp5kD6NfxfD7frtVqB17xen2a7oG3ALBm+oMoFQBEPD+dTvtBfpImDXjIGFvjnD/3c7ksG5MU4HDxWeZa0HB3XhKAXcdxOn5vUi9gnIDXSqm2lHLPK8pkfVyAbSLqm4T5HRs1YB8RO0KIid8g03FRAT4rpbpSyh3TINPxUQB2GGM9zvkn05gg420CJovLZT9ISNA5tgB9ItoOGhFmnh/AcZ/X9xhj65zzV2Eiwsz1A1j2B8dHAOgS0W6YnduY6wkYj8d3lFKn/j66Ea84jtOrVqtfbQSE3YYnYDAY5Eql0hYAnNDv6kKIx2F3anO+J8DmzqLY1goQxVE12ebqDJgcrSjGrs5AFEfVZJt/AF0m+jHzUTtnAAAAAElFTkSuQmCC");background-repeat:no-repeat;background-position:50%}.jessibuca-container .jessibuca-play-big:hover:after{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACEElEQVRoQ+2ZXStEQRjH/3/yIXwDdz7J+i7kvdisXCk3SiFJW27kglBcSFFKbqwQSa4krykuKB09Naf2Yndn5jgzc06d53Znd36/mWfeniVyHsw5PwqB0DOonYEoijYBlOpAFwCMkHwLDS/9mwhEDUCfAAyTXA4tYSLwC6CtCegegH6S56FETAR+AHRoACcBTJAUWa+RloBAXwAYIrnt0yBNgZi7qtbHgw8RFwLC/QFglOScawlXAjH3gUqrE1cirgVi7mkAYyS/0xbxJSDcdwAGSa6nKeFTIOZeUyL3aYiEEBDuLwDjJGf+KxFKIOY+BdBL8iipSGiBmHtWbbuftiJZERBuOfgGSK7aSGRJIObeUml1ayKSRQHhlgtkiaTcdltGVgUE+ppkV54FaiS78yrwqlLoOI8Cch2XV548W7WRpTVwA6DP9kGUFYEpAOUkT9LQAvtq1M+0udKkQSgBqSlJWWYxKXj8vRACK+o6bbRIdYI+Ba7U7rKjg7L53JdAhWTZBsy0rWuBXZUuNVMg23auBF7UIl2yBbJt70JAoKV6/WwLk6R9mgKSJlJ1kLTxFmkJyCla8UZd15GJQKvyumyJ8gy8DAEvfZoINPqD41EtUjmUgoaJwAaAnjrKebVI34OSq85NBNqlCAWgE0CV5GEWwI3vQlmCbcSinYFCwPEIFDPgeIC1P1/MgHaIHDf4Aydx2TF7wnKeAAAAAElFTkSuQmCC")}.jessibuca-container .jessibuca-recording{display:none;position:absolute;left:50%;top:0;padding:0 3px;transform:translateX(-50%);justify-content:space-around;align-items:center;width:95px;height:20px;background:#000;opacity:1;border-radius:0 0 8px 8px;z-index:1}.jessibuca-container .jessibuca-recording .jessibuca-recording-red-point{width:8px;height:8px;background:#ff1f1f;border-radius:50%;animation:magentaPulse 1s linear infinite}.jessibuca-container .jessibuca-recording .jessibuca-recording-time{font-size:14px;font-weight:500;color:#ddd}.jessibuca-container .jessibuca-recording .jessibuca-icon-recordStop{width:16px;height:16px;cursor:pointer}.jessibuca-container .jessibuca-loading{display:none;flex-direction:column;justify-content:center;align-items:center;position:absolute;z-index:20;left:0;top:0;right:0;bottom:0;width:100%;height:100%;pointer-events:none}.jessibuca-container .jessibuca-loading-text{line-height:20px;font-size:13px;color:#fff;margin-top:10px}.jessibuca-container .jessibuca-controls{background-color:#161616;box-sizing:border-box;display:flex;flex-direction:column;justify-content:flex-end;position:absolute;z-index:40;left:0;right:0;bottom:0;height:38px;width:100%;padding-left:13px;padding-right:13px;font-size:14px;color:#fff;opacity:0;visibility:hidden;-webkit-user-select:none;-moz-user-select:none;user-select:none}.jessibuca-container .jessibuca-controls .jessibuca-controls-item{position:relative;display:flex;justify-content:center;padding:0 8px}.jessibuca-container .jessibuca-controls .jessibuca-controls-item:hover .icon-title-tips{visibility:visible;opacity:1}.jessibuca-container .jessibuca-controls .jessibuca-fullscreen,.jessibuca-container .jessibuca-controls .jessibuca-fullscreen-exit,.jessibuca-container .jessibuca-controls .jessibuca-icon-audio,.jessibuca-container .jessibuca-controls .jessibuca-microphone-close,.jessibuca-container .jessibuca-controls .jessibuca-pause,.jessibuca-container .jessibuca-controls .jessibuca-play,.jessibuca-container .jessibuca-controls .jessibuca-record,.jessibuca-container .jessibuca-controls .jessibuca-record-stop,.jessibuca-container .jessibuca-controls .jessibuca-screenshot{display:none}.jessibuca-container .jessibuca-controls .jessibuca-icon-audio,.jessibuca-container .jessibuca-controls .jessibuca-icon-mute{z-index:1}.jessibuca-container .jessibuca-controls .jessibuca-controls-bottom{display:flex;justify-content:space-between;height:100%}.jessibuca-container .jessibuca-controls .jessibuca-controls-bottom .jessibuca-controls-left,.jessibuca-container .jessibuca-controls .jessibuca-controls-bottom .jessibuca-controls-right{display:flex;align-items:center}.jessibuca-container.jessibuca-controls-show .jessibuca-controls{opacity:1;visibility:visible}.jessibuca-container.jessibuca-controls-show-auto-hide .jessibuca-controls{opacity:.8;visibility:visible;display:none}.jessibuca-container.jessibuca-hide-cursor *{cursor:none!important}.jessibuca-container .jessibuca-icon-loading{width:50px;height:50px;background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAHHklEQVRoQ91bfYwdVRX/nTvbPuuqlEQM0q4IRYMSP0KkaNTEEAokNUEDFr9iEIOiuCC2++4dl+Tti9nOmbfWFgryESPhH7V+IIpG8SN+Fr8qqKgQEKoUkQREwXTLs8495mze1tf35s2bfTu7ndf758y55/x+c879OvcMYYnbxMTEy4IgOImIxkRkrYisNsasUrPe+wNE9C8ielRE9iVJsndmZubBpYRES6E8DMNXeu83ENHrAJwO4OUARvrY+i+ABwDcLSJ7jDF3RlF0f9H4CiNcrVZPCIJgk4hcCOCNBQH9EYBveO93NRqNx4rQuWjCExMT64IguEJE3kdEq4sA1alDRDTsb02SZOfMzMxDi7ExMGFr7THGGCciVwKYG5PL0HTMb69UKtNTU1Ozg9gbiLC1diMRXQ/gxEGMFtDnQRHZHMfxHQvVtWDCzrkdANSredvfRWQ3Ee0F8DCAJwDs994nQRCM6qxNROu892uI6A0ATs2rWER2xHF8VV55lctN2Dl3LICvA3hzDgMPENFXROT2SqVyb71efzZHnzkRnRNGRkY2isj5AM7K0e/HAN7OzP/MIZuP8OTk5FiSJDpjnpylVER+YIzZEUXRN/MY7ydTrVbXE9FlRPT+LFkiesh7f1Ycx4/009nXw9balxDRLwC8OEPZ/SLi4jjWCCi8WWtfA2CKiN6WofzxIAhePz09/dfMj5P1slqtPj8IgntEZF0vORH51Ozs7NU7d+5sFs60Q2EYhpeKyDUZq8LDInJ6HMdP98KS6WHn3E8BvKlHZx2X72Xmry410Xb91trTiOjLAF7Rw+5uZu6FufcYds7pl7wiTSkRPSUi5zHzr5eT7LytWq32gmaz+a0MZ1zDzB9LxZ72sFqtbjDGfLcHmWeI6IwoinTfe8RarVYzzWbzJxnb2A3M/P1OgF0hPT4+XhkdHd0H4LgUNv8xxpy5devW3x4xpm2Gt2zZMjoyMnJ363DSCemJ/fv3j3XOLV2EnXMNXQ57hPIFURTdVgay8xhaq4geKVem4Jph5mr788MIV6vVtcYY9W5XI6Iboij6SJnIzmNxzl0E4Itp2IIgWDs9Pf23+XeHEQ7D8EYR+VBKx8eYeU0ZybaR1s3OxhSMNzLzh7sIb968+YUrVqxQ7z6na6ATlS6UOzG2Qlv366bj3bMHDx4c27Zt25P6/JCHnXO6Cf90yhe6l5lfXWbvto3nm4no0hSHXRVFkR56/k/YWvsbItJ0zGFNRC6K4/hLQ0JYt8FdW0si2hNF0RmHCLcSbWnr6pPM/CIAMgyEFaNz7tsAzuvEmyTJKZotmQtpa+04EV2bQuo6Zh4fFrItwu8C8PmUSP1oHMfXzxEOw3CXiGzqFPLen9NoNL43TIQ19UREmmRY0YF7FzO/k5xzLwWgYdCZaZj13h/faDT+PUyEW15OO/T8MQiCjUr4HAC6Ee/MG/+MmfNkN0r3Pay124jo4x3ADuiBRwl/EMBNKTF/SxzHl5SOTQ5AzrnLANyQsjxdooRrmk1I0TPFzPUc+ksnYq09l4i+k8aJrLXbiajr7EhEV0ZRlDZzl45gJyDNhRljfpkCdLt6WF2vIdDZPsDMnys9uxSA1tpXEdHvU1599qgknHHqu/moDOlWNkTTyu2rTGKMOfeonLQ0lFunv08AOBPAXu/9jkajsafnsgTgVma+eBjHcBbmrI3HXcxc1D1vab5b1tbyQKVSOb5erz9TGrQFAMk8POhWLI7jOwuwUxoV/Y6Hn2Hmy0uDtgAgc4RbZQt/Ttl7PrVy5crj6vW6L8BWKVS057TuAqAX0p3t3cz8hVKgLQDEIcLW2suJ6LoUnX9i5tMKsFUKFYcIZ6VpAWxiZr2xG/p2WCI+4yDxeKVSWXM0jOXDCE9OTq5JkuTRNDcS0U1RFKWdqobK612XaWEYflJEru7BYuhDu4tw66ShxSFpd0laD7meme8ZKre2gU0teXDOnQ2gV3q2FBfig37wnjUevVI/auhIlzwMSnYOe1bnPkUtWrXznuUualkM2b6EtWzJGKMlBaf0MrScZUuLJduXsAq07l1/DuCEDIP3iUi4VIVpRRCd19G3Ek8FtfTQe//DrAI1lSu69LBIogsirMK1Wm11s9n8GoC35AByH4DbvPe3r1q16g8LKS7NoXtRIrk83G4ha/bugURL93cD+Mt8+TAR6YT3j0ql8rtBC70HZb1gwmooDMO3eu+vJaKTBjXc6rfPe39ho9H41SL15O4+EOFWiGv5n2sViz83t8VuwWW9pRyY8Dxu59zJIqJVAhcP+JPHI8y8bL8SLJrwPHH9jYeI3kFEF+Ssmp/rqjN7HMe6lV2WVhjhdrRhGJ7a+lFrPYDXAtB667Q/X5723p+tNwLLwrbf1rIIEBryxpgTkyQZA6DlFccS0fMA6G84d6RVvBZht5eO/wEB1Kvsoc6vtAAAAABJRU5ErkJggg==") no-repeat 50%;background-size:100% 100%;animation:rotation 1s linear infinite}.jessibuca-container .jessibuca-icon-screenshot{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAE5UlEQVRoQ+1YW2sdVRT+1s7JxbsoVkEUrIIX0ouz15zYNA+N1RdtQfCltlUfvLbqL/BCwZ8grbHtizQqPojgBSr0JkiMmT2nxgapqBURtPVCq7HxJCeZJVPmxDlzZubMmXOSEsnAvOy917fXt9e39tp7E5b4R0vcfywTuNgRbBgBx3HuJqLVzPzmYjprjHkcwAlmLqXNm4XAISLaSESPaq2HF4OE67rbRGRYRA7btn1fbgLGmKsA/Azg0gBkGzO/vZAkHMd5hIiqc5wHcCMz/5k0Z2oExsfHV1QqldPAf8lORNu11m8tBAljzFYAYWxRSl1vWdZvuQj4RsYYF4AVBlgIOVVlE55HRIxt23ZuCfmGjuOsJ6LPoiAistW27XfaEYmIbOYhPc9bXywWR1oiEJDYQkR1zrYjEjGyqfqbKd8a7kJVtLgQ+30i8pht2wfyRKIdmJkJBPkQTbILfudJ7CTZNBvVpggEcgpvc/ML38zESbLJsxBNE/A9biX0rdjGyTQXgbxyapdsarb0PMlXtWnGoXbKpm0Essqp3bJpK4E0OXmed3+hUBDP8w5FI91M0rdcyLLILElOCbaZilSWeXMncRx4klTCY1spfG3dhZJWx3GcDUR0EEB3ZMw0ET2gtT6SZWWzjmlrBIJCl0hAKfWgZVmHszqXZVxbCSxpCS2JJA6umIhe8ZKKVLPbaBJ+S9toqVRa53nedgAbAKwIwH4FcAzAa0R0l4i8F7PPz189k6RFRA+LyNcAXojDV0oNW5b1eW4Cxpg9AHZkSaaa6hhzb065uDSCH2LmRB8Sk9gY4293g43Qo/1pV80m8yQMfZSZ781cB1zXHRKRZ2IMpgD8A+DamL4ZItqitX4/jbQx5iEA7wLoihn3V/ACckWMJN/QWj9b1x5tGBsbW6uUOh5pPy0iL3Z2dn6ilJqanp5ep5TaJSLhF4NppdRNaU8gPmapVLrO87yfIoXuWyJ6uVKp+HmFjo6OQSJ6FcBtYT+UUmstyxqvkWuUgDFmP4AnQu2/e563qlgs+u9DNZ8xZhRAX7VRRPbath0XuXk7Y8xeAE+FgL6fnJzsHRwcLIfBR0ZGLunq6poAsDLUvp+Zw7b1r9PGmJMAbg8Z7WDmoThZuK67WkS+DD18fcPMdzSQUBR/EzN/nIC/SUQ+DPXV4dclsTHmHAD/SfHCNzc3t7Kvr++HJKeMMacA3BL0nyuXyzcPDAxMxo0fHR29slAo/Ajg6qD/fE9Pzw29vb1/x42fmJi4vFwu+5G/LOg/y8zXNJLQ2dAES5JANMQ7mfn1jBI6ycx3NiMhItqstf4oAX+ziHwQ6qvDj5NQNIn/ALCKmX+JSeIvABRD7fuY+ekGBPYBeDI05tTMzExvf3+/vz2Hk91/ET8RSeI6/DoCpVJpjed5fmKGvzMAXpqdnT3oed5Ud3d3v4jsAqBr9Ei0Rmv9VRqBBPzvROQVETnq2xJRdRu9tRF+bCVOKWT+Kvl/TSIFk6SW/LAjKfjV5K8rZABi8dOOEv7FI7Z8x6zwEWbemLbyMfJr5qiSiJ96oclymBOR3bZtP9+M89WxxpjdAHY2sN3DzM8ljWl4I3Nd9x7/OE1ENcdpETnmH3e11n41zv0l4J8RkU+J6AAz+xtF4teQQG7PFslwmcAiLfSyhC72Qv9/I/Avns2OT7QJskoAAAAASUVORK5CYII=") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-screenshot:hover{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAED0lEQVRoQ+2ZycsdRRTFf2ejqHFAMQqiYBTUoElUHLNx3GgCgpuYRF2o0UT9CxwQ/BMkMSbZSKLiQgQHUDCJgjiAxiEiESdEcJbEedgcKaj3UV+/6q7u/jovPPkK3qbr1ql76p5bt6qemPKmKfefeQKHOoLFCNg+H1gi6fFJOmv7VmCvpD1N87Yh8ApwNXCzpB2TIGF7DRDm2inpmt4EbB8LfAMcGUHWSHryYJKwfRMwmuMP4BRJv9TN2RgB2wuB72BWsq+V9MTBIGF7NZBiGzhJ0o+9CIRBtt8FLqgADC6nRDbpVO9Iuqi3hCKB5cDrGZDVkp4aIhIV2aSQyyW9MScCkcQqIOfsnCORkc3I31b5VtyFRmg1IQ7dt0ja3icSQ2C2JhAjUU2ykd+dE7tBNp2i2olAJJFuc+nCt564QTadF6IzgUhiVGiqyinKaQjZpJP2ItBXTkPJZhACXeU0pGwGI9BWTkPLZlACBTldG4o5EA6E1dY66edcyNrs8Q36zg1vVaTazNs7iXPgDVJJzYs7VRvHRzaDEohyugJ4CTi84sg/wHWSdnVxsGQ7aQLXS9pZcqpL/6AEplpCU5HE8YpJ9YrXUKQ6baN1+HPaRm1fBqwFQnKGK2ZoPwCvAo8Ai4FnMpPMHMwapHUj8DFwbw3+Dklv9iZgexOwvktSRduxU2VDlErwmyXV+lCbxLbDdndlCT3TX3vV7JgnKfRuSVflfMkSsL0ZuDMz4E/gL+CETN+/wCpJzzaRtn0D8DRwWMbu1/gCcnSm7zFJd1W/jxGwvQx4r2IYnlbuA14GAomQFw8B6YtBKFSnNj2BxEJ3IvB1pdB9CjwQ8yqYhcg/DJxZ8WOZpA/SbzkC24DbEqOfgPMkBRKzmu23gEuSj1sk5SI3Y2J7C3BHMuZz4FxJf6fgto8APgIWJd+3SUrHjr9O294HnJUMWi8pSGqs2V4CvJ88fH0i6eyChKr4KyS9WIO/Ang+6RvDz0XgABCeFEdtkaQv65yy/QVweuwPY0+T9FuNQ8cAXwHHxf7wdHiypN9r7BfEl8GjYv9+SceXJLQ/mSDYTh2Baog3SHq0pYT2STqno4RWSnqhBn8l8FzSN4bfJol/jkn8bXUS228DFyfft0paVyCwFbg9sQkSDEkctueZZju8iO+tJPEYfo7A0piYKd73wP3xnB+20cvjNnphxdmlkj4sEMjhfwY8COyOY0fb6Bkl/K6FLKxS+M1KpDhJY8mvrG5doRwlf66QZfGbjhLh4pEt35kV3iUp/IvTunU8qtTil/7gaHOY2yjpntaez9b5RmBDYewmSXfX2RRvZLYvbThOh+NuqMa9Ww1+yLnXgO2SwkZR24oEens2oYHzBCa00PMSOtQL/f+NwH+Hg8hAnbrYgQAAAABJRU5ErkJggg==") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-play{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACgklEQVRoQ+3ZPYsTQRjA8eeZZCFlWttAwCIkZOaZJt8hlvkeHrlccuAFT6wEG0FQOeQQLCIWih6chQgKgkkKIyqKCVYip54IWmiQkTmyYhFvd3Zn3yDb7szu/7cv7GaDkPEFM94PK0DSZ9DzDAyHw7uI2HRDlVJX5/N5r9FoHCYdr/fvCRiNRmpJ6AEidoUQ15NG+AH8BgD2n9AHANAmohdJQfwAfgGA4xF4bjabnW21Whob62ILoKNfAsAGEd2PU2ATcNSNiDf0/cE5/xAHxDpgEf0NADaJ6HLUiKgAbvcjpdSGlPJZVJCoAUfdSqkLxWLxTLlc/mkbEgtgET1TSnWklLdtIuIEuN23crlcp16vv7cBSQKgu38AwBYRXQyLSArg3hsjRDxNRE+CQhIF/BN9qVAobFYqle+mkLQAdLd+8K0T0U0TRJoAbvc9fVkJId75gaQRoLv1C2STiPTb7rFLWgE6+g0RncwyYEJEtawCvjDGmpzzp5kD6NfxfD7frtVqB17xen2a7oG3ALBm+oMoFQBEPD+dTvtBfpImDXjIGFvjnD/3c7ksG5MU4HDxWeZa0HB3XhKAXcdxOn5vUi9gnIDXSqm2lHLPK8pkfVyAbSLqm4T5HRs1YB8RO0KIid8g03FRAT4rpbpSyh3TINPxUQB2GGM9zvkn05gg420CJovLZT9ISNA5tgB9ItoOGhFmnh/AcZ/X9xhj65zzV2Eiwsz1A1j2B8dHAOgS0W6YnduY6wkYj8d3lFKn/j66Ea84jtOrVqtfbQSE3YYnYDAY5Eql0hYAnNDv6kKIx2F3anO+J8DmzqLY1goQxVE12ebqDJgcrSjGrs5AFEfVZJt/AF0m+jHzUTtnAAAAAElFTkSuQmCC") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-play:hover{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACEElEQVRoQ+2ZXStEQRjH/3/yIXwDdz7J+i7kvdisXCk3SiFJW27kglBcSFFKbqwQSa4krykuKB09Naf2Yndn5jgzc06d53Znd36/mWfeniVyHsw5PwqB0DOonYEoijYBlOpAFwCMkHwLDS/9mwhEDUCfAAyTXA4tYSLwC6CtCegegH6S56FETAR+AHRoACcBTJAUWa+RloBAXwAYIrnt0yBNgZi7qtbHgw8RFwLC/QFglOScawlXAjH3gUqrE1cirgVi7mkAYyS/0xbxJSDcdwAGSa6nKeFTIOZeUyL3aYiEEBDuLwDjJGf+KxFKIOY+BdBL8iipSGiBmHtWbbuftiJZERBuOfgGSK7aSGRJIObeUml1ayKSRQHhlgtkiaTcdltGVgUE+ppkV54FaiS78yrwqlLoOI8Cch2XV548W7WRpTVwA6DP9kGUFYEpAOUkT9LQAvtq1M+0udKkQSgBqSlJWWYxKXj8vRACK+o6bbRIdYI+Ba7U7rKjg7L53JdAhWTZBsy0rWuBXZUuNVMg23auBF7UIl2yBbJt70JAoKV6/WwLk6R9mgKSJlJ1kLTxFmkJyCla8UZd15GJQKvyumyJ8gy8DAEvfZoINPqD41EtUjmUgoaJwAaAnjrKebVI34OSq85NBNqlCAWgE0CV5GEWwI3vQlmCbcSinYFCwPEIFDPgeIC1P1/MgHaIHDf4Aydx2TF7wnKeAAAAAElFTkSuQmCC") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-pause{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABA0lEQVRoQ+1YwQqCUBAcfWXXsLr2AXWTPXno8yVB8AP6Aa3oHI+kCDqYaawJljSe133uzO44bx0M/HEG/v1gAd9mkAyQgY4I/F8LJUlyrQFtD2AtIkcNoFEU+Z7n7QD4DfFHEVlocrVmgAUAIAOl3mILPcDgEFcUhyrUKMGUUcroc3NQRimj9XJBGaWMvvPydKN0o6/9QTdKN6rZANxj6EbpRulGuZnjYqs8BbyR8Ub2Izeys+u6yyAIDpo/ehzHM2NMDsA0xFsRmWhyfTIDWSXxCEBmrd2EYXjSHJqm6bQoii2AOYBL5Z0xgFxEVppcrQvQJO0zhgX0iXbdWWSADHRE4AZQ731AhEUeNwAAAABJRU5ErkJggg==") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-pause:hover{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAA7klEQVRoQ+2YSwrCQBBEX6HiVvxsPYDewfN7By/gD9ciQkvERQwJdBSiYs0mEDo96aruombEjy/9+P/jAj7NoBkwA28i8H8tFBFRA9oeWEo6ZgCNiDGwAYpn3TpKmmVytWbABQBmoNRbbqEHGB7iiuJYhRol2DJqGX1uDsuoZdRmLuNZSzGWUcuoZdRHSp/IylNgK2ErYSthK3FHwLcSvpXIjoLt9Jfa6TMwl3TIMBkRE2AH9BriL5KGmVyvWIltJXEfKN6tJJ0ym0bECFgDU+Ba+WZQFCdpkcnVuoBM0i5jXECXaNftZQbMwJsI3AAPN3dAQflHegAAAABJRU5ErkJggg==") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-record{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAC+UlEQVRoQ+1ZS2sTURT+zlDJYE3XSq219QHVuEjnJDT+Bff9Abqw2voAEfGxqygUqWhVFHGl/yMLu9BwByxk5SNI66ML6U7axjhHbmhgWiftncxoOiV3FcI53z3f/e65594zhIQPSnj86BBot4IdBToKRFyBnbeFlFIScVEiuYvIWC6Xe2YK8pcC7SYA4CMzH4mDQBXAqilQBDsLQLfPf9FxnF4i8kwwmypARI+Wl5dvmIBEsUmlUkNE9NaHsVCpVAZGR0d/m+A2JSAid3K53E0TkCg2pVKpz7KseR/GfKVSGYxMAMA0M1+JEpyJb6lUOm5ZVnkrAsVisaunp+esiByr1Wp3R0ZGvmifzZK4XQQWHMc52MgBpdQuAOcAXABwuB400ZTjONdaIjA7O5u2bVsnWU1EujzP+5nP5xdMVjvIJkCBD8x8VCm1G8AYgAkAAxt8Z5j5YmgCSqlTAJ4D2OcD/AXgATNfbYVEAIFPIvKKiE4D6GuCea8xX6gtpJT6DmBvECgRFRzHeROWRAABE4iWCbwHEFhkPM/L5vP5dyaz+23+KwHXdR3P854S0YG1ILSCuthNMfNM2OC1/RYENLY+ygcBnPfht6ZAA6BYLNr6dyqVokKhsGpaNQ2TWJstreXaE2aed133sojcj41AKyvdzCdAgSXLsk4MDw9/a/i4rntbRPxFNZoC/5jAV2be759DKTUJ4FZSFFi0bbs/k8noy2R9dAjEuWU2YgXkQOK3kD6BMsysi2Z9JC2Jdcw/ALzwPO+xvmcl7Rj177JVEbkO4BARjSflFDJJuW1dBxJPoCIiL4noDIB1BS0pW6j+oJmbm+uuVqvjRKQfLr0bZHnIzJf0f6HeAybahrUJqAPruhLlcnnPysqKfpXp11n/Gv62zoHAroS+AafT6QkiGrIsazKbzX7eVIHEt1US39gCkOzWYthkjNE+tuZujDGZQ8XRXn8N4KT5lLFZ6uaYPt+nwyDuvC80YdhvB9uOAu1WoaNAR4GIK/AHvdr+QAexB7EAAAAASUVORK5CYII=") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-record:hover{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACfUlEQVRoQ+2ZSYsUQRCFvycK4nJXXEbHBdwO4kn/gv9CD467ICIutxEFkREdFUU86T/xojcPntyQcT2INw+uISFVkD1Wd2dWlU7nUHlqisiX+fJFZGREi8yHMt8/HYG5VrBToFOg4QnMPxcyM2t4KE2nT0i6EwvylwIjQOCFpE1tEPgGfI0FamC3AFgazP8IrJL0KwZzkAI3gLMxIA1ttgCPA4w3wHpJP2NwBxG4KOlcDEgTGzNbA8wEGP57vA0CU5JONtlczFwz2wY8HUbAzBYCB4CtwCVJb33OIAXmioC70LoyBsxsEXAQOApsLIhelnS6FgEzW+5BBvwA/FS+SPJFa40KBZ5L2mxmS4AJ4IjHxCzwaUnHkgmY2V7gLrAyAPwOXJN0qg6DCgIvgQfAPsDjo2pcKddLciEz+wCs6AO6W9KjVBIVBGIgahN4BvRLMjslPYlZPbT53wR2AbeBtcUmXEFPdh5U06mbd/shBBzbr/Jx4FCAX0+BEsDMFocEYrNmFcE+BD4XsXZL0oyZnQCutkagzkn3m1NBwDe/Q9L74MAuFEqUn5op8I8JvJO0elacTALnc1HAH3Njkvwx+WeYWUegTa/pwaqIgexdyIN4uyRPmqULZRXEvulPwD3gpr+zcrtGQxfzRHYG2AAczuUWiom3kc4D2RN4BdwH9gM9CS0XFyoLGu9UuN974eIFVDiuSzruH5LqgRhtU20q8kBPV8LMlhVVmVdnYwX+SMdAZVeieAF7eeltmElJr4cpkH1bJfvGVvatxdR4bMu+teZuWxtKxWncXn8I7EldtQV7vz79fp9KwZp//9CksB8F206BuVahU6BToOEJ/Ab7+KdABdTt8AAAAABJRU5ErkJggg==") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-recordStop{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAGDElEQVRoQ82ZaahVVRTHf//moKKggQawcmg0olGl0awvRoMVBRGFlQ1YQZIZqRVKmJmFgVk59EFQykYjgmajbJ7n2WiAbKKCBq0Vfznndd723Lvvve/5bMH9cvfaa63/2WuvaYteoIjYHDgEOAAYDOwIbA/4f9PvwHfAt8DbwGvAS5L8f49Ine6OCO89CTgFOBrYqU1Z3wBPAUskPdDm3i72jgBExCXAWGBQp4qTfR8CMyXd0a68tgBExEjgBmCfdhW1yP8eMFHS/S3y0xKAiNgQmA2MaUHwB8DnwNfAbwX/FsDOwG7Ani3I8ElcLOnvHG8WQET0Ax4C9msi7BHgbuAFSXaHhhQRewBDgZOBE5qwvuV1SSuayWsKICIcVZ4Atq4R8mdxKnMkfZT7UnXrEeE7dD7gO7VpDc/PwAhJrzaS3xBAROzrUFcJhVUZjhrjJX3cieHpnogYUNytUTXy/gAOlvROna5aABHhGG5f3qZmk33ztt4wvAbIBcCcBicxSNLKdK0RgNeB/RPmVcBxkp5eF8aXMiPiKODRGpd6XZJduhutBSAipgNX1Bg/tJkv9iao4u4tBzZJ5N4oaXz1v24AImIvwLE4peGSnDX7jCLC2f3JGoV7S3q//D8F8DJwULJpgiQnrz6niLgSmJYofkXSwWsBiIgRwGPNmPscARARDqGp7zu0Orz/l4kjYhlweGLk4Ebhq8oXEc6wGwH/tAhyA2C1JGfsphQRTqBvJkzLJB3ZBaBIKGkGXSqpWab013FWvacooXO21K07256WS4QRsRQ4PhHgsPrxmjsQEZOB6xKGIZJebGZVRDwOHNOJ5ZU9j0s6NqPnUJcpCc9kSVNKAA5ZQyoMn0gamDMsIj4rCrQca7P1zyT1zwmIiE+AKt9yScNUFGuuZaoxd7okR4Ccfzq997S0fleSy5acrjQ//QUMNADXH/cmu0dKcoWZE+r2MKs8I+YdSW5Dc7rcizycMI0ygKuA6ysLjiT9JX3RgtC+BLArYJet5q4JBuBG5aKKsV/ZryWt/p8BcJj2R3VjVNJsA1gEnFH5821JzZqXLtaI6LMTsNIafYsM4L6iOyoNe1FSNSI1PIj1AMCh1CG1pPsNYEkxGin/fFVSWg/VglgPAF4BDqwYs8QAFgDnVP78SJIzbJbWAwBXC9VRzgIDcLVXjfm/AP0kuR/NhbY+uwMR4e7QDf6WFaOmGYBHJbcnlh7USvPSlycQEXYdu1CVxhiARxzPJwsXSarrTbux9TEAh3qH/CqtKSU2Az5NZpsPSTqxBRdy49/SfWki60NJ2WFXTUXqwdmAsphbCJxZUeIGfltJvg8NKSIMfPcc0Mx6tpiLiK2AH4qeoxS3UNJZJYC6emicpJkZAOOAGT0EcLmkmzvQM8oz1BLAxsX8vjqBWynJ86FcJDoLGO4OC8jOMgthnrX696Qkn35Oh+dB21aYfgJ2kLSqqzCKiGuAaxNJkyRNzSlYl+sNmq2pkiZZbxWAJ8g/Aj6NksI+3kplui5AFL2271m1AvVJb1fmqXSsMhGYkhjznqSeNi0d4YsIz3/SCNXNK+omcy5ZPVKv0r2STu3Iig431dRolrRCkvuCLqoD4BlM3Th7nqTzOrSnrW0RcSdQp+tASX4gbAzAK8Ub2KwarQ8Cp0vy20CvU5FUFwN1SfRSSbemSpu9D9wCXFZjpacDoyU925sIIuIw4K5k8lCqmCWpzpbmb2QRMRc4t4GhfiOYJunLngCJiF2Aq4ELG8iZL6mRDflHvohwpnXGrSM/VM8DFkt6rh0gxRd3K3s24BBeRzMkpaP+bnzZR77iTvgLuOR29mxEDnmer7rk9dPT98CvBbNreGdSD8s8WT4i81rpjD5G0vzcR2kJQAHCs5ubgKZjwERhednrHvAa2eaPMFaSm6UstQyglBQRDm92qWwJnNXencGnZpdp67W+bQAVIKOLCz6sTUNTdjdTcyW5N2+bOgZQAeLHQLuV5/UeM6ZZPDXKfa1nqs/4QUXSG21bXdnQYwBV5RHhy2rXcmh0E+5GxOTGyCWwp34fSCovd09sX7P3X2uzPXCoLsVMAAAAAElFTkSuQmCC") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-recordStop:hover{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAHn0lEQVRoQ81ZbYxcVRl+nnvu7ErSEmtqDdKwO3e2LWJLSEuFNiofFv9AUIpfiSFqCzt31lITGgEjHxKIKVirqXbnzpZSf5BAoHwIhpiAgDVSwBaU1rZLd+7skiIJKCWVpOzOPfc1d3dn986dO3Nn9kvuz3ve87zPc857znnPe4gZ+BZvlzPMed4XDG2sBGWFAGcRXET6ZwTwIsZpgbxL4B0ID/nKf8370Hz1xE08PV33nDKACDOO/roQ15K4TASfbQWLxL9E8AKJvcWs+WQrfcO2UxKQcfSNAn8TwKVTdVzdT/oJbi/aZl+reC0JsArelRDeC8jnW3XUnL0cofC2Ys58ojl7oDkBj4hKv697CXQnA8sxCEsE3hbKh4E9hfMEOBuUNMBzkzAE6Ct9SvXgW9RJtokC0r+VDqb8pyByfgOwZ0g84mv1cqmH/Y2cpntlmUG9BgauEcHVdW3JN6RsXF3axKFGeA0FdBVGVvpi/AnAJ2NAhkHpBU3H7eabSSMV1271yVL63g0C3gigPcbmA/r+umJP28F6+HUFZPLDy4XqVQCjW2HkexJQN7s2j0+FeLRPZqd0idL3Algfg/cRRa8u5toPx/mKFZDJyyKhPgZgQU0nssfNqvxMEK8RktdZoThxM2G0qaUDG/hetC1WgOXo1wG5IGJcNkS+OpBLvTgb5CuYXfnypT75x2hICfh6yVYrEwWknfJ9BH8cJU/fX9MoFmdS1Pja2w+gLYwrkF+U7NTN4X9VM9CxUz6nlD5So5JyeTGbemEmSSZhZQrly0T4fNROa3Xe0A95tPK/SoDleH8DcGF1J97q2ipYYHP+WY6+BZCtEccHXNtcXSPA6iuvg89nGxnPuQIAlqMPAhKJfVnn2qlge588iS3H2wfgS1XxJXpFve0rbNexS9JKwzQIvxmRvsDQCt7QDSwl2ad7h8+nof4Rsdvn2uYlEwKCAwW+jp6gT7u2Wf+kBBCcqjT8RwFZkUQktp18AzS+mXQQWo73NICrqjHU0uAcGl0DlqPvAOSusIFP/+LBbNsrjYhZjvccgK9MiXylk+A5N2de0QijszBykSHGy1XRQd5RzKq7RwVkHG+/ABdPGBADbtZckkTMcjw3mIgku0btArgl28wkYViONxBQndSN/SXbXMvRZM3UQS4zuedS7nOzqVuSQfXh6afW/Kdrq+VJvmLOpxFQLaHleEH+8VgE4ErXNp9JArUcfQiQROeNcXjYtVXiGhq7i+AP1ZsM1tNy9E8A+XmowfdFZQZzHPw4CejMS6dBHYRs6OzirbTyXi+IXIjsiXPeUekX76L3cRJw6Z1ivnWWDgb17BCvXloF7yEIvjP5k4dcWzW6vEyYzmUIje+W0ZB9KFgDjwO4JqTqFdc2J3ekBtMw9wK8YCu9KETpiWAG9kJwbejnQdc2I/lQvIr/g4ADAFaF2OwNZmAPgO9P/pQ3XTu1LCn+60xpM90iNs3tQmP+yv2RUs4eWk55K8Dwnn/Kb1cdgz/gB0ls5nIGzumVBaahgwv+/AleIluZcbxuAQpV+6vvX9jM5WUuBWR6R1aJYQQhFOKPbnY55TU++FL1aDPn2irublplNpcCrILOQaQ3TMCArGXnHvmEGtHFcG2TxFPFrPm15BAqHwPY1HqpjyX9rp1KLHbFZKRv++2qazwb9R4E8N2Qk7IxohYObOapRiLSjlckYCUJbdTeTDLXtUPO9Nv0fwCYIawHXdu8riIgJh/iFtdW2xsKKOgtFNk2HQEQ3uTm1K9a9UPB+qCGOipgVUFSJ0W/W1WBE7zn5sxFSeTSee86EpdT4ImBxFpmgEcfSgglwPMl2wxmv+FnOV5QD1oYMjq5gOozB7MsTyRGVkHfCZGfVe1G4O1FW92T5GA22+MuWwK5p2Snbh8djIrz83bKvI+Ufh9AKrxT+aKsZjLT2RAxdtfWxeoMFJ7frj5dOaeqyioZR98mkLurycgR107N0ntAUuiUj0bL8YxERU1p0Sp4gxB0VEETj7lZ8xuzMcr1MGNytCBehtys2Vkd5hGE8bJeXDl7t2ub18+FiEze2yVEjS+D/qqBbNtrDQUEjWNvYLIjSlaA36sR9e2BzRyeDSHBocph/TCBmkOU4OairX4T9Vv3fcByyr8G+KMaosSAaNlQ6kn9ZSZFWIXyFyH8XbjyUMEXkR2lXKqWS2R11/CxHO9+ABtjiQryMNRWN8u3piOka5cs9rX+KQA7Fod4wM2a8RySBIyGU768TcgtdUieJrEbvjxczKX+2oqQ8REPrrLfAzAvri8h24p2Klrqj+wvTXhNO95GjqXcqp45KUcF3CfAAaEcN+H/25e2/wb2BkfmezAWUrgEgtWEfDnhtVJD0O3mzAeS6CW+UlYArMLwCoj6JYCGZcCIw8pij3vAq8dtH6g3udn2Q0nkg/amBVTA0gXveopsaea9txkCkzZynOC2Vl/rWxYwMSN5b8PoAifWtkY0Yi14CcT9rm0Gd/OWvykLqHjq7Bu5QIm6QkQuAbG85hSPUiKGIDhM8s+a+tnB7ra/t8w61GHaAsLOl+2W+WVdPpfaWCzBE63BM0fbfTlF4KQo/0RKpY71b+To4p6J73/tXyc1fevA3AAAAABJRU5ErkJggg==") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreen{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAHTElEQVRoQ+1Zb4xcVRX/nZl5u2/LrrO0EFKoBYpVaRu3u/e+3WlDZJdIRLQhNLIiEggxqURIjGmqTTAmWiRpjH4wghq+KIQYupYQEvEDmEVdyu7OfbPbzQaEYqtSwTb4Z3aV7s6b9445mzvm7XRm3oy7oanZ82ny5txzz++ec8+/S7jIiS5y/bEG4EJbcJkFpqenryqXy6cbKBUB+AeANIBuAG8AuAzAn06ePOkNDw+H9dZOTU11h2H4EwB7ALwL4FIA7wFw7O9aSxkAE9H9SqnHazGc50LGGFFQlGuW/pbNZq/aunXrYtICY8xmAD8C8HEAnUn8sf9/oLX+SiKAQqFweRRFvwewvgbzmwA+BOAkgEsAZAG85rpubseOHaVmlTHGfBTAYwA6gKU7WCaiOWaWPT9mv1eLO6S1/mYiAGPMddYtUtXMRPRVx3F+FkXRup07d/7FGDMEYExrHTSrfIVvfHx8Uy6XO22MWae1fu/IkSPpbdu2pRcWFmpakYgeVEo92gyAdQCKADI1HZL581rrp4lIfHPV6Pjx45cEQfCvBgL3a62/nwhgZmbm0lKp9OeYf56rMqmc9v4oikb6+/v/uhoIGigvAUGChdBBrfXhRAD5fL6XiCZsZDhHRAeY+VBVlIiYeTQMw725XG5uJSDqKc/M9xDR1wFsF/lEdKdS6ulEABMTExvS6fQMgCsBhPPz825nZ+dnieinANrjApj5mSAI7t61a9fC/+JSDZS/t62t7WgQBH+0IVoA7GsqjDIz+b4vCyXcnSuXy9fmcrkz+Xz+TgB3ENHeqlN43HXdB7dv3x60AqKR8p7nPXHixIn2YrEo7itRipn5057n/SrRAhbA320eEAGbtdbvyvfJycn16XR6BIBEnzg9PD8//63BwcGwGRBJylcEG2MkbEtUFAS3NgVAmI0xkl23Wt/bppR6rSK0UChcGUXRcwBUFYjDWuuDSffBHpBk82XEzPfKyVc+Wlf+HQDJGQLgDs/zjiZawJrudQBXAzirlNpIRMs2nJiY+HA6nRYQH4kJ7NZaS/htSBLlgiB4jJnFJZeoWnn7jYwxDxCRJK/LmXnI87yXEgHEzHs2m81urlce5PP5fiL6BYAPAmhrJZmNjo5murq6ngdwcy3lK0rKYc7Nze1n5gNE9Cml1HgiAGviguu6A0nlge/7N83Nzf12aGionHTy1f+Pjo5KdBuOu00tGZKpmfmHAJ5oygJjY2Nd3d3di0nKt6rwSvjFK6Iocnp7e/+ZaIGVbHSh1q51ZBfq5Cv7rllgzQIrPIGLwoUkqdVLqssASCKbnp6+ure3VyrSRGLmVHWpkbioRYbx8fErHMcZbKofsGMVKRHu01pLc1+XJMGUSqXPEdGTrZQSIlAycVdX1+FSqXRw9+7dUvXWJFE+k8lI53e71vrZphKZMeYPMvvJZDK3SfNea1GsZpoH8EWl1NFmLTE7O9u2sLDwNoANAA65rvtwrcw/NTV1TRiGp2w/8AXP836eCMAWWicAXENEvymXy/sGBgakvP4v1ajnzzDzl7TWzyX1A1KquK4r7hkf2xxQSn2vem2sHwijKLqlv7//xUQAtpyW6YBMJUJm3hNvJBo0I3XL3fim1kVfAHB9/Dsz3+95nkztlsgClYr1BgBRKpW6oa+v75VEAMJgjDkrNbj8jndCzXZSSXfU930l/bRtWyvsC+KKAEYq98kYIzy3W4abtNajiQCsBQTAByzzsNZ6ZLWUrygwOTl5YyqVEgXjriQjzVcdx9nb09Nz1vf9F5j5EzK5Y+ZBz/NeTgRw7Nixjra2NpkLycBW5jK3OY7zUq2hU6NmJMkK8r/v+3uYWXrsZdMOAM86jnN3EAS/BjAgjgDgy1rrHycCsBNkCZ9X2DtwIxGNVS9cqfLWPalQKNzFzN8GcK2dQCxtRUTSxPQx827L+13P876WCMA27W8BOG82Wlm8GsrHZNHIyEhqy5YtvwTwyXqWI6KHlFKPJAKwYVSiULVZl9aupvJxZexIU+J8TRBE9B2l1DcSAdjLKneg1nh9fzabfbRYLG4qlUpvd3R0bCqXy7tOnTr1VKOHjVqb2jC5j4gmwzAM0+l0OgzDVCqVkvGhuO8yYuZHPM97KBGA7/vXM/O0TBpqMMvo+x17waWGkhLgMrGK1vrJpCRWkRcrD+STvCvIXiJLhgNdddzoAa21vCmcR8uKOWPMRgBSPrRSpcpY8T6l1FNJ0UfeBTKZjNyxlqg60cUXL1PUupBsIO9XMkqX96v4mFvcS0Z+Mg86TUTtzCxvCh1E9BmllPxXk+zrzxQRzTBzJxG5zCzuIjJ32DG+WCOuk1hFqoKlfNSMBWSU5zDzFnEPInqLmSWpbZANARzRWr8jQHt6ev4tAuX34uLi+iiKiknjdskzlepzdna2s729PSgWi24YhuszmYxn99sYRdHSGx0RnUmlUqf7+vqO1zuYVlylJbO/X8xrAN6vk15zoQt90v+3FvgPXUePXrKTg9MAAAAASUVORK5CYII=") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreen:hover{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAFvklEQVRoQ+2ZaaiVVRSGn9fS0iabCNO0eSaosAmplKJRxMiygSQCixQipBKMoDRBon5EI/0pQ8JuRQTVj4omo+FH04/muVum2GCDWVYr3ss+8t3vfud8+3guXi6cBYc7nD2sd6+11/BuMcxFw1x/ugCG2oL9LBAR44HeFkr9B/wMbAOMBT4B9gC+BiZL+rfZ3Ijw+PuB6cA6YFdgAzAy/V41NQB/rpL0QNWAAS4UEVbQm+XKj8B4SX/VTYiIicC9wMnAjnXjC9/fKemaWgARsSfwEbBbxeDPgAOBL4AdgF2AD4ETJP2dq0xEHArcA4yGvjv4D/Br2vOo9P/ycosl3ZQD4IDkFiMqBl8LPASMkfRdREwFVknalKt8Y1xETJDUGxFea0NE2CX9aWbF+ZLuzgEwBlgPbNtEqYuAlZLsl4MmEWGL/t5iwQWS7sgB4Iv1TcE//yyZ1Ke9AOiR9MNgIGihvAOCrWJZKGlZDoCjgTdTZLDy1wGLS1HCkehF4DxJ9t0tlhbKXwbcAByRFp8taWUOgN2B94G9AZ/A9sD5wIPAdqUFngAuBTZuiUu1UH4O8DjwVQrR3nZuVhiNCEcFT3S4swX2k7QmImYDs3zqJRCOzfOBTe2AaKW8pOUR4cPy/tbH9+0cSc/mWMATfkp5wAtMlLQuAXNo7QEcfYqyBLjZFssBUad8IVI5bDsqWs7OAuCREeHselCaeLgkx/o+iQi71lPAsSUQyyQtrLsM6SB8h8oyxydf2Meu/CrgnGGZJcluNUDKpYRN9zEwCVgLjJPUb8OIODiBOKSw2lhJDr8tJSIc5ZzE7JIN6ad8OijrNQ9w8nJynSrppRwAjXhs5e0+lYklIo4DHgP2AUa1k8wiwjnmGeB0YIDyBSv4MB2yHQnPkvRGDgAjfxs4vq48iIhpwCuSXAq0JRHh6HZB0W2qFnCmBu4CludaYCen8zrl29K2w8Hp0o+U9EutBTrca0imdzuyITn2wqZdC3Qt0OEJDAsXcnHXLKmWSwn/PUmSK9JaiYgR5VKjdlKbAyJiL+DU3H7AtIpLhMslublvKinBXAg83E4pkWodZ2J3WO60XPVWSlLend9MSU9mJbKI+DxxPzPcvDdJ8Y2a6TfgCjcguZaIiFHA94ArTnd7S6oyf0TsC3yZ+oFLJD1SCyAVWp8Cnvxy6oRcXm+Winp+DXClK9S6fiAiXKrYPYu0jYu128tzI6LRD7gzPFPS8zkAXAGaHXDF6InTi41Ei2akablbAm8XfQ44rKSMmTezdn2SgLpinQK4nJ8i6fVaAGmyS2nX4JbNnVBuJ1V3RyPCzZD7abetDdmYXNFsRx/PFBEeMzMNmCbJRMIAqWpoDGDnNNIlb89gKV844VMSiKIrmdL8ILEdayPCljotMXeOQq/lADDdZ17IhK1daAbgTqiKdGrajNRZIZ2wSV732GW2w9HGbMcL7kvSJb5a0n05AEzqOnw69hqAT2pVxcSOlE8AbP2LgVvMfiQGorGVm5hjgJPSP26TdH0OADft3wJV3GhjfsfKF1zJILzX08AZLSy3SNLSHACOPnaXslkHXfmiMqnZd5xvBuJWSTfmAHCC8h2ootfdYJshnpASkX+eCKxo9bBRtWkKk3OBt5KrmgO1JUwf2n3LslTSohwAjs/vmmmoGGyGYnW64Da9SwBfdlOBLieyGOtCeeAt/K7gvbyWyQEnuiqZJ8l0zAAph9FxgMuHdqpUx23XTivqoo/fBdIdqxta/r5foit+WQZgF/IlNgFlxfx+VaS57V5O8eaD/Jbmu2Lqw+H3XEn+rlLS6887iTz285ILOruL1zwyrWFrFHWyVXwv+/JRjgVM5Vnp/ZN7GIyTmgsvb/iopNVObJL+8IIpyfnOrK+j2yNidKP6jAiD8CF5Xc+fnA7PXtB4o3Od1SvpvWYH046rtGv2rTK+C2CrHHOLTboW6FqgwxP4Hz4mJ0+J869tAAAAAElFTkSuQmCC") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreenExit{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAADd0lEQVRoQ+2Zz2sdVRTHv+fJBDW6anDVXen6wZszYxYBiYgtFGst3VSDunKjpS0GpUlqfjVpsVVs6aaL0or4YxMVFCJZ2ZLdPUP+gq5bQnTxtNAkfTnlhnnlkmQy9yV9780rudt77tzv5/y4v4bQ4Y06XD/2ANodwec/AiJygJnvtdvTWfPnRkBEJAiCN8rl8kMfiPn5+Ve7u7v3rays0Orq6lJfX99/PuN2auMDoAD+BvA2M6/mTWSMOUtE48D6AjHGzN/kjdlNvy+AnWOOmQ/lTSYiEwDOWzsimgrDcCRvzG76GwGw8/zJzO9sN6GInAMwbW1UdSSKoqndCMwb6wNwGsB39Q+p6h/M/C4R2dTa1AoHYBWKyCkA1+pqiWi2Wq0e7e/vf7yRoJAAKcQggMtuJKIoOtoxACnE0/xOi/SXMAxPuhCFjUBdpIjYVWXSEf0TM3/g9BeriDMKdSPEz8z8vrU1xgwT0YXCrEJZy1iSJKOqOub0/8jMA0mSfKKqNwoPkHp7ioiGHIhRIvpHVa93BEBa2JcAfOlALAHo6RgAKzRJkk9V1S6xL7kpV4idOM31taxaIKJHqmpPnMMA9hcOQES2PDJkAT1XAAC+ZebPfWB3auNzmLObVsNRUNUXVHUujuM7OxXnMy4XwOcj29mIyOuq+lapVGrYCelKpkEQ3CyXy4tbzdN0AGPMxr2iYZ+sra3FcRybtgCIiK2BKw2rdgaUSqWoUqlIkQAepFDdAF7cBq5ERI9rtdr1OI7tmE2t6SmUEYFHAEaexYW/1QC2EF+ru5GIvg7D0D2GNJxprQY4o6qv1I/b6SpzOYqiLxpWng5oOQAzXxWRWwA+dkRfYOb1p5hGW6sBJpn5KytSRG4D+KguWFXHoyhy7xdeLC0F2ChSRL4H8OFuINoKYIUbY34gogHH3eeZef1K6tPaDpCm068A3nMEDzHzxY4BUNWSiPxORO6z5aDPPlGICNQ9bYyZIaLjjudzIQoFkKbTbwCO+UI0HcB9J/LdeY0xs0R02IGYYObRrWqiFQCfEZEtSHsfmGZm+4qxbbM/hQD8BeBNa0hEM2EYnmgLgP3lFARBT1dXly4vL//b29tbzQNIU+llAHeJaLFSqRzJes5vegR8xGbZLCwsHKzVav8z8/0sm0ID+MDvAfh4qZk2exFopnd9vv0ELrXBQO7fD10AAAAASUVORK5CYII=") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreenExit:hover{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAC/ElEQVRoQ+2Zy49NQRCHvx+ReK6IlZ34E7CUiCAR4xEbTLCyQRATYswwb2IQZDYWgojHZpCQECts+ResiQwLj0RClNSkb9Lu3HtPz7mZc8+V6eXt6tP1VVV3VdcVbT7U5vozC9BqD/7/HjCzlZLet9rS9fbP9ICZvQPWSfqRAmFmS4ClMHm+JiR9S1mXVyYFwIBXwEZJv7I2MrPjQH8A6JN0OWtNM/OpAL7HS0mbsjYzswGgN8gNS+rJWtPM/HQAfJ9nkrY22tDMTgMjQaZH0nAzCmatTQE4ClyNPvQU2CbJQ2vKKB2Aa2hmR4DrkbbPgQ5Jv6sJSgkQILqA0dgTkjraBiBAxPHtPz2UtDuGKK0HKkqamd8qg5HS9yXtjebLdYjrHNRqiAeS9gQvnQGGSnML1bvGzOwc0BfN35PUaWYHgRulBwjW9ju+O4JwqM/AWFsABIgLwKkIYgJY1jYAAeJQuGIXVIVcKTKxh8WfBin9J+AVpx/eFWUEqFkyNACKp0rhgWYArkg6kQibSyylmPOklQdibijBX+fSLHFRJkDid+qKmdlaYENOI0zeEcBNSZ9qbVIEQHWuyGOTNZLetgrAz8ClPFpHa1ZL8rf5lFGEB2oBfAxQi4D5DeDmAP7mGJPka0oD4LnDr9imH/xFe8AP4vLIjBclxWXItCOtaIBjwOKo3HaFRyWdnLbmYUHhAJKumdkt4ECk9JCkSitmWixFAwxKOjt5uZvdBvZH2vZLit8XSSBFA/yjpJndAfY1A9FSgOCJu0BnBNErqfIkzfRCywECxCNgR6Rtt6TzmdqHBmyKXG4ZM4sTWc04NzNPWE+AuG3ZlZInSuGBinXMbBzYGVkrE6JUACGcHgPbUyGKAIj7REmZ18y897o5ghiQ5E/bltRChwE/kF7Xj0jyLkbDYWbzgBfA+iA4LmlXqwD8LydvszjAF0lfswBCKC0E3gBeP22p186f8RBKUbaejJmtAr5L+lBPptQAKfCzAClWmkmZWQ/MpHVTvv0X9iFAQGQyevIAAAAASUVORK5CYII=") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-audio{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACrUlEQVRoQ+2ZPYgTURCAZzbBXJnCeL2Cnb87b9MEtPBUrrMQFAtrtT5/ClGs9LBWWz0RtbBUFCF4oJDsbO68wsLA2YqQSmLlvpEHu7IuMdlLcus+yUKKhJfZ+ebnvZl5CJY/aLn+MAP41x7M1QPMfFtr/crzvHfTAs8FoNPp1LTWzwHgqIg0lFLvrQHwfX8BER8DwC6jNCIecF13wwoA3/dvIuKNpLJa60Oe560XGoCZd4rICiKeTCtaeABmPg4AJmRqg6xcaABmvg4At4aFRyEBhoVM4UMoCplHADCfJTEL5YEsIVNID5iQAYCHALCYxeq5b6PMfF5EBAAEESthGK7W6/XPRpFWq7W3VCqtZg2ZcT3g+/6i4zjzIlLSWn/yPO/DIGMNLCWY2Sj/+xGRK0qpZfNDEASnROTFVi0fr8+aA8z8Ld6KEfGt67oLYwMAwEUium8EREn7OgeAjwCwPyo/nrque3YSgAtE9GDaAM1mc65arc4Zuf1+P2w0Gt9jJZl5DQAORt+fENG5wgEw8zUAMB/zbBBRwyqAIAjuiMjlSOlNItpjFUCqWl0josMzgChR/9hGAWBbknjmAdPhDdqa0gfZzAMJKyVP4v8hhJYRcSni+0JEu63ahZj5anyQici6UuqIVQDdbrfS6/UqRulyufyTiH5sF8AlIro37VpoWEHIzGZ2tM+sEZFnSqkzk9RCS0R01wjIsZz+mug53hDRia0AnI4bGgDYISItz/M2jYC8Gpp2u30MEWuO4zha665Sqp0ZYFStX/iWchRAItFGzoHSsrJ2ZFl1mHg6bfVYJeGJv85CC++BpIJZ5kSFC6G0ha0e7mYJqcJ7IOkRay84UhD2XjHFIFZf8iW9YcYoYRi+tO6aNeupOs66iU/icV46zf/MAKZpzXFk/QL+JG1PUPhRiQAAAABJRU5ErkJggg==") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-audio:hover{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACSElEQVRoQ+2Zu4sUQRCHf5+C+gf4yBXMfMYHGvjCzEBQDIzV+HwEohipGKupD0QNDE8UEwUFTe68wEDhTMVUMFJ+0tArzbjs9u3Ojt0wBR0M9MzUV1XdXVWNKhcq1189wP/2YKcesH1d0nPgdVvgnQDY3iTpqaT9kuaAt9UA2D4o6aGkzVHpXcByFQC2r0q60lB2D7BUNIDtjZIeSDoyRNGyAWwfiiET4n6YlAtg+7Kka2PCozyAMSHT5CkLIIbMfUlbMhdmOQCZIVOeB2LI3JN0NNPq6bTZe8D2aUmOY72kN8DnoIXt7eF5FSEzkQdsB+OEsFwr6RPwbpixhqYStoPyqVwAbkaAY5KeTWD5wStZHrD9XdJgK34FhBP9H8kFOAvciQBhn3/RAcBHSTvjfx4DJ6cBOAPcbRvA9gZJYQT5DfwYKGl7UdLu+PwIOFUiwCVJYQRZBuZqA7gh6XxUegXYVhtAmq0uAnt7gLhQm9vorBZx74Hcc6D3QLKH/z2JGyVnlYs4pCfzEe4rsLW2XehicpAtAftqAwiZbhhBfgE/ZwVwDrjddi40KiG0HXpHO+KcJ8CJaXKheeBWBOgqnf6W1BwvgcOrATieFDTrJL0HViJAVwXNgVgPrJH0BfiQDTDKtREiNK7KLSnHASQLLacP1PxcVkWWq8PU3emq2yqJJ0b1Qsv2QKpdZp+orBBqmrfq5m5mSJXtgUZI1XnB0YCo94opCal6L/ka3ghtlIXqrllzT9VJ5k19Ek/y0zbf6QHatOYk3/oDujC8QMWgjf4AAAAASUVORK5CYII=") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-mute{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAKYklEQVRoQ+1Z+3NV1Rld397nXJIbIGBARTQgohGNQZJLEtFSMmpfan10aJ1OZzqd/jOd/g3t9AetD2KLCiigNFUgj/tIQoh1SqBRwVqNYgp53XvP2V9nped0Lpebl/LQmZ4ZZpjkZJ+99voe61tb8C1/5Fu+f/wfwPVm8DIG+vv7H1bVWufcp9baUefcWCqVKi5lo11dXV5NTc06EblPRNoAtABYqapD1tq9zrmelpaWaRHRpaxb6d3LAGSz2d+IyAbn3FljTG+xWEy3t7efW+yHuru7q621t3med7+qPgigGcCdAPIAuowxzyUSiaONjY2Fxa4533uVABwEsA3ARQDHAez1fb9769atn823kKrKyZMnVxUKhdtFJKWq3wWQAnAzgBoAH6vqQWvtH8nAUlmd69uXAcjlci+q6sMA1gL4BMB+Vd2fSCR6K4HYs2eP3bRp0zJjDN/f7Jzjphk2PPkN0YcDACOqekhVO5PJZPZqMvBLAI8BeATAagBnARwRkT97ntdXDmJ4eHj59PT0emPMVufcA9y8iNwBoA6AjQCEAE5dEwDpdPo2EXlQRJ4G8B0A6yImDqjqvnImstnsOlVtFZHvA9gJ4C4AfhnlLAJnABxW1T3V1dWZq8aAqppMJrM+AvE4gB8CuKGUCd/3jzU1NX3JuB8cHNwchuGjBKyq7QCWV4jXawcg/ng6nb7ZWrtTVX8C4CEAtxCEiLzBZAzD8ERNTc1YoVBY6ZxjtXkyYoDvxaETL3ftAfDLvb29t1prufnHohBZQxCqmmVJVNVjQRB8VF1dXeece0hVfxAlcD1wSZe/dgCy2Wy97/sz1topAIWpqambRKTDGPOsqu4AUAvgPICMiBxU1SMzMzMfJJPJG1SVYB+P6n8pE6xCpxebA8PDw4mJiYkqHqLnedPzldxKZfRXqvqliJwtFosjXEBVG0Xkp9wcgMYoLr4EMAjgDRE5PD09PVpTU1MXhiHrP6sY8+G2kjIaJ/HLCyXxiRMnbiwWi7cqk0zkbCqV+nzRfSCbzXay6ojISQDHVq5c+Y+JiYl1zrmnnHNPiwjre5yoFwAwnN6MQfi+v8bzvF0EoaqsYgw7wyokIm86515aCEAul9vinNtujHFBEKTb2tpOLQXApwA+EJHjzrnX8/l8jicbBAE3z4S+P+qs8ZrjERMHABxiOFVVVd2oqruMMT9WVTY2gjgXFYCXAfTNFxa5XI7sMRT57Nu+fXt6KQAosNj2uwB0iki3tXZ1GIbPAOA/hlCybMF/A8gxnBjnQRB86Ps+QbAZMrG3RlqIDfGlCxcu9OzatcsNDg5S4NWqqm+tpbgbb2pqmh4YGHjIOfczfoPvt7S0HF0qgDEROaKqPK1jUeKyzj8jIk1lDJQzsb8ExHrn3E4RmZUmqsqceWV0dLS3oaGhKp/P3yMid3N9Y8xnVKuFQoHgm0WEADwRefGrAPhYRP5CBoIg6BaRWmstw4EMUOhValYEEjNxwDl3yPf9j4MguMkYs9M5x80yPA9fvHhxqKamZo21ltKd+ULBNyoiB/L5fMbzvDuMMVQCy5xzf2ptbe1eKgPUP7MACoVCj+d5q4wxTwCIc2DFPMqUOdEP4HWWWM/zzhWLRXb2LSISOOeGkskkf7YhyitulKLvfRF5XkQOOeduFpEnVLVaRF5taWnpXSqAD6NG1VksFnuXCIDfIog0O7Yx5kgYhp8ZYyipYa39Ynx8fKa2trbBOccDeRbA7QCGVfX3IkLgdSLCUsxcey2VSvVdawD8XtwnWJ2YR2dqa2svnjt3jsrUiwAwJH8OYBMBAPgdN/xNAVCaE2855w4mk8m/UYVGM8RG6iwRoXznxDYLwDm3T0TWiAibZlJEXrseIVTKeJwTrzKcEonEaYIYGhpanc/nycCvRaRRVf8uIn+IBiiG0DcGAMF8QW3IzYVheKitrW2UP0yn048YY34BoDV655UwDF83xqyKc4A5cb0ZiNn4XFXfBfCC53lHtm3bNp7NZjm5dQCgHE+q6lFjzEHn3IqIgerrmcSVCgfdjTe5Kd/3M9PT0zO+76+PbBdK8DOq2kPpEZXRqq+aAx+xjLIPhGHYW9LIWPYoC+brA/O0CLhosnuHGkdV+4wxDC+OpRxlLyQSidGZmZnN1tonnXMJ+kjNzc0EVfGpZKtQC/2LjYzzK0VdJCWeiqrGffN04rm+w3mAQ00imtZo0bxFJpxzRycnJ8fr6uqqwzBU3/enpqamUiKyW0SoYjtTqRTL8JIA0E75K4A9xpjjFFwAqIXIAAGUi7n5Tp2/m4yaG4f9G6OXeUizboeI9J4+ffrT3bt3kyFkMpkHjDEssRKG4StLlRKcxCglqAD3MoRokVhr2fJ3A6CYK3cdFgLAuYGHwpLqAWDcU/9QwB02xuwLw/Dd1tZWgmJ1utcY8wgNBpbelpaWoaUwMCAiH3Hudc4dcc4Ne55H04oDCk+ldKBZaOPx78kAxdowLUsRIQBWn1nLRkTeJtu+7x+n28GJrFAo3Gmttc65kVQqRfCLC6FMJvPbSDWeofCanJz854oVK2hwcd79UVTyKL4Yz4t9ZiJfiALxqIgkVPVRAN8r8Z32s+aLSF8ikaCqTUxOTi6bmpqa7Ojo4N8vDkB/fz/dNYbRuLX2cw4YuVyuyhhzZxiG7SLCmZdT2UYArNOLeWjkciamOfaqqn5ijGmKGOXAE7sdbxtj9pY6gP8di+d2sS+rQl1dXVVr1651Y2NjrqOjg9UDXKSnp2d1IpHgpptVdbuI0DKnilwVzbzzAZm1VTgTR0NSfxAEN/i+z1mA1S2eCRgqByImepubm8cWOp1F39Awod57771ksVjkgH+3qpIpzrtbANy0QGLPAqC85ogYy2P6Tr7vP6iqnDViB5DNjjlBWdHb1tbGPjHns2gA8QpUkhs3blxrjOHGyQJ1zD2RhcIGV2nNS4ytVCrVIyKzJTM2zyIvlt4qq9MsE5W82HIkSwYQh1Qul1sJoF5EtkbOA9mgLGbFKl/3EgATExN9peHZ19e3ng5gpH8uYWIuVzwG8pUAxH+czWbpJqwPw/DeyMjaDoD/Z7MqrVIEMOvMOef2VLofKGMidsU5Qx+iig2CoGf58uXjjY2NE6UsfC0AXIgh1dDQQEeOecEEZ25QL3HKihveggCYY319fbdUYIJ9gobYc6p6prW1lU32f8/XBhCvxAGF10uqui262GNusGpRhvDhnM24fkFE0nMZW2TC8zzmAjs/c4ylukdVOa29H88SVySEyhMqm81yBKSpu4VMiMgOVaX0YCOcva4yxjw/3x0ZmcjlcrxnI5Ps+mtUdYTgwzD8sLwqXTEGSqtUfX09PR/aKIxldvAGOt0A3nHOvRwEwfEdO3ZMz1UbR0ZGlp0/f/4WEam31vL+4by19hQ7dPnNzhUHEG9qYGBgVRAEd0UNj2YYWThjjHmrUChk2tvbKfDmfHjX7Pt+te/7nAnYUKcqhd1VA8Dkrq+vXxcxQdnAewbOAb1BEAwtBCAq16azs3N2j5TalSTFVQMw3+leyd996wH8BxA4v3x6wGifAAAAAElFTkSuQmCC") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-mute:hover{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAHsUlEQVRoQ+2Z969VVRCFv7H33nvvvfcSe2+xxJgY4z9j/Bs0/mABFQXBhl1sgNjQSCyoiL2BDaxs873MJsfDuZd7gfeQxJ3cvAfv3HP22rNmzZo5wRq+Yg3fP/8DWN0RXCYCpZSzgM2Br4GPgW8j4s9hNlpKWQfYETgUOB44GtgMmA1MBF4BFkdEGea+Xdd2AbgF2B2YD0wHZkbEZ4M+qJSyIbArcARwMnAUsC/wO/AscCfwQkT8Meg9+13XBeBx4EjgZ+ClPLGXI+KbfjcqpXivLYA9gWOA0/PnDsDGwOeA977bCAwb1V7P7gIwDpBG2wJfAg/nZ3oXiFLK2sD6ef0+uWlp48kbSddfwAfAVOB+YNZoRuBG4CLgbGDLpNLTwIPAjDaIUsomwM7A4cCJyfm9ga0Bwbn+Bt4fKwDyV+5eAZyayWgkHgGmmBdNEKUUk/U44DzgNGA/YN1WyBWBucATwH3Aq6MZgbXyRAVxMXABsFUrEi9GxILkvbQ5JwGfABiR9ho7APXJpRSTzxO9CjgF2ClBPJrJ+JYSm/Io2Mvyeq+r1Km3G3sAPrmUsktu3pyQItskiFkpiS8CnybfBXl+5sBu8K8qP3YASik+/DdgEaBWbw+cCVwHnJRF7gd5nJEwwT9JmglC2hmRZiRUoQ8HzYFSynrABhk+C17PQtolozcBC/Kklb7FwCHANbk5f3d5zZuAlDI5rdoqj/pvxMwHBaHKaE3ie5eXxKWU7QCjb6WeHxHfDVMH1GlV521AinyUSnR5Jqr6XhP1JzUdeKwBQpqdkSBUMf+tMAjA68YPAOBA4FhgSToBJbhzdUVADyQlrMKTgdfyZJVVE1qLYGWta2FGQpm1UPldT1AQl2ZhE4R2xGgZAetJT1qUUoyeVDQCUyJi5jAA/JJlX99iNF7OgnYl4EcKbdS64Y8JtNJpXoKwGJrYFjm9kPliBDRznq4GT+No3ZCqHoY/zaVr8xnjI+KFYQEojz7M05JGPsQICOCwVgTakdB6mBOCsEIrxdWamDMT0iSapAcBB+T99Vq6Vb8nTQWgqx23IgCMwDONCAhAOghAo9dVrARSI1Hp5H1UMUG4WekpODcqrQQm1aw5ioDfU920Ih6YHuuBiJAFA+fASOY3ABhuXeYljRzYtNcNkwavZ/4YRblvJExM5dTN+38aPTfpx9/nAHdlHgnI52nNJ0WEtn4oAIax5oBfHgaAD5LLJp72WRDSoyb+91ln9s8Dsb5owd8Bbk/gyrFSbK49FBEzxhpAs05IC/NIGbXH0JnKbQFIyeuBvRLAbW44VW+1A2jmxJMZjXd1odlD7JER0L7bsRkBAeh4zQ9ltEZgzCnUjLh0MicmJZ0+TBD2Gkbg5pTm94A7snmSQv8ZAIKR956iEjs1IlQczaJ14obsJ7xGibV4mnOVQpNXRxJ35Zx+Zhpwj5GIiIWlFOVSo6j5ky4WLBNflTMCqtBqS+IuEMqnfshEVe91vUqsYxddsImubJsDyqjFTgBD54AevymjtZDphbQF/epAnxIxYh+sMc9nsiqPUse2VOeqOZRednk2SNrqiREhqKHqwFdZyOxfNXUC0I0KwGFVr0rc6zkWMM2bG7Jbsy6oTEZC2pjo0sUiah/iWObqdLH3R4QyPBQA7fRz2YBXANWNCqBt5vqdun/7NTepadOpujykOu2QItoMI+RyuuFh6ZYnDGslPAHD7Mk4BvTmypoAPBXNXHvqsDwAUsND8aQtYvJeu2Ak9EZq/7SIEJTqdHCOdewjTHjtx8AReCP7XBsVT8gC45BLWfNUmg3N8jZe/24E5Lb38nAEoPrIfYE9VaOd0w6jZHGTbh9EhNcMDODWDKeKIPIvsh/Qo1+Ykqf5ks+DLtXG++lwjazfdRRzbgOENcIaYGLrar1GN/prRPj9gQHIP2lkuNVuGwzlzBOxU7LntSvTCph4gyyHAwLQF1mRPVGpaERteOq0w0hI26UTQGdP/abYXS2lmzWZlkSE6iEnvc7S76alkP2q2q2LtGrK1X6rjlWsATZJWguHZfYCqlvtCeoE0Eg4AbSx6rsGfkNTSnGTqo+8tYsyUsqdPt+mpV9iVwBWWVvEEXuccyersEWrTgAtdkZipHOLCOtEzzUwgHqHdJImtRs3Cs5F7bYsRBa4rnu2B1uO10ckszE8U+Xs3FSnnrPYNpKhATQoZUNu+bcyGwk/5ong2vdtA5DjTXqqSnUo1o5E51S8AlkhAI1oSBsfrm6b4OaGvyuDTZUSQHMyt8z7gVYk6lTc4uaoRoXSTiyMiF+aUVgpABkNtdpCZ16Y4OaGUbHLqnkxCABzzHFkOxLSyeT31dTciLCOLF0rDaARDVVKVXJq4Rsac0PV0ke57LOVUe207906B1sZCXPBnDDHlGpP325tTu0lVgmF2glVSlGlPEUT3Eg4DFbvBVdfVzl56PmOLNXOg/D7RtQa4YxW8PPaqrTKItBSKR8qCLksJWzgLWbaaOvASxFhgexcpRQrsAehSCgWTsOdj/7YfrOzygE0gFjgfN0kDaSVUbAaa6N9xaTB67nyXbP0UQxUrEVdtBtNACa3Rc9ISCOLne5Tdzt7eQBSIEzsukedwTIvxkcNQL/TXZV/W+MB/AMANfVPjBGemwAAAABJRU5ErkJggg==") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-text{font-size:14px;width:30px}.jessibuca-container .jessibuca-speed{font-size:14px;color:#fff}.jessibuca-container .jessibuca-quality-menu-list{position:absolute;left:50%;bottom:100%;visibility:hidden;opacity:0;transform:translateX(-50%);transition:visibility .3s,opacity .3s;background-color:rgba(0,0,0,.5);border-radius:4px}.jessibuca-container .jessibuca-quality-menu-list.jessibuca-quality-menu-shown{visibility:visible;opacity:1}.jessibuca-container .icon-title-tips{pointer-events:none;position:absolute;left:50%;bottom:100%;visibility:hidden;opacity:0;transform:translateX(-50%);transition:visibility .3s ease 0s,opacity .3s ease 0s;background-color:rgba(0,0,0,.5);border-radius:4px}.jessibuca-container .icon-title{display:inline-block;padding:5px 10px;font-size:12px;white-space:nowrap;color:#fff}.jessibuca-container .jessibuca-quality-menu{padding:8px 0}.jessibuca-container .jessibuca-quality-menu-item{display:block;height:25px;margin:0;padding:0 10px;cursor:pointer;font-size:14px;text-align:center;width:50px;color:hsla(0,0%,100%,.5);transition:color .3s,background-color .3s}.jessibuca-container .jessibuca-quality-menu-item:hover{background-color:hsla(0,0%,100%,.2)}.jessibuca-container .jessibuca-quality-menu-item:focus{outline:none}.jessibuca-container .jessibuca-quality-menu-item.jessibuca-quality-menu-item-active{color:#2298fc}.jessibuca-container .jessibuca-volume-panel-wrap{position:absolute;left:50%;bottom:100%;visibility:hidden;opacity:0;transform:translateX(-50%) translateY(22%);transition:visibility .3s,opacity .3s;background-color:rgba(0,0,0,.5);border-radius:4px;height:120px;width:50px;overflow:hidden}.jessibuca-container .jessibuca-volume-panel-wrap.jessibuca-volume-panel-wrap-show{visibility:visible;opacity:1}.jessibuca-container .jessibuca-volume-panel{cursor:pointer;position:absolute;top:21px;height:60px;width:50px;overflow:hidden}.jessibuca-container .jessibuca-volume-panel-text{position:absolute;left:0;top:0;width:50px;height:20px;line-height:20px;text-align:center;color:#fff;font-size:12px}.jessibuca-container .jessibuca-volume-panel-handle{position:absolute;top:48px;left:50%;width:12px;height:12px;border-radius:12px;margin-left:-6px;background:#fff}.jessibuca-container .jessibuca-volume-panel-handle:before{bottom:-54px;background:#fff}.jessibuca-container .jessibuca-volume-panel-handle:after{bottom:6px;background:hsla(0,0%,100%,.2)}.jessibuca-container .jessibuca-volume-panel-handle:after,.jessibuca-container .jessibuca-volume-panel-handle:before{content:"";position:absolute;display:block;left:50%;width:3px;margin-left:-1px;height:60px}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-controls{width:100vh}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-play-big:after{transform:translate(-50%,-50%) rotate(270deg)}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-loading{flex-direction:row}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-loading-text{transform:rotate(270deg)}');class gt{constructor(e){var t;this.player=e,((e,t)=>{e._opt.hasControl&&e._opt.controlAutoHide?e.$container.classList.add("jessibuca-controls-show-auto-hide"):e.$container.classList.add("jessibuca-controls-show");const i=e._opt,o=i.operateBtns;e.$container.insertAdjacentHTML("beforeend",`\n ${i.background?`
    `:""}\n
    \n ${ut.loading}\n ${i.loadingText?`
    ${i.loadingText}
    `:""}\n
    \n ${i.hasControl&&o.play?'
    ':""}\n ${i.hasControl?`\n
    \n
    \n
    00:00:01
    \n
    ${ut.recordStop}
    \n
    \n `:""}\n ${i.hasControl?`\n
    \n
    \n
    \n ${i.showBandwidth?'
    ':""}\n
    \n
    \n ${o.audio?`\n
    \n ${ut.audio}\n ${ut.mute}\n
    \n
    \n
    \n
    \n
    \n
    \n
    \n `:""}\n ${o.play?`
    ${ut.play}
    ${ut.pause}
    `:""}\n ${o.screenshot?`
    ${ut.screenshot}
    `:""}\n ${o.record?`
    ${ut.record}
    ${ut.recordStop}
    `:""}\n ${o.fullscreen?`
    ${ut.fullscreen}
    ${ut.fullscreenExit}
    `:""}\n
    \n
    \n
    \n `:""}\n\n `),Object.defineProperty(t,"$poster",{value:e.$container.querySelector(".jessibuca-poster")}),Object.defineProperty(t,"$loading",{value:e.$container.querySelector(".jessibuca-loading")}),Object.defineProperty(t,"$play",{value:e.$container.querySelector(".jessibuca-play")}),Object.defineProperty(t,"$playBig",{value:e.$container.querySelector(".jessibuca-play-big")}),Object.defineProperty(t,"$recording",{value:e.$container.querySelector(".jessibuca-recording")}),Object.defineProperty(t,"$recordingTime",{value:e.$container.querySelector(".jessibuca-recording-time")}),Object.defineProperty(t,"$recordingStop",{value:e.$container.querySelector(".jessibuca-recording-stop")}),Object.defineProperty(t,"$pause",{value:e.$container.querySelector(".jessibuca-pause")}),Object.defineProperty(t,"$controls",{value:e.$container.querySelector(".jessibuca-controls")}),Object.defineProperty(t,"$fullscreen",{value:e.$container.querySelector(".jessibuca-fullscreen")}),Object.defineProperty(t,"$fullscreen",{value:e.$container.querySelector(".jessibuca-fullscreen")}),Object.defineProperty(t,"$volume",{value:e.$container.querySelector(".jessibuca-volume")}),Object.defineProperty(t,"$volumePanelWrap",{value:e.$container.querySelector(".jessibuca-volume-panel-wrap")}),Object.defineProperty(t,"$volumePanelText",{value:e.$container.querySelector(".jessibuca-volume-panel-text")}),Object.defineProperty(t,"$volumePanel",{value:e.$container.querySelector(".jessibuca-volume-panel")}),Object.defineProperty(t,"$volumeHandle",{value:e.$container.querySelector(".jessibuca-volume-panel-handle")}),Object.defineProperty(t,"$volumeOn",{value:e.$container.querySelector(".jessibuca-icon-audio")}),Object.defineProperty(t,"$volumeOff",{value:e.$container.querySelector(".jessibuca-icon-mute")}),Object.defineProperty(t,"$fullscreen",{value:e.$container.querySelector(".jessibuca-fullscreen")}),Object.defineProperty(t,"$fullscreenExit",{value:e.$container.querySelector(".jessibuca-fullscreen-exit")}),Object.defineProperty(t,"$record",{value:e.$container.querySelector(".jessibuca-record")}),Object.defineProperty(t,"$recordStop",{value:e.$container.querySelector(".jessibuca-record-stop")}),Object.defineProperty(t,"$screenshot",{value:e.$container.querySelector(".jessibuca-screenshot")}),Object.defineProperty(t,"$speed",{value:e.$container.querySelector(".jessibuca-speed")})})(e,this),t=this,Object.defineProperty(t,"controlsRect",{get:()=>t.$controls.getBoundingClientRect()}),ht(e,this),((e,t)=>{const{events:{proxy:i},debug:o}=e;function r(e){const{bottom:i,height:o}=t.$volumePanel.getBoundingClientRect(),{height:r}=t.$volumeHandle.getBoundingClientRect();return Ee(i-e.y-r/2,0,o-r/2)/(o-r)}if(i(window,["click","contextmenu"],i=>{i.composedPath().indexOf(e.$container)>-1?t.isFocus=!0:t.isFocus=!1}),i(window,"orientationchange",()=>{setTimeout(()=>{e.resize()},300)}),i(t.$controls,"click",e=>{e.stopPropagation()}),i(t.$pause,"click",t=>{e.pause()}),i(t.$play,"click",t=>{e.play(),e.resumeAudioAfterPause()}),i(t.$playBig,"click",t=>{e.play(),e.resumeAudioAfterPause()}),i(t.$volume,"mouseover",()=>{t.$volumePanelWrap.classList.add("jessibuca-volume-panel-wrap-show")}),i(t.$volume,"mouseout",()=>{t.$volumePanelWrap.classList.remove("jessibuca-volume-panel-wrap-show")}),i(t.$volumeOn,"click",i=>{i.stopPropagation(),Be(t.$volumeOn,"display","none"),Be(t.$volumeOff,"display","block");const o=e.volume;e.volume=0,e._lastVolume=o}),i(t.$volumeOff,"click",i=>{i.stopPropagation(),Be(t.$volumeOn,"display","block"),Be(t.$volumeOff,"display","none"),e.volume=e.lastVolume||.5}),i(t.$screenshot,"click",t=>{t.stopPropagation(),e.video.screenshot()}),i(t.$volumePanel,"click",t=>{t.stopPropagation(),e.volume=r(t)}),i(t.$volumeHandle,"mousedown",()=>{t.isVolumeDroging=!0}),i(t.$volumeHandle,"mousemove",i=>{t.isVolumeDroging&&(e.volume=r(i))}),i(document,"mouseup",()=>{t.isVolumeDroging&&(t.isVolumeDroging=!1)}),i(t.$record,"click",t=>{t.stopPropagation(),e.recording=!0}),i(t.$recordStop,"click",t=>{t.stopPropagation(),e.recording=!1}),i(t.$recordingStop,"click",t=>{t.stopPropagation(),e.recording=!1}),i(t.$fullscreen,"click",t=>{t.stopPropagation(),e.fullscreen=!0}),i(t.$fullscreenExit,"click",t=>{t.stopPropagation(),e.fullscreen=!1}),e._opt.hasControl&&e._opt.controlAutoHide){i(e.$container,"mouseover",()=>{e.fullscreen||(Be(t.$controls,"display","block"),r())}),i(e.$container,"mousemove",()=>{e.$container&&t.$controls&&(e.fullscreen,"none"===t.$controls.style.display&&(Be(t.$controls,"display","block"),r()))}),i(e.$container,"mouseout",()=>{s(),Be(t.$controls,"display","none")});let o=null;const r=()=>{s(),o=setTimeout(()=>{Be(t.$controls,"display","none")},5e3)},s=()=>{o&&(clearTimeout(o),o=null)}}})(e,this),e._opt.hotKey&&((e,t)=>{const{events:{proxy:i}}=e,o={};function r(e,t){o[e]?o[e].push(t):o[e]=[t]}r(se,()=>{e.fullscreen&&(e.fullscreen=!1)}),r(ae,()=>{e.volume+=.05}),r(ne,()=>{e.volume-=.05}),i(window,"keydown",e=>{if(t.isFocus){const t=document.activeElement.tagName.toUpperCase(),i=document.activeElement.getAttribute("contenteditable");if("INPUT"!==t&&"TEXTAREA"!==t&&""!==i&&"true"!==i){const t=o[e.keyCode];t&&(e.preventDefault(),t.forEach(e=>e()))}}})})(e,this),this.player.debug.log("Control","init")}destroy(){if(this.$poster){if(!Fe(this.$poster)){const e=this.player.$container.querySelector(".jessibuca-poster");e&&this.player.$container&&this.player.$container.removeChild(e)}}if(this.$loading){if(!Fe(this.$loading)){const e=this.player.$container.querySelector(".jessibuca-loading");e&&this.player.$container&&this.player.$container.removeChild(e)}}if(this.$controls){if(!Fe(this.$controls)){const e=this.player.$container.querySelector(".jessibuca-controls");e&&this.player.$container&&this.player.$container.removeChild(e)}}if(this.$recording){if(!Fe(this.$recording)){const e=this.player.$container.querySelector(".jessibuca-recording");e&&this.player.$container&&this.player.$container.removeChild(e)}}if(this.$playBig){if(!Fe(this.$playBig)){const e=this.player.$container.querySelector(".jessibuca-play-big");e&&this.player.$container&&this.player.$container.removeChild(e)}}this.player.$container&&(this.player.$container.classList.remove("jessibuca-controls-show-auto-hide"),this.player.$container.classList.remove("jessibuca-controls-show")),this.player.debug.log("control","destroy")}autoSize(){const e=this.player;e.$container.style.padding="0 0";const t=e.width,i=e.height,o=t/i,r=e.video.$videoElement.width/e.video.$videoElement.height;if(o>r){const o=(t-i*r)/2;e.$container.style.padding=`0 ${o}px`}else{const o=(i-t/r)/2;e.$container.style.padding=`${o}px 0`}}toggleBar(e){this.$controls&&(xe(e)||(e="none"===Ce(this.$controls,"display",!1)),Be(this.$controls,"display",e?"flex":"none"))}getBarIsShow(){let e=!1;return this.$controls&&(e="none"!==Ce(this.$controls,"display",!1)),e}}pt(".jessibuca-container{position:relative;display:block;width:100%;height:100%;overflow:hidden}.jessibuca-container.jessibuca-fullscreen-web{position:fixed;z-index:9999;left:0;top:0;right:0;bottom:0;width:100vw!important;height:100vh!important;background:#000}");class mt{static init(){mt.types={avc1:[],avcC:[],hvc1:[],hvcC:[],btrt:[],dinf:[],dref:[],esds:[],ftyp:[],hdlr:[],mdat:[],mdhd:[],mdia:[],mfhd:[],minf:[],moof:[],moov:[],mp4a:[],mvex:[],mvhd:[],sdtp:[],stbl:[],stco:[],stsc:[],stsd:[],stsz:[],stts:[],tfdt:[],tfhd:[],traf:[],trak:[],trun:[],trex:[],tkhd:[],vmhd:[],smhd:[]};for(let e in mt.types)mt.types.hasOwnProperty(e)&&(mt.types[e]=[e.charCodeAt(0),e.charCodeAt(1),e.charCodeAt(2),e.charCodeAt(3)]);let e=mt.constants={};e.FTYP=new Uint8Array([105,115,111,109,0,0,0,1,105,115,111,109,97,118,99,49]),e.STSD_PREFIX=new Uint8Array([0,0,0,0,0,0,0,1]),e.STTS=new Uint8Array([0,0,0,0,0,0,0,0]),e.STSC=e.STCO=e.STTS,e.STSZ=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0]),e.HDLR_VIDEO=new Uint8Array([0,0,0,0,0,0,0,0,118,105,100,101,0,0,0,0,0,0,0,0,0,0,0,0,86,105,100,101,111,72,97,110,100,108,101,114,0]),e.HDLR_AUDIO=new Uint8Array([0,0,0,0,0,0,0,0,115,111,117,110,0,0,0,0,0,0,0,0,0,0,0,0,83,111,117,110,100,72,97,110,100,108,101,114,0]),e.DREF=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,12,117,114,108,32,0,0,0,1]),e.SMHD=new Uint8Array([0,0,0,0,0,0,0,0]),e.VMHD=new Uint8Array([0,0,0,1,0,0,0,0,0,0,0,0])}static box(e){let t=8,i=null,o=Array.prototype.slice.call(arguments,1),r=o.length;for(let e=0;e>>24&255,i[1]=t>>>16&255,i[2]=t>>>8&255,i[3]=255&t,i.set(e,4);let s=8;for(let e=0;e>>24&255,e>>>16&255,e>>>8&255,255&e,t>>>24&255,t>>>16&255,t>>>8&255,255&t,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255]))}static trak(e){return mt.box(mt.types.trak,mt.tkhd(e),mt.mdia(e))}static tkhd(e){let t=e.id,i=e.duration,o=e.presentWidth,r=e.presentHeight;return mt.box(mt.types.tkhd,new Uint8Array([0,0,0,7,0,0,0,0,0,0,0,0,t>>>24&255,t>>>16&255,t>>>8&255,255&t,0,0,0,0,i>>>24&255,i>>>16&255,i>>>8&255,255&i,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,o>>>8&255,255&o,0,0,r>>>8&255,255&r,0,0]))}static mdia(e){return mt.box(mt.types.mdia,mt.mdhd(e),mt.hdlr(e),mt.minf(e))}static mdhd(e){let t=e.timescale,i=e.duration;return mt.box(mt.types.mdhd,new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,t>>>24&255,t>>>16&255,t>>>8&255,255&t,i>>>24&255,i>>>16&255,i>>>8&255,255&i,85,196,0,0]))}static hdlr(e){let t=null;return t="audio"===e.type?mt.constants.HDLR_AUDIO:mt.constants.HDLR_VIDEO,mt.box(mt.types.hdlr,t)}static minf(e){let t=null;return t="audio"===e.type?mt.box(mt.types.smhd,mt.constants.SMHD):mt.box(mt.types.vmhd,mt.constants.VMHD),mt.box(mt.types.minf,t,mt.dinf(),mt.stbl(e))}static dinf(){return mt.box(mt.types.dinf,mt.box(mt.types.dref,mt.constants.DREF))}static stbl(e){return mt.box(mt.types.stbl,mt.stsd(e),mt.box(mt.types.stts,mt.constants.STTS),mt.box(mt.types.stsc,mt.constants.STSC),mt.box(mt.types.stsz,mt.constants.STSZ),mt.box(mt.types.stco,mt.constants.STCO))}static stsd(e){return"audio"===e.type?mt.box(mt.types.stsd,mt.constants.STSD_PREFIX,mt.mp4a(e)):"avc"===e.videoType?mt.box(mt.types.stsd,mt.constants.STSD_PREFIX,mt.avc1(e)):mt.box(mt.types.stsd,mt.constants.STSD_PREFIX,mt.hvc1(e))}static mp4a(e){let t=e.channelCount,i=e.audioSampleRate,o=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,t,0,16,0,0,0,0,i>>>8&255,255&i,0,0]);return mt.box(mt.types.mp4a,o,mt.esds(e))}static esds(e){let t=e.config||[],i=t.length,o=new Uint8Array([0,0,0,0,3,23+i,0,1,0,4,15+i,64,21,0,0,0,0,0,0,0,0,0,0,0,5].concat([i]).concat(t).concat([6,1,2]));return mt.box(mt.types.esds,o)}static avc1(e){let t=e.avcc;const i=e.codecWidth,o=e.codecHeight;let r=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,i>>>8&255,255&i,o>>>8&255,255&o,0,72,0,0,0,72,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,255,255]);return mt.box(mt.types.avc1,r,mt.box(mt.types.avcC,t))}static hvc1(e){let t=e.avcc;const i=e.codecWidth,o=e.codecHeight;let r=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,i>>>8&255,255&i,o>>>8&255,255&o,0,72,0,0,0,72,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,255,255]);return mt.box(mt.types.hvc1,r,mt.box(mt.types.hvcC,t))}static mvex(e){return mt.box(mt.types.mvex,mt.trex(e))}static trex(e){let t=e.id,i=new Uint8Array([0,0,0,0,t>>>24&255,t>>>16&255,t>>>8&255,255&t,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1]);return mt.box(mt.types.trex,i)}static moof(e,t){return mt.box(mt.types.moof,mt.mfhd(e.sequenceNumber),mt.traf(e,t))}static mfhd(e){let t=new Uint8Array([0,0,0,0,e>>>24&255,e>>>16&255,e>>>8&255,255&e]);return mt.box(mt.types.mfhd,t)}static traf(e,t){let i=e.id,o=mt.box(mt.types.tfhd,new Uint8Array([0,0,0,0,i>>>24&255,i>>>16&255,i>>>8&255,255&i])),r=mt.box(mt.types.tfdt,new Uint8Array([0,0,0,0,t>>>24&255,t>>>16&255,t>>>8&255,255&t])),s=mt.sdtp(e),a=mt.trun(e,s.byteLength+16+16+8+16+8+8);return mt.box(mt.types.traf,o,r,a,s)}static sdtp(e){let t=new Uint8Array(5),i=e.flags;return t[4]=i.isLeading<<6|i.dependsOn<<4|i.isDependedOn<<2|i.hasRedundancy,mt.box(mt.types.sdtp,t)}static trun(e,t){let i=new Uint8Array(28);t+=36,i.set([0,0,15,1,0,0,0,1,t>>>24&255,t>>>16&255,t>>>8&255,255&t],0);let o=e.duration,r=e.size,s=e.flags,a=e.cts;return i.set([o>>>24&255,o>>>16&255,o>>>8&255,255&o,r>>>24&255,r>>>16&255,r>>>8&255,255&r,s.isLeading<<2|s.dependsOn,s.isDependedOn<<6|s.hasRedundancy<<4|s.isNonSync,0,0,a>>>24&255,a>>>16&255,a>>>8&255,255&a],12),mt.box(mt.types.trun,i)}static mdat(e){return mt.box(mt.types.mdat,e)}}mt.init();class ft extends Ve{constructor(e){super(),this.player=e,this.isAvc=!0,this.mediaSource=new window.MediaSource,this.sourceBuffer=null,this.hasInit=!1,this.isInitInfo=!1,this.cacheTrack={},this.timeInit=!1,this.sequenceNumber=0,this.mediaSourceOpen=!1,this.dropping=!1,this.firstRenderTime=null,this.mediaSourceAppendBufferError=!1,this.mediaSourceAppendBufferFull=!1,this.isDecodeFirstIIframe=!1,this.player.video.$videoElement.src=window.URL.createObjectURL(this.mediaSource);const{debug:t,events:{proxy:i}}=e;i(this.mediaSource,"sourceopen",()=>{this.mediaSourceOpen=!0,this.player.emit(F.mseSourceOpen)}),i(this.mediaSource,"sourceclose",()=>{this.player.emit(F.mseSourceClose)}),e.debug.log("MediaSource","init")}destroy(){this.stop(),this.mediaSource=null,this.mediaSourceOpen=!1,this.sourceBuffer=null,this.hasInit=!1,this.isInitInfo=!1,this.sequenceNumber=0,this.cacheTrack=null,this.timeInit=!1,this.mediaSourceAppendBufferError=!1,this.mediaSourceAppendBufferFull=!1,this.isDecodeFirstIIframe=!1,this.off(),this.player.debug.log("MediaSource","destroy")}get state(){return this.mediaSource&&this.mediaSource.readyState}get isStateOpen(){return this.state===ie}get isStateClosed(){return this.state===oe}get isStateEnded(){return this.state===te}get duration(){return this.mediaSource&&this.mediaSource.duration}set duration(e){this.mediaSource.duration=e}decodeVideo(e,t,i,o){const r=this.player;if(!(!r||r&&r.isDestroyedOrClosed()))if(this.hasInit){if(i&&0===e[1]){let t=dt(e.slice(5));const i=this.player.video.videoInfo;i&&i.width&&i.height&&t&&t.codecWidth&&t.codecHeight&&(t.codecWidth!==i.width||t.codecHeight!==i.height)&&(this.player.debug.warn("MediaSource",`width or height is update, width ${i.width}-> ${t.codecWidth}, height ${i.height}-> ${t.codecHeight}`),this.isInitInfo=!1,this.player.video.init=!1)}if(!this.isDecodeFirstIIframe&&i&&(this.isDecodeFirstIIframe=!0),this.isDecodeFirstIIframe){null===this.firstRenderTime&&(this.firstRenderTime=t);const r=t-this.firstRenderTime;this._decodeVideo(e,r,i,o)}else this.player.debug.warn("MediaSource","decodeVideo isDecodeFirstIIframe false")}else if(i&&0===e[1]){const o=15&e[0];if(r.video.updateVideoInfo({encTypeCode:o}),o===P)return void this.emit(O.mediaSourceH265NotSupport);r._times.decodeStart||(r._times.decodeStart=Se()),this._decodeConfigurationRecord(e,t,i,o),this.hasInit=!0}}_decodeConfigurationRecord(e,t,i,o){let r=e.slice(5),s={};s=dt(r);const a={id:1,type:"video",timescale:1e3,duration:0,avcc:r,codecWidth:s.codecWidth,codecHeight:s.codecHeight,videoType:s.videoType},n=mt.generateInitSegment(a);this.isAvc=!0,this.appendBuffer(n.buffer),this.sequenceNumber=0,this.cacheTrack=null,this.timeInit=!1}_decodeVideo(e,t,i,o){const r=this.player;let s=e.slice(5),a=s.byteLength;const n=r.video.$videoElement,A=r._opt.videoBufferDelay;if(n.buffered.length>1&&(this.removeBuffer(n.buffered.start(0),n.buffered.end(0)),this.timeInit=!1),this.dropping&&t-this.cacheTrack.dts>A)this.dropping=!1,this.cacheTrack={};else if(this.cacheTrack&&t>=this.cacheTrack.dts){let e=8+this.cacheTrack.size,i=new Uint8Array(e);i[0]=e>>>24&255,i[1]=e>>>16&255,i[2]=e>>>8&255,i[3]=255&e,i.set(mt.types.mdat,4),i.set(this.cacheTrack.data,8),this.cacheTrack.duration=t-this.cacheTrack.dts;let o=mt.moof(this.cacheTrack,this.cacheTrack.dts),s=new Uint8Array(o.byteLength+i.byteLength);s.set(o,0),s.set(i,o.byteLength),this.appendBuffer(s.buffer),r.handleRender(),r.updateStats({fps:!0,ts:t,buf:r.demux&&r.demux.delay||0}),r._times.videoStart||(r._times.videoStart=Se(),r.handlePlayToRenderTimes())}else r.debug.log("MediaSource","timeInit set false , cacheTrack = {}"),this.timeInit=!1,this.cacheTrack={};this.cacheTrack||(this.cacheTrack={}),this.cacheTrack.id=1,this.cacheTrack.sequenceNumber=++this.sequenceNumber,this.cacheTrack.size=a,this.cacheTrack.dts=t,this.cacheTrack.cts=o,this.cacheTrack.isKeyframe=i,this.cacheTrack.data=s,this.cacheTrack.flags={isLeading:0,dependsOn:i?2:1,isDependedOn:i?1:0,hasRedundancy:0,isNonSync:i?0:1},this.timeInit||1!==n.buffered.length||(r.debug.log("MediaSource","timeInit set true"),this.timeInit=!0,n.currentTime=n.buffered.end(0)),!this.isInitInfo&&n.videoWidth>0&&n.videoHeight>0&&(r.debug.log("MediaSource",`updateVideoInfo: ${n.videoWidth},${n.videoHeight}`),r.video.updateVideoInfo({width:n.videoWidth,height:n.videoHeight}),r.video.initCanvasViewSize(),this.isInitInfo=!0)}appendBuffer(e){const{debug:t,events:{proxy:i}}=this.player;if(null===this.sourceBuffer&&(this.sourceBuffer=this.mediaSource.addSourceBuffer(ee),i(this.sourceBuffer,"error",e=>{t.error("MediaSource","sourceBuffer error",e),this.player.emit(F.mseSourceBufferError,e)})),this.mediaSourceAppendBufferError)t.error("MediaSource","this.mediaSourceAppendBufferError is true");else if(this.mediaSourceAppendBufferFull)t.error("MediaSource","this.mediaSourceAppendBufferFull is true");else if(!1===this.sourceBuffer.updating&&this.isStateOpen)try{this.sourceBuffer.appendBuffer(e)}catch(e){t.warn("MediaSource","this.sourceBuffer.appendBuffer()",e.code,e),22===e.code?(this.stop(),this.mediaSourceAppendBufferFull=!0,this.emit(O.mediaSourceFull)):11===e.code?(this.stop(),this.mediaSourceAppendBufferError=!0,this.emit(O.mediaSourceAppendBufferError)):(t.error("MediaSource","appendBuffer error",e),this.player.emit(F.mseSourceBufferError,e))}else this.isStateClosed?this.player.emitError(O.mseSourceBufferError,"mediaSource is not attached to video or mediaSource is closed"):this.isStateEnded?this.player.emitError(O.mseSourceBufferError,"mediaSource is closed"):!0===this.sourceBuffer.updating&&this.player.emit(F.mseSourceBufferBusy)}stop(){this.abortSourceBuffer(),this.removeSourceBuffer(),this.endOfStream()}dropSourceBuffer(e){const t=this.player.video.$videoElement;this.dropping=e,t.buffered.length>0&&t.buffered.end(0)-t.currentTime>1&&(this.player.debug.warn("MediaSource","dropSourceBuffer",`$video.buffered.end(0) is ${t.buffered.end(0)} - $video.currentTime ${t.currentTime}`),t.currentTime=t.buffered.end(0))}removeBuffer(e,t){if(this.isStateOpen&&!1===this.sourceBuffer.updating)try{this.sourceBuffer.remove(e,t)}catch(e){this.player.debug.warn("MediaSource","removeBuffer() error",e)}else this.player.debug.warn("MediaSource","removeBuffer() this.isStateOpen is",this.isStateOpen,"this.sourceBuffer.updating",this.sourceBuffer.updating)}endOfStream(){const e=this.player.video&&this.player.video.$videoElement;if(this.isStateOpen&&e&&e.readyState>=1)try{this.mediaSource.endOfStream()}catch(e){this.player.debug.warn("MediaSource","endOfStream() error",e)}}abortSourceBuffer(){this.isStateOpen&&this.sourceBuffer&&(this.sourceBuffer.abort(),this.sourceBuffer=null)}removeSourceBuffer(){if(!this.isStateClosed&&this.mediaSource&&this.sourceBuffer)try{this.mediaSource.removeSourceBuffer(this.sourceBuffer)}catch(e){this.player.debug.warn("MediaSource","removeSourceBuffer() error",e)}}getSourceBufferUpdating(){return this.sourceBuffer&&this.sourceBuffer.updating}}const bt=()=>"undefined"!=typeof navigator&&parseFloat((""+(/CPU.*OS ([0-9_]{3,4})[0-9_]{0,1}|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent)||[0,""])[1]).replace("undefined","3_2").replace("_",".").replace("_",""))<10&&!window.MSStream,yt=()=>"wakeLock"in navigator;class vt{constructor(e){if(this.player=e,this.enabled=!1,yt()){this._wakeLock=null;const e=()=>{null!==this._wakeLock&&"visible"===document.visibilityState&&this.enable()};document.addEventListener("visibilitychange",e),document.addEventListener("fullscreenchange",e)}else bt()?this.noSleepTimer=null:(this.noSleepVideo=document.createElement("video"),this.noSleepVideo.setAttribute("title","No Sleep"),this.noSleepVideo.setAttribute("playsinline",""),this._addSourceToVideo(this.noSleepVideo,"webm","data:video/webm;base64,GkXfowEAAAAAAAAfQoaBAUL3gQFC8oEEQvOBCEKChHdlYm1Ch4EEQoWBAhhTgGcBAAAAAAAVkhFNm3RALE27i1OrhBVJqWZTrIHfTbuMU6uEFlSua1OsggEwTbuMU6uEHFO7a1OsghV17AEAAAAAAACkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVSalmAQAAAAAAAEUq17GDD0JATYCNTGF2ZjU1LjMzLjEwMFdBjUxhdmY1NS4zMy4xMDBzpJBlrrXf3DCDVB8KcgbMpcr+RImIQJBgAAAAAAAWVK5rAQAAAAAAD++uAQAAAAAAADLXgQFzxYEBnIEAIrWcg3VuZIaFVl9WUDiDgQEj44OEAmJaAOABAAAAAAAABrCBsLqBkK4BAAAAAAAPq9eBAnPFgQKcgQAitZyDdW5khohBX1ZPUkJJU4OBAuEBAAAAAAAAEZ+BArWIQOdwAAAAAABiZIEgY6JPbwIeVgF2b3JiaXMAAAAAAoC7AAAAAAAAgLUBAAAAAAC4AQN2b3JiaXMtAAAAWGlwaC5PcmcgbGliVm9yYmlzIEkgMjAxMDExMDEgKFNjaGF1ZmVudWdnZXQpAQAAABUAAABlbmNvZGVyPUxhdmM1NS41Mi4xMDIBBXZvcmJpcyVCQ1YBAEAAACRzGCpGpXMWhBAaQlAZ4xxCzmvsGUJMEYIcMkxbyyVzkCGkoEKIWyiB0JBVAABAAACHQXgUhIpBCCGEJT1YkoMnPQghhIg5eBSEaUEIIYQQQgghhBBCCCGERTlokoMnQQgdhOMwOAyD5Tj4HIRFOVgQgydB6CCED0K4moOsOQghhCQ1SFCDBjnoHITCLCiKgsQwuBaEBDUojILkMMjUgwtCiJqDSTX4GoRnQXgWhGlBCCGEJEFIkIMGQcgYhEZBWJKDBjm4FITLQagahCo5CB+EIDRkFQCQAACgoiiKoigKEBqyCgDIAAAQQFEUx3EcyZEcybEcCwgNWQUAAAEACAAAoEiKpEiO5EiSJFmSJVmSJVmS5omqLMuyLMuyLMsyEBqyCgBIAABQUQxFcRQHCA1ZBQBkAAAIoDiKpViKpWiK54iOCISGrAIAgAAABAAAEDRDUzxHlETPVFXXtm3btm3btm3btm3btm1blmUZCA1ZBQBAAAAQ0mlmqQaIMAMZBkJDVgEACAAAgBGKMMSA0JBVAABAAACAGEoOogmtOd+c46BZDppKsTkdnEi1eZKbirk555xzzsnmnDHOOeecopxZDJoJrTnnnMSgWQqaCa0555wnsXnQmiqtOeeccc7pYJwRxjnnnCateZCajbU555wFrWmOmkuxOeecSLl5UptLtTnnnHPOOeecc84555zqxekcnBPOOeecqL25lpvQxTnnnE/G6d6cEM4555xzzjnnnHPOOeecIDRkFQAABABAEIaNYdwpCNLnaCBGEWIaMulB9+gwCRqDnELq0ehopJQ6CCWVcVJKJwgNWQUAAAIAQAghhRRSSCGFFFJIIYUUYoghhhhyyimnoIJKKqmooowyyyyzzDLLLLPMOuyssw47DDHEEEMrrcRSU2011lhr7jnnmoO0VlprrbVSSimllFIKQkNWAQAgAAAEQgYZZJBRSCGFFGKIKaeccgoqqIDQkFUAACAAgAAAAABP8hzRER3RER3RER3RER3R8RzPESVREiVREi3TMjXTU0VVdWXXlnVZt31b2IVd933d933d+HVhWJZlWZZlWZZlWZZlWZZlWZYgNGQVAAACAAAghBBCSCGFFFJIKcYYc8w56CSUEAgNWQUAAAIACAAAAHAUR3EcyZEcSbIkS9IkzdIsT/M0TxM9URRF0zRV0RVdUTdtUTZl0zVdUzZdVVZtV5ZtW7Z125dl2/d93/d93/d93/d93/d9XQdCQ1YBABIAADqSIymSIimS4ziOJElAaMgqAEAGAEAAAIriKI7jOJIkSZIlaZJneZaomZrpmZ4qqkBoyCoAABAAQAAAAAAAAIqmeIqpeIqoeI7oiJJomZaoqZoryqbsuq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq4LhIasAgAkAAB0JEdyJEdSJEVSJEdygNCQVQCADACAAAAcwzEkRXIsy9I0T/M0TxM90RM901NFV3SB0JBVAAAgAIAAAAAAAAAMybAUy9EcTRIl1VItVVMt1VJF1VNVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVN0zRNEwgNWQkAkAEAkBBTLS3GmgmLJGLSaqugYwxS7KWxSCpntbfKMYUYtV4ah5RREHupJGOKQcwtpNApJq3WVEKFFKSYYyoVUg5SIDRkhQAQmgHgcBxAsixAsiwAAAAAAAAAkDQN0DwPsDQPAAAAAAAAACRNAyxPAzTPAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA0jRA8zxA8zwAAAAAAAAA0DwP8DwR8EQRAAAAAAAAACzPAzTRAzxRBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA0jRA8zxA8zwAAAAAAAAAsDwP8EQR0DwRAAAAAAAAACzPAzxRBDzRAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEOAAABBgIRQasiIAiBMAcEgSJAmSBM0DSJYFTYOmwTQBkmVB06BpME0AAAAAAAAAAAAAJE2DpkHTIIoASdOgadA0iCIAAAAAAAAAAAAAkqZB06BpEEWApGnQNGgaRBEAAAAAAAAAAAAAzzQhihBFmCbAM02IIkQRpgkAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAGHAAAAgwoQwUGrIiAIgTAHA4imUBAIDjOJYFAACO41gWAABYliWKAABgWZooAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAYcAAACDChDBQashIAiAIAcCiKZQHHsSzgOJYFJMmyAJYF0DyApgFEEQAIAAAocAAACLBBU2JxgEJDVgIAUQAABsWxLE0TRZKkaZoniiRJ0zxPFGma53meacLzPM80IYqiaJoQRVE0TZimaaoqME1VFQAAUOAAABBgg6bE4gCFhqwEAEICAByKYlma5nmeJ4qmqZokSdM8TxRF0TRNU1VJkqZ5niiKommapqqyLE3zPFEURdNUVVWFpnmeKIqiaaqq6sLzPE8URdE0VdV14XmeJ4qiaJqq6roQRVE0TdNUTVV1XSCKpmmaqqqqrgtETxRNU1Vd13WB54miaaqqq7ouEE3TVFVVdV1ZBpimaaqq68oyQFVV1XVdV5YBqqqqruu6sgxQVdd1XVmWZQCu67qyLMsCAAAOHAAAAoygk4wqi7DRhAsPQKEhKwKAKAAAwBimFFPKMCYhpBAaxiSEFEImJaXSUqogpFJSKRWEVEoqJaOUUmopVRBSKamUCkIqJZVSAADYgQMA2IGFUGjISgAgDwCAMEYpxhhzTiKkFGPOOScRUoox55yTSjHmnHPOSSkZc8w556SUzjnnnHNSSuacc845KaVzzjnnnJRSSuecc05KKSWEzkEnpZTSOeecEwAAVOAAABBgo8jmBCNBhYasBABSAQAMjmNZmuZ5omialiRpmud5niiapiZJmuZ5nieKqsnzPE8URdE0VZXneZ4oiqJpqirXFUXTNE1VVV2yLIqmaZqq6rowTdNUVdd1XZimaaqq67oubFtVVdV1ZRm2raqq6rqyDFzXdWXZloEsu67s2rIAAPAEBwCgAhtWRzgpGgssNGQlAJABAEAYg5BCCCFlEEIKIYSUUggJAAAYcAAACDChDBQashIASAUAAIyx1lprrbXWQGettdZaa62AzFprrbXWWmuttdZaa6211lJrrbXWWmuttdZaa6211lprrbXWWmuttdZaa6211lprrbXWWmuttdZaa6211lprrbXWWmstpZRSSimllFJKKaWUUkoppZRSSgUA+lU4APg/2LA6wknRWGChISsBgHAAAMAYpRhzDEIppVQIMeacdFRai7FCiDHnJKTUWmzFc85BKCGV1mIsnnMOQikpxVZjUSmEUlJKLbZYi0qho5JSSq3VWIwxqaTWWoutxmKMSSm01FqLMRYjbE2ptdhqq7EYY2sqLbQYY4zFCF9kbC2m2moNxggjWywt1VprMMYY3VuLpbaaizE++NpSLDHWXAAAd4MDAESCjTOsJJ0VjgYXGrISAAgJACAQUooxxhhzzjnnpFKMOeaccw5CCKFUijHGnHMOQgghlIwx5pxzEEIIIYRSSsaccxBCCCGEkFLqnHMQQgghhBBKKZ1zDkIIIYQQQimlgxBCCCGEEEoopaQUQgghhBBCCKmklEIIIYRSQighlZRSCCGEEEIpJaSUUgohhFJCCKGElFJKKYUQQgillJJSSimlEkoJJYQSUikppRRKCCGUUkpKKaVUSgmhhBJKKSWllFJKIYQQSikFAAAcOAAABBhBJxlVFmGjCRcegEJDVgIAZAAAkKKUUiktRYIipRikGEtGFXNQWoqocgxSzalSziDmJJaIMYSUk1Qy5hRCDELqHHVMKQYtlRhCxhik2HJLoXMOAAAAQQCAgJAAAAMEBTMAwOAA4XMQdAIERxsAgCBEZohEw0JweFAJEBFTAUBigkIuAFRYXKRdXECXAS7o4q4DIQQhCEEsDqCABByccMMTb3jCDU7QKSp1IAAAAAAADADwAACQXAAREdHMYWRobHB0eHyAhIiMkAgAAAAAABcAfAAAJCVAREQ0cxgZGhscHR4fICEiIyQBAIAAAgAAAAAggAAEBAQAAAAAAAIAAAAEBB9DtnUBAAAAAAAEPueBAKOFggAAgACjzoEAA4BwBwCdASqwAJAAAEcIhYWIhYSIAgIABhwJ7kPfbJyHvtk5D32ych77ZOQ99snIe+2TkPfbJyHvtk5D32ych77ZOQ99YAD+/6tQgKOFggADgAqjhYIAD4AOo4WCACSADqOZgQArADECAAEQEAAYABhYL/QACIBDmAYAAKOFggA6gA6jhYIAT4AOo5mBAFMAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCAGSADqOFggB6gA6jmYEAewAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYIAj4AOo5mBAKMAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCAKSADqOFggC6gA6jmYEAywAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYIAz4AOo4WCAOSADqOZgQDzADECAAEQEAAYABhYL/QACIBDmAYAAKOFggD6gA6jhYIBD4AOo5iBARsAEQIAARAQFGAAYWC/0AAiAQ5gGACjhYIBJIAOo4WCATqADqOZgQFDADECAAEQEAAYABhYL/QACIBDmAYAAKOFggFPgA6jhYIBZIAOo5mBAWsAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCAXqADqOFggGPgA6jmYEBkwAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYIBpIAOo4WCAbqADqOZgQG7ADECAAEQEAAYABhYL/QACIBDmAYAAKOFggHPgA6jmYEB4wAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYIB5IAOo4WCAfqADqOZgQILADECAAEQEAAYABhYL/QACIBDmAYAAKOFggIPgA6jhYICJIAOo5mBAjMAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCAjqADqOFggJPgA6jmYECWwAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYICZIAOo4WCAnqADqOZgQKDADECAAEQEAAYABhYL/QACIBDmAYAAKOFggKPgA6jhYICpIAOo5mBAqsAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCArqADqOFggLPgA6jmIEC0wARAgABEBAUYABhYL/QACIBDmAYAKOFggLkgA6jhYIC+oAOo5mBAvsAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCAw+ADqOZgQMjADECAAEQEAAYABhYL/QACIBDmAYAAKOFggMkgA6jhYIDOoAOo5mBA0sAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCA0+ADqOFggNkgA6jmYEDcwAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYIDeoAOo4WCA4+ADqOZgQObADECAAEQEAAYABhYL/QACIBDmAYAAKOFggOkgA6jhYIDuoAOo5mBA8MAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCA8+ADqOFggPkgA6jhYID+oAOo4WCBA+ADhxTu2sBAAAAAAAAEbuPs4EDt4r3gQHxghEr8IEK"),this._addSourceToVideo(this.noSleepVideo,"mp4","data:video/mp4;base64,AAAAHGZ0eXBNNFYgAAACAGlzb21pc28yYXZjMQAAAAhmcmVlAAAGF21kYXTeBAAAbGliZmFhYyAxLjI4AABCAJMgBDIARwAAArEGBf//rdxF6b3m2Ui3lizYINkj7u94MjY0IC0gY29yZSAxNDIgcjIgOTU2YzhkOCAtIEguMjY0L01QRUctNCBBVkMgY29kZWMgLSBDb3B5bGVmdCAyMDAzLTIwMTQgLSBodHRwOi8vd3d3LnZpZGVvbGFuLm9yZy94MjY0Lmh0bWwgLSBvcHRpb25zOiBjYWJhYz0wIHJlZj0zIGRlYmxvY2s9MTowOjAgYW5hbHlzZT0weDE6MHgxMTEgbWU9aGV4IHN1Ym1lPTcgcHN5PTEgcHN5X3JkPTEuMDA6MC4wMCBtaXhlZF9yZWY9MSBtZV9yYW5nZT0xNiBjaHJvbWFfbWU9MSB0cmVsbGlzPTEgOHg4ZGN0PTAgY3FtPTAgZGVhZHpvbmU9MjEsMTEgZmFzdF9wc2tpcD0xIGNocm9tYV9xcF9vZmZzZXQ9LTIgdGhyZWFkcz02IGxvb2thaGVhZF90aHJlYWRzPTEgc2xpY2VkX3RocmVhZHM9MCBucj0wIGRlY2ltYXRlPTEgaW50ZXJsYWNlZD0wIGJsdXJheV9jb21wYXQ9MCBjb25zdHJhaW5lZF9pbnRyYT0wIGJmcmFtZXM9MCB3ZWlnaHRwPTAga2V5aW50PTI1MCBrZXlpbnRfbWluPTI1IHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCB2YnZfbWF4cmF0ZT03NjggdmJ2X2J1ZnNpemU9MzAwMCBjcmZfbWF4PTAuMCBuYWxfaHJkPW5vbmUgZmlsbGVyPTAgaXBfcmF0aW89MS40MCBhcT0xOjEuMDAAgAAAAFZliIQL8mKAAKvMnJycnJycnJycnXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXiEASZACGQAjgCEASZACGQAjgAAAAAdBmjgX4GSAIQBJkAIZACOAAAAAB0GaVAX4GSAhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZpgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGagC/AySEASZACGQAjgAAAAAZBmqAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZrAL8DJIQBJkAIZACOAAAAABkGa4C/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmwAvwMkhAEmQAhkAI4AAAAAGQZsgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGbQC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBm2AvwMkhAEmQAhkAI4AAAAAGQZuAL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGboC/AySEASZACGQAjgAAAAAZBm8AvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZvgL8DJIQBJkAIZACOAAAAABkGaAC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmiAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZpAL8DJIQBJkAIZACOAAAAABkGaYC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmoAvwMkhAEmQAhkAI4AAAAAGQZqgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGawC/AySEASZACGQAjgAAAAAZBmuAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZsAL8DJIQBJkAIZACOAAAAABkGbIC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBm0AvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZtgL8DJIQBJkAIZACOAAAAABkGbgCvAySEASZACGQAjgCEASZACGQAjgAAAAAZBm6AnwMkhAEmQAhkAI4AhAEmQAhkAI4AhAEmQAhkAI4AhAEmQAhkAI4AAAAhubW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAABDcAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAzB0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAA+kAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAALAAAACQAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAPpAAAAAAABAAAAAAKobWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAB1MAAAdU5VxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAACU21pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAhNzdGJsAAAAr3N0c2QAAAAAAAAAAQAAAJ9hdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAALAAkABIAAAASAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGP//AAAALWF2Y0MBQsAN/+EAFWdCwA3ZAsTsBEAAAPpAADqYA8UKkgEABWjLg8sgAAAAHHV1aWRraEDyXyRPxbo5pRvPAyPzAAAAAAAAABhzdHRzAAAAAAAAAAEAAAAeAAAD6QAAABRzdHNzAAAAAAAAAAEAAAABAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAABAAAAAQAAAIxzdHN6AAAAAAAAAAAAAAAeAAADDwAAAAsAAAALAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAAiHN0Y28AAAAAAAAAHgAAAEYAAANnAAADewAAA5gAAAO0AAADxwAAA+MAAAP2AAAEEgAABCUAAARBAAAEXQAABHAAAASMAAAEnwAABLsAAATOAAAE6gAABQYAAAUZAAAFNQAABUgAAAVkAAAFdwAABZMAAAWmAAAFwgAABd4AAAXxAAAGDQAABGh0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAACAAAAAAAABDcAAAAAAAAAAAAAAAEBAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAQkAAADcAABAAAAAAPgbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAC7gAAAykBVxAAAAAAALWhkbHIAAAAAAAAAAHNvdW4AAAAAAAAAAAAAAABTb3VuZEhhbmRsZXIAAAADi21pbmYAAAAQc21oZAAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAADT3N0YmwAAABnc3RzZAAAAAAAAAABAAAAV21wNGEAAAAAAAAAAQAAAAAAAAAAAAIAEAAAAAC7gAAAAAAAM2VzZHMAAAAAA4CAgCIAAgAEgICAFEAVBbjYAAu4AAAADcoFgICAAhGQBoCAgAECAAAAIHN0dHMAAAAAAAAAAgAAADIAAAQAAAAAAQAAAkAAAAFUc3RzYwAAAAAAAAAbAAAAAQAAAAEAAAABAAAAAgAAAAIAAAABAAAAAwAAAAEAAAABAAAABAAAAAIAAAABAAAABgAAAAEAAAABAAAABwAAAAIAAAABAAAACAAAAAEAAAABAAAACQAAAAIAAAABAAAACgAAAAEAAAABAAAACwAAAAIAAAABAAAADQAAAAEAAAABAAAADgAAAAIAAAABAAAADwAAAAEAAAABAAAAEAAAAAIAAAABAAAAEQAAAAEAAAABAAAAEgAAAAIAAAABAAAAFAAAAAEAAAABAAAAFQAAAAIAAAABAAAAFgAAAAEAAAABAAAAFwAAAAIAAAABAAAAGAAAAAEAAAABAAAAGQAAAAIAAAABAAAAGgAAAAEAAAABAAAAGwAAAAIAAAABAAAAHQAAAAEAAAABAAAAHgAAAAIAAAABAAAAHwAAAAQAAAABAAAA4HN0c3oAAAAAAAAAAAAAADMAAAAaAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAACMc3RjbwAAAAAAAAAfAAAALAAAA1UAAANyAAADhgAAA6IAAAO+AAAD0QAAA+0AAAQAAAAEHAAABC8AAARLAAAEZwAABHoAAASWAAAEqQAABMUAAATYAAAE9AAABRAAAAUjAAAFPwAABVIAAAVuAAAFgQAABZ0AAAWwAAAFzAAABegAAAX7AAAGFwAAAGJ1ZHRhAAAAWm1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALWlsc3QAAAAlqXRvbwAAAB1kYXRhAAAAAQAAAABMYXZmNTUuMzMuMTAw"),this.noSleepVideo.addEventListener("loadedmetadata",()=>{this.noSleepVideo.duration<=1?this.noSleepVideo.setAttribute("loop",""):this.noSleepVideo.addEventListener("timeupdate",()=>{this.noSleepVideo.currentTime>.5&&(this.noSleepVideo.currentTime=Math.random())})}))}_addSourceToVideo(e,t,i){var o=document.createElement("source");o.src=i,o.type=`video/${t}`,e.appendChild(o)}get isEnabled(){return this.enabled}enable(){const e=this.player.debug;if(yt())return navigator.wakeLock.request("screen").then(t=>{this._wakeLock=t,this.enabled=!0,e.log("wakeLock","Wake Lock active."),this._wakeLock.addEventListener("release",()=>{e.log("wakeLock","Wake Lock released.")})}).catch(t=>{throw this.enabled=!1,e.error("wakeLock",`${t.name}, ${t.message}`),t});if(bt())return this.disable(),this.noSleepTimer=window.setInterval(()=>{document.hidden||(window.location.href=window.location.href.split("#")[0],window.setTimeout(window.stop,0))},15e3),this.enabled=!0,Promise.resolve();return this.noSleepVideo.play().then(e=>(this.enabled=!0,e)).catch(e=>{throw this.enabled=!1,e})}disable(){const e=this.player.debug;yt()?(this._wakeLock&&this._wakeLock.release(),this._wakeLock=null):bt()?this.noSleepTimer&&(e.warn("wakeLock","NoSleep now disabled for older iOS devices."),window.clearInterval(this.noSleepTimer),this.noSleepTimer=null):this.noSleepVideo.pause(),this.enabled=!1}}class wt extends Ve{constructor(e,t){var i;super(),this.$container=e,this._opt=Object.assign({},g,t),this.debug=new fe(this),this.debug.log("Player","init"),this._opt.forceNoOffscreen=!0,(Te()||/ipad|android(?!.*mobile)|tablet|kindle|silk/i.test(window.navigator.userAgent.toLowerCase()))&&(this.debug.log("Player","isMobile and set _opt.controlAutoHide false"),this._opt.controlAutoHide=!1),this._opt.autoUseSystemFullScreen&&(ye.isEnabled&&this._opt.useWebFullScreen&&(this.debug.log("Player","screenfull.isEnabled is true and _opt.useWebFullScreen is true , set _opt.useWebFullScreen false"),this._opt.useWebFullScreen=!1),Oe(ye.isEnabled)&&Oe(this._opt.useWebFullScreen)&&(this.debug.log("Player","screenfull.isEnabled is false and _opt.useWebFullScreen is false , set _opt.useWebFullScreen true"),this._opt.useWebFullScreen=!0)),this._opt.useWCS&&(this._opt.useWCS="VideoEncoder"in window),this._opt.useMSE&&(this._opt.useMSE=window.MediaSource&&window.MediaSource.isTypeSupported(ee)),this._opt.wcsUseVideoRender&&(this._opt.wcsUseVideoRender=window.MediaStreamTrackGenerator&&"function"==typeof window.MediaStreamTrackGenerator),this._opt.useMSE&&(this._opt.useWCS&&this.debug.log("Player","useWCS set true->false"),this._opt.forceNoOffscreen||this.debug.log("Player","forceNoOffscreen set false->true"),this._opt.useWCS=!1,this._opt.forceNoOffscreen=!0),this._opt.forceNoOffscreen||("undefined"==typeof OffscreenCanvas?(this._opt.forceNoOffscreen=!0,this._opt.useOffscreen=!1):this._opt.useOffscreen=!0),this._opt.hasAudio||(this._opt.operateBtns.audio=!1),this._opt.hasControl=this._hasControl(),this._loading=!1,this._playing=!1,this._hasLoaded=!1,this._destroyed=!1,this._closed=!1,this._checkHeartTimeout=null,this._checkLoadingTimeout=null,this._checkStatsInterval=null,this._startBpsTime=null,this._isPlayingBeforePageHidden=!1,this._stats={buf:0,fps:0,abps:0,vbps:0,ts:0},this._times={playInitStart:"",playStart:"",streamStart:"",streamResponse:"",demuxStart:"",decodeStart:"",videoStart:"",playTimestamp:"",streamTimestamp:"",streamResponseTimestamp:"",demuxTimestamp:"",decodeTimestamp:"",videoTimestamp:"",allTimestamp:""},this._videoTimestamp=0,this._audioTimestamp=0,i=this,Object.defineProperty(i,"rect",{get:()=>{const e=i.$container.getBoundingClientRect();return e.width=Math.max(e.width,i.$container.clientWidth),e.height=Math.max(e.height,i.$container.clientHeight),e}}),["bottom","height","left","right","top","width"].forEach(e=>{Object.defineProperty(i,e,{get:()=>i.rect[e]})}),this.events=new be(this),this.video=new Ye(this),this._opt.hasAudio&&(this.audio=new qe(this)),this.recorder=new tt(this),this._onlyMseOrWcsVideo()?this.loaded=!0:this.decoderWorker=new it(this),this.stream=null,this.demux=null,this._lastVolume=null,this._opt.useWCS&&(this.webcodecsDecoder=new ct(this),this.loaded=!0),this._opt.useMSE&&(this.mseDecoder=new ft(this),this.loaded=!0),this.control=new gt(this),Te()&&(this.keepScreenOn=new vt(this)),(e=>{try{const t=t=>{Le(t)===e.$container&&(e.emit(M.fullscreen,e.fullscreen),e.fullscreen?e._opt.useMSE&&e.resize():e.resize())};ye.on("change",t),e.events.destroys.push(()=>{ye.off("change",t)})}catch(e){}if(e.on(F.decoderWorkerInit,()=>{e.debug.log("player","has loaded"),e.loaded=!0}),e.on(F.play,()=>{e.loading=!1}),e.on(F.fullscreen,t=>{if(t)try{ye.request(e.$container).then(()=>{}).catch(t=>{Te()&&e._opt.useWebFullScreen&&(e.webFullscreen=!0)})}catch(t){Te()&&e._opt.useWebFullScreen&&(e.webFullscreen=!0)}else try{ye.exit().then(()=>{e.webFullscreen&&(e.webFullscreen=!1)}).catch(()=>{e.webFullscreen=!1})}catch(t){e.webFullscreen=!1}}),Te()&&e.on(F.webFullscreen,t=>{t?e.$container.classList.add("jessibuca-fullscreen-web"):e.$container.classList.remove("jessibuca-fullscreen-web"),e.emit(M.fullscreen,e.fullscreen)}),e.on(F.resize,()=>{e.video&&e.video.resize()}),e._opt.debug){const t=[F.timeUpdate],i=[F.stats,F.playToRenderTimes,F.audioInfo,F.videoInfo];Object.keys(F).forEach(o=>{e.on(F[o],r=>{t.includes(o)||(i.includes(o)&&(r=JSON.stringify(r)),e.debug.log("player events",F[o],r))})}),Object.keys(O).forEach(t=>{e.on(O[t],i=>{e.debug.log("player event error",O[t],i)})})}})(this),(e=>{const{_opt:t,debug:i,events:{proxy:o}}=e;t.supportDblclickFullscreen&&o(e.$container,"dblclick",t=>{const i=Le(t).nodeName.toLowerCase();"canvas"!==i&&"video"!==i||(e.fullscreen=!e.fullscreen)}),o(document,"visibilitychange",()=>{t.hiddenAutoPause&&(i.log("visibilitychange",document.visibilityState,e._isPlayingBeforePageHidden),"visible"===document.visibilityState?e._isPlayingBeforePageHidden&&e.play():(e._isPlayingBeforePageHidden=e.playing,e.playing&&e.pause()))}),o(window,"fullscreenchange",()=>{null!==e.keepScreenOn&&"visible"===document.visibilityState&&e.enableWakeLock()})})(this),this.debug.log("Player","init and version is",p),this._opt.useWCS&&this.debug.log("Player","use WCS"),this._opt.useMSE&&this.debug.log("Player","use MSE"),this._opt.useOffscreen&&this.debug.log("Player","use offscreen");try{this.debug.log("Player options",JSON.stringify(this._opt))}catch(e){}}async destroy(){this._destroyed=!0,this._loading=!1,this._playing=!1,this._hasLoaded=!1,this._lastVolume=null,this._times={playInitStart:"",playStart:"",streamStart:"",streamResponse:"",demuxStart:"",decodeStart:"",videoStart:"",playTimestamp:"",streamTimestamp:"",streamResponseTimestamp:"",demuxTimestamp:"",decodeTimestamp:"",videoTimestamp:"",allTimestamp:""},this.decoderWorker&&(await this.decoderWorker.destroy(),this.decoderWorker=null),this.video&&(this.video.destroy(),this.video=null),this.audio&&(this.audio.destroy(),this.audio=null),this.stream&&(await this.stream.destroy(),this.stream=null),this.recorder&&(this.recorder.destroy(),this.recorder=null),this.control&&(this.control.destroy(),this.control=null),this.webcodecsDecoder&&(this.webcodecsDecoder.destroy(),this.webcodecsDecoder=null),this.mseDecoder&&(this.mseDecoder.destroy(),this.mseDecoder=null),this.demux&&(this.demux.destroy(),this.demux=null),this.events&&(this.events.destroy(),this.events=null),this.clearCheckHeartTimeout(),this.clearCheckLoadingTimeout(),this.clearStatsInterval(),this.releaseWakeLock(),this.keepScreenOn=null,this.resetStats(),this._audioTimestamp=0,this._videoTimestamp=0,this.emit("destroy"),this.off(),this.debug.log("play","destroy end")}set fullscreen(e){Te()&&this._opt.useWebFullScreen?(this.emit(F.webFullscreen,e),setTimeout(()=>{this.updateOption({rotate:e?270:0}),this.resize()},10)):this.emit(F.fullscreen,e)}get fullscreen(){return ye.isFullscreen||this.webFullscreen}set webFullscreen(e){this.emit(F.webFullscreen,e)}get webFullscreen(){return this.$container.classList.contains("jessibuca-fullscreen-web")}set loaded(e){this._hasLoaded=e}get loaded(){return this._hasLoaded}set playing(e){e&&(this.loading=!1),this.playing!==e&&(this._playing=e,this.emit(F.playing,e),this.emit(F.volumechange,this.volume),e?this.emit(F.play):this.emit(F.pause))}get playing(){return this._playing}get volume(){return this.audio&&this.audio.volume||0}set volume(e){e!==this.volume&&(this.audio&&this.audio.setVolume(e),this._lastVolume=e)}get lastVolume(){return this._lastVolume}set loading(e){this.loading!==e&&(this._loading=e,this.emit(F.loading,this._loading))}get loading(){return this._loading}set recording(e){e?this.playing&&this.recorder&&this.recorder.startRecord():this.recorder&&this.recorder.stopRecordAndSave()}get recording(){return!!this.recorder&&this.recorder.recording}set audioTimestamp(e){null!==e&&(this._audioTimestamp=e)}get audioTimestamp(){return this._audioTimestamp}set videoTimestamp(e){null!==e&&(this._videoTimestamp=e,this._opt.useWCS||this._opt.useMSE||this.audioTimestamp&&this.videoTimestamp&&this.audio&&this.audio.emit(F.videoSyncAudio,{audioTimestamp:this.audioTimestamp,videoTimestamp:this.videoTimestamp,diff:this.audioTimestamp-this.videoTimestamp}))}get videoTimestamp(){return this._videoTimestamp}get isDebug(){return!0===this._opt.debug}updateOption(e){this._opt=Object.assign({},this._opt,e)}init(){return new Promise((e,t)=>{this.stream||(this.stream=new _e(this)),this.audio||this._opt.hasAudio&&(this.audio=new qe(this)),this.demux||(this.demux=new at(this)),this._opt.useWCS&&(this.webcodecsDecoder||(this.webcodecsDecoder=new ct(this))),this._opt.useMSE&&(this.mseDecoder||(this.mseDecoder=new ft(this))),this.decoderWorker||this._onlyMseOrWcsVideo()?e():(this.decoderWorker=new it(this),this.debug.log("Player","waiting decoderWorker init"),this.once(F.decoderWorkerInit,()=>{this.debug.log("Player","decoderWorker init success"),this.loaded=!0,e()}))})}play(e,t){return new Promise((i,o)=>{if(!e&&!this._opt.url)return o();this._closed=!1,this.loading=!0,this.playing=!1,this._times.playInitStart=Se(),e||(e=this._opt.url),this._opt.url=e,this.clearCheckHeartTimeout(),this.init().then(()=>{this._times.playStart=Se(),this._opt.isNotMute&&this.mute(!1),this.webcodecsDecoder&&this.webcodecsDecoder.once(O.webcodecsH265NotSupport,()=>{this.emit(O.webcodecsH265NotSupport),this._opt.autoWasm||this.emit(F.error,O.webcodecsH265NotSupport)}),this.mseDecoder&&(this.mseDecoder.once(O.mediaSourceH265NotSupport,()=>{this.emit(O.mediaSourceH265NotSupport),this._opt.autoWasm||this.emit(F.error,O.mediaSourceH265NotSupport)}),this.mseDecoder.once(O.mediaSourceFull,()=>{this.emitError(O.mediaSourceFull)}),this.mseDecoder.once(O.mediaSourceAppendBufferError,()=>{this.emitError(O.mediaSourceAppendBufferError)}),this.mseDecoder.once(O.mediaSourceBufferListLarge,()=>{this.emitError(O.mediaSourceBufferListLarge)}),this.mseDecoder.once(O.mediaSourceAppendBufferEndTimeout,()=>{this.emitError(O.mediaSourceAppendBufferEndTimeout)})),this.enableWakeLock(),this.stream.fetchStream(e,t),this.checkLoadingTimeout(),this.stream.once(O.fetchError,e=>{this.emitError(O.fetchError,e)}),this.stream.once(O.websocketError,e=>{this.emitError(O.websocketError,e)}),this.stream.once(F.streamEnd,e=>{this.emitError(F.streamEnd,e)}),this.stream.once(F.streamSuccess,()=>{i(),this._times.streamResponse=Se(),this.video.play(),this.checkStatsInterval()})}).catch(e=>{o(e)})})}close(){return new Promise((e,t)=>{this._close().then(()=>{this.video&&this.video.clearView(),e()})})}resumeAudioAfterPause(){this.lastVolume&&(this.volume=this.lastVolume)}_close(){return new Promise((e,t)=>{this._closed=!0,this.stream&&(this.stream.destroy(),this.stream=null),this.demux&&(this.demux.destroy(),this.demux=null),this.decoderWorker&&(this.decoderWorker.destroy(),this.decoderWorker=null),this.webcodecsDecoder&&(this.webcodecsDecoder.destroy(),this.webcodecsDecoder=null),this.mseDecoder&&(this.mseDecoder.destroy(),this.mseDecoder=null),this.audio&&(this.audio.destroy(),this.audio=null),this.clearCheckHeartTimeout(),this.clearCheckLoadingTimeout(),this.clearStatsInterval(),this.playing=!1,this.loading=!1,this.recording=!1,this.video&&(this.video.resetInit(),this.video.pause(!0)),this.releaseWakeLock(),this.resetStats(),this._audioTimestamp=0,this._videoTimestamp=0,this._times={playInitStart:"",playStart:"",streamStart:"",streamResponse:"",demuxStart:"",decodeStart:"",videoStart:"",playTimestamp:"",streamTimestamp:"",streamResponseTimestamp:"",demuxTimestamp:"",decodeTimestamp:"",videoTimestamp:"",allTimestamp:""},setTimeout(()=>{e()},0)})}pause(e=!1){return e?this.close():this._close()}mute(e){if(this.audio){const t=this.audio.getLastVolume();this.audio.mute(e),this._lastVolume=e?0:t||.5}}resize(){this.video.resize()}startRecord(e,t){this.recording||(this.recorder.setFileName(e,t),this.recording=!0)}stopRecordAndSave(){this.recording&&(this.recording=!1)}_hasControl(){let e=!1,t=!1;return Object.keys(this._opt.operateBtns).forEach(e=>{this._opt.operateBtns[e]&&(t=!0)}),(this._opt.showBandwidth||this._opt.text||t)&&(e=!0),e}_onlyMseOrWcsVideo(){return!1===this._opt.hasAudio&&(this._opt.useMSE||this._opt.useWCS&&!this._opt.useOffscreen)}checkHeart(){this.clearCheckHeartTimeout(),this.checkHeartTimeout()}checkHeartTimeout(){this._checkHeartTimeout=setTimeout(()=>{if(this.playing){if(0!==this._stats.fps)return;if(this.isDestroyedOrClosed())return;this.pause().then(()=>{this.emit(F.timeout,F.delayTimeout),this.emit(F.delayTimeout)})}},1e3*this._opt.heartTimeout)}checkStatsInterval(){this._checkStatsInterval=setInterval(()=>{this.updateStats()},1e3)}clearCheckHeartTimeout(){this._checkHeartTimeout&&(clearTimeout(this._checkHeartTimeout),this._checkHeartTimeout=null)}checkLoadingTimeout(){const e=parseFloat((Math.floor(11*Math.random())-5)/10),t=this._opt.loadingTimeout+e;this.debug.log("Player",`checkLoadingTimeout loadingTimeout is ${this._opt.loadingTimeout} and newLoadingTimeout is ${t}`),this._checkLoadingTimeout=setTimeout(()=>{this.playing||this.isDestroyedOrClosed()||this.pause().then(()=>{this.emit(F.timeout,F.loadingTimeout),this.emit(F.loadingTimeout)})},1e3*t)}clearCheckLoadingTimeout(){this._checkLoadingTimeout&&(clearTimeout(this._checkLoadingTimeout),this._checkLoadingTimeout=null)}clearStatsInterval(){this._checkStatsInterval&&(clearInterval(this._checkStatsInterval),this._checkStatsInterval=null)}handleRender(){this.isDestroyedOrClosed()||(this.loading&&(this.emit(F.start),this.loading=!1,this.clearCheckLoadingTimeout()),this.playing||(this.playing=!0),this.checkHeart())}updateStats(e={}){if(this.isDestroyedOrClosed())return;this._startBpsTime||(this._startBpsTime=Se()),De(e.ts)&&(this._stats.ts=e.ts),De(e.buf)&&(this._stats.buf=e.buf),e.fps&&(this._stats.fps+=1),e.abps&&(this._stats.abps+=e.abps),e.vbps&&(this._stats.vbps+=e.vbps);const t=Se();t-this._startBpsTime<1e3||(this.emit(F.stats,this._stats),this.emit(F.performance,function(e){let t=0;return e>=24?t=2:e>=15&&(t=1),t}(this._stats.fps)),this._stats.fps=0,this._stats.abps=0,this._stats.vbps=0,this._startBpsTime=t)}resetStats(){this._startBpsTime=null,this._stats={buf:0,fps:0,abps:0,vbps:0,ts:0}}enableWakeLock(){this._opt.keepScreenOn&&this.keepScreenOn&&this.keepScreenOn.enable()}releaseWakeLock(){this._opt.keepScreenOn&&this.keepScreenOn&&this.keepScreenOn.disable()}handlePlayToRenderTimes(){if(this.isDestroyedOrClosed())return;const e=this._times;e.playTimestamp=e.playStart-e.playInitStart,e.streamTimestamp=e.streamStart-e.playStart,e.streamResponseTimestamp=e.streamResponse-e.streamStart,e.demuxTimestamp=e.demuxStart-e.streamResponse,e.decodeTimestamp=e.decodeStart-e.demuxStart,e.videoTimestamp=e.videoStart-e.decodeStart,e.allTimestamp=e.videoStart-e.playInitStart,this.emit(F.playToRenderTimes,e)}getOption(){return this._opt}emitError(e,t=""){this.emit(F.error,e,t),this.emit(e,t)}isControlBarShow(){const e=this._opt.hasControl,t=this._opt.controlAutoHide;let i=e&&!t;return i&&this.control&&(i=this.control.getBarIsShow()),i}getControlBarShow(){let e=!1;return this.control&&(e=this.control.getBarIsShow()),e}toggleControlBar(e){this.control&&(this.control.toggleBar(e),this.resize())}isDestroyed(){return this._destroyed}isClosed(){return this._closed}isDestroyedOrClosed(){return this.isDestroyed()||this.isClosed()}}class St extends Ve{constructor(e){super();let t=e,i=e.container;if(this.TAG_NAME="Jessibuca","string"==typeof e.container&&(i=document.querySelector(e.container)),!i)throw new Error("Jessibuca need container option");if("CANVAS"===i.nodeName||"VIDEO"===i.nodeName)throw new Error(`Jessibuca container type can not be ${i.nodeName} type`);if(t.videoBuffer>=t.heartTimeout)throw new Error(`Jessibuca videoBuffer ${t.videoBuffer}s must be less than heartTimeout ${t.heartTimeout}s`);if(this._checkHasCreated(i))throw new Error("Jessibuca container has been created and can not be created again",i);if(t.videoBuffer>10&&console.warn("Jessibuca",`videoBuffer ${t.videoBuffer}s is too long, will black screen for ${t.videoBuffer}s , it is recommended to set it to less than 10s`),!i.classList)throw new Error("Jessibuca container option must be DOM Element");var o,r,s;i.classList.add("jessibuca-container"),o=i,r=h,s="xxxxxxxxxxxx4xxx".replace(/[xy]/g,function(e){var t=16*Math.random()|0;return("x"==e?t:3&t|8).toString(16)}),o&&(o.dataset?o.dataset[r]=s:o.setAttribute("data-"+r,s)),delete t.container,delete t.url,t.forceNoOffscreen=!0,Te()&&(t.controlAutoHide=!1),De(t.videoBuffer)&&(t.videoBuffer=1e3*Number(t.videoBuffer)),De(t.timeout)&&(Ie(t.loadingTimeout)&&(t.loadingTimeout=t.timeout),Ie(t.heartTimeout)&&(t.heartTimeout=t.timeout)),this._opt=t,this.$container=i,this._loadingTimeoutReplayTimes=0,this._heartTimeoutReplayTimes=0,this.initDecoderWorkerTimeout=null,this._destroyed=!1,this.events=new be(this),this.debug=new fe(this),this._initPlayer(i,t),console.log(`Jessibuca version: ${p}`)}async destroy(){var e,t;this._destroyed=!0,this.off(),this._clearInitDecoderWorkerTimeout(),this.player&&(await this.player.destroy(),this.player=null),this.events&&(this.events.destroy(),this.events=null),this.$container&&(this.$container.classList.remove("jessibuca-container"),this.$container.classList.remove("jessibuca-fullscreen-web"),e=this.$container,t=h,e&&(e.dataset?delete e.dataset[t]:e.removeAttribute("data-"+t)),this.$container.innerHTML="",this.$container=null),this._opt={},this._loadingTimeoutReplayTimes=0,this._heartTimeoutReplayTimes=0}_initPlayer(e,t){this.player=new wt(e,t);try{this.debug.log("jessibuca","_initPlayer",JSON.stringify(this.player.getOption()))}catch(e){}this._bindEvents()}_resetPlayer(e={}){this.player.destroy(),this.player=null,this._opt=Object.assign(this._opt,e),this._opt.url="",this._initPlayer(this.$container,this._opt)}_bindEvents(){Object.keys(M).forEach(e=>{this.player.on(M[e],t=>{this.emit(e,t)})})}isDestroyed(){return this._destroyed}setDebug(e){this.debug.log(this.TAG_NAME,"setDebug()",e),this.player.updateOption({debug:!!e})}mute(){this.debug.log(this.TAG_NAME,"mute()"),this.player.mute(!0)}cancelMute(){this.debug.log(this.TAG_NAME,"cancelMute()"),this.player.mute(!1)}setVolume(e){this.debug.log(this.TAG_NAME,"setVolume()",e),this.player.volume=e}audioResume(){this.debug.log(this.TAG_NAME,"audioResume()"),this.player.audio&&this.player.audio.audioEnabled(!0)}setTimeout(e){this.debug.log(this.TAG_NAME,"setTimeout()",e),e=Number(e),this.player.updateOption({timeout:e,loadingTimeout:e,heartTimeout:e})}setScaleMode(e){e=Number(e),this.debug.log(this.TAG_NAME,"setScaleMode()",e);let t={isFullResize:!1,isResize:!1};switch(e){case z:t.isFullResize=!1,t.isResize=!1;break;case Y:t.isFullResize=!1,t.isResize=!0;break;case X:t.isFullResize=!0,t.isResize=!0}this.player.updateOption(t),this.resize()}pause(){return new Promise((e,t)=>{this.debug.log(this.TAG_NAME,"pause()"),this.player?this.player.pause().then(()=>{e()}).catch(e=>{t(e)}):t("player is null")})}async close(){return this.debug.log(this.TAG_NAME,"close()"),await this.destroy(),!0}clearView(){this.debug.log(this.TAG_NAME,"clearView()"),this.player.video.clearView()}play(e,t={}){return new Promise((i,o)=>{try{this.debug.log(this.TAG_NAME,`play() ${e}`,JSON.stringify(t))}catch(i){this.debug.log(this.TAG_NAME,`play() ${e}`,t)}if(!this.isDestroyed())return e||this._opt.url?void(e?this._opt.url?e===this._opt.url?this.player.playing?i():(this.clearView(),this.player.play(this._opt.url,this._opt.playOptions).then(()=>{i(),this.player.resumeAudioAfterPause()}).catch(e=>{this.debug.warn("jessibuca","pause -> play and play error",e),this.player.pause().then(()=>{o(e)})})):this.player.pause().then(()=>{this.clearView(),this._play(e,t).then(()=>{i()}).catch(e=>{this.debug.warn("jessibuca","this._play error",e),o(e)})}).catch(e=>{this.debug.warn("jessibuca","this._opt.url is null and pause error",e),o(e)}):this._play(e,t).then(()=>{i()}).catch(e=>{this.debug.warn("jessibuca","this._play error",e),o(e)}):this.player.play(this._opt.url,this._opt.playOptions).then(()=>{i(),this.player.resumeAudioAfterPause()}).catch(e=>{this.debug.warn("jessibuca","url is null and play error",e),this.player.pause().then(()=>{o(e)})})):(this.emit(F.error,O.playError),void o("play url is empty"));o("Jessibuca is destroyed")})}_play(e,t={}){return new Promise((i,o)=>{this._opt.url=e,this._opt.playOptions=t;const r=0===e.indexOf("http"),s=r?A:n,a=r||-1!==e.indexOf(".flv")||this.player._opt.isFlv?d:c;this.player.updateOption({protocol:s,demuxType:a}),this.player.once(O.webglAlignmentError,()=>{this.pause().then(()=>{this.debug.log("Jessibuca","webglAlignmentError"),this._resetPlayer({openWebglAlignment:!0}),this.play(e,t).then(()=>{this.debug.log("Jessibuca","webglAlignmentError and play success")}).catch(()=>{this.debug.log("Jessibuca","webglAlignmentError and play error")})})}),this.player.once(O.webglContextLostError,()=>{this.pause().then(()=>{this.debug.log("Jessibuca","webglContextLostError and paused")}).catch(()=>{this.debug.warn("Jessibuca","webglContextLostError and paused error")})}),this.player.once(O.webglInitError,()=>{this.pause().then(()=>{this.debug.log("Jessibuca","webglInitError and paused")}).catch(()=>{this.debug.warn("Jessibuca","webglInitError and paused error")})}),this.player.once(O.mediaSourceH265NotSupport,()=>{this.pause().then(()=>{this.player._opt.autoWasm?(this.debug.log("Jessibuca","auto wasm [mse-> wasm] reset player and play"),this._resetPlayer({useMSE:!1,useWCS:!1}),this.play(e,t).then(()=>{this.debug.log("Jessibuca","auto wasm [mse-> wasm] reset player and play success")}).catch(()=>{this.debug.log("Jessibuca","auto wasm [mse-> wasm] reset player and play error")})):this.debug.log("Jessibuca","media source h265 not support and paused")})}),this.player.once(O.mediaSourceFull,()=>{this.pause().then(()=>{this.debug.log("Jessibuca","media source full"),this._resetPlayer(),this.play(e,t).then(()=>{this.debug.log("Jessibuca","media source full and reset player and play success")}).catch(()=>{this.debug.warn("Jessibuca","media source full and reset player and play error")})})}),this.player.once(O.mediaSourceAppendBufferError,()=>{this.pause().then(()=>{this.debug.log("Jessibuca","media source append buffer error"),this._resetPlayer(),this.play(e,t).then(()=>{this.debug.log("Jessibuca","media source append buffer error and reset player and play success")}).catch(()=>{this.debug.warn("Jessibuca","media source append buffer error and reset player and play error")})})}),this.player.once(O.mediaSourceBufferListLarge,()=>{this.pause().then(()=>{this.debug.log("Jessibuca","media source buffer list large"),this._resetPlayer(),this.play(e,t).then(()=>{this.debug.log("Jessibuca","media source buffer list large and reset player and play success")}).catch(()=>{this.debug.warn("Jessibuca","media source buffer list large and reset player and play error")})})}),this.player.once(O.mediaSourceAppendBufferEndTimeout,()=>{this.pause().then(()=>{this.debug.log("Jessibuca","media source append buffer end timeout"),this._resetPlayer(),this.play(e,t).then(()=>{this.debug.log("Jessibuca","media source append buffer end timeout and reset player and play success")}).catch(()=>{this.debug.warn("Jessibuca","media source append buffer end timeout and reset player and play error")})})}),this.player.once(O.mseSourceBufferError,()=>{this.pause().then(()=>{this.player._opt.autoWasm?(this.debug.log("Jessibuca","auto wasm [mse-> wasm] reset player and play"),this._resetPlayer({useMSE:!1}),this.play(e,t).then(()=>{this.debug.log("Jessibuca","auto wasm [mse-> wasm] reset player and play success")}).catch(()=>{this.debug.warn("Jessibuca","auto wasm [mse-> wasm] reset player and play error")})):this.debug.log("Jessibuca","mse source buffer error and paused")})}),this.player.once(O.webcodecsH265NotSupport,()=>{this.pause().then(()=>{this.player._opt.autoWasm?(this.debug.log("Jessibuca","auto wasm [wcs-> wasm] reset player and play"),this._resetPlayer({useWCS:!1,useMSE:!1}),this.play(e,t).then(()=>{this.debug.log("Jessibuca","auto wasm [wcs-> wasm] reset player and play success")}).catch(()=>{this.debug.warn("Jessibuca","auto wasm [wcs-> wasm] reset player and play error")})):this.debug.log("Jessibuca","webcodecs h265 not support and paused")})}),this.player.once(O.webcodecsWidthOrHeightChange,()=>{this.pause().then(()=>{this.debug.log("Jessibuca","webcodecs Width Or Height Change reset player and play"),this._resetPlayer({useWCS:!0}),this.play(e,t).then(()=>{this.debug.log("Jessibuca","webcodecs Width Or Height Change reset player and play success")}).catch(()=>{this.debug.warn("Jessibuca","webcodecs Width Or Height Change reset player and play error")})})}),this.player.once(O.webcodecsDecodeError,()=>{this.pause().then(()=>{this.player._opt.autoWasm?(this.debug.log("Jessibuca","webcodecs decode error reset player and play"),this._resetPlayer({useWCS:!1}),this.play(e,t).then(()=>{this.debug.log("Jessibuca","webcodecs decode error reset player and play success")}).catch(()=>{this.debug.warn("Jessibuca","webcodecs decode error reset player and play error")})):this.debug.log("Jessibuca","webcodecs decode error and paused")})}),this.player.once(O.webcodecsConfigureError,()=>{this.pause().then(()=>{this.player._opt.autoWasm?(this.debug.log("Jessibuca","webcodecs Configure error reset player and play"),this._resetPlayer({useWCS:!1}),this.play(e,t).then(()=>{this.debug.log("Jessibuca","webcodecs Configure error reset player and play success")}).catch(()=>{this.debug.warn("Jessibuca","webcodecs Configure error reset player and play error")})):this.debug.log("Jessibuca","webcodecs Configure error and paused")})}),this.player.once(O.wasmDecodeError,()=>{this.player._opt.wasmDecodeErrorReplay?this.pause().then(()=>{this.debug.log("Jessibuca","wasm decode error and reset player and play"),this._resetPlayer({useWCS:!1}),this.play(e,t).then(()=>{this.debug.log("Jessibuca","wasm decode error and reset player and play success")}).catch(e=>{this.debug.warn("Jessibuca","wasm decode error and reset player and play error")})}):this.pause().then(()=>{this.debug.log("Jessibuca","wasm decode error and paused")}).catch(e=>{this.debug.warn("Jessibuca","wasm decode error and paused error",e)})}),this.player.once(O.fetchError,e=>{this.pause().then(()=>{this.debug.log("Jessibuca","fetch error and pause play")}).catch(e=>{this.debug.warn("Jessibuca","fetch error and pause play error",e)})}),this.player.once(O.websocketError,()=>{this.pause().then(()=>{this.debug.log("Jessibuca","websocket Error and pause play")}).catch(e=>{this.debug.warn("Jessibuca","websocket Error and pause play error",e)})}),this.player.once(F.streamEnd,()=>{this.pause().then(()=>{this.debug.log("Jessibuca","stream End and pause play")}).catch(e=>{this.debug.warn("Jessibuca","stream End and pause play error",e)})}),this.player.on(F.delayTimeout,()=>{this.player._opt.heartTimeoutReplay&&(this._heartTimeoutReplayTimes{this._heartTimeoutReplayTimes=0}).catch(()=>{}))}),this.player.on(F.loadingTimeout,()=>{this.player._opt.loadingTimeoutReplay&&(this._loadingTimeoutReplayTimes{this._loadingTimeoutReplayTimes=0}).catch(()=>{}))}),this.hasLoaded()?this.player.play(e,t).then(()=>{i()}).catch(e=>{this.debug.warn("Jessibuca","hasLoaded and play error",e),this.player&&this.player.pause().then(()=>{o(e)})}):(this.debug.log("Jessibuca","_play ant waiting decoderWorkerInit"),this._checkInitDecoderWorkerTimeout(),this.player.once(F.decoderWorkerInit,()=>{this._clearInitDecoderWorkerTimeout(),this.isDestroyed()||(this.debug.log("Jessibuca","_play decoderWorkerInit success and play"),this.player.play(e,t).then(()=>{i()}).catch(e=>{this.debug.warn("Jessibuca","decoderWorkerInit and play error",e),this.player&&this.player.pause().then(()=>{o(e)})}))}))})}resize(){this.player.resize()}setBufferTime(e){e=Number(e),this.debug.log(this.TAG_NAME,"setBufferTime()",e),this.player.updateOption({videoBuffer:1e3*e}),this.player.decoderWorker&&this.player.decoderWorker.updateWorkConfig({key:"videoBuffer",value:1e3*e})}setRotate(e){e=parseInt(e,10),this.debug.log(this.TAG_NAME,"setRotate()",e);this.player._opt.rotate!==e&&-1!==[0,90,180,270].indexOf(e)&&(this.player.updateOption({rotate:e}),this.resize())}hasLoaded(){return this.player.loaded}setKeepScreenOn(){this.debug.log(this.TAG_NAME,"setKeepScreenOn()"),this.player.updateOption({keepScreenOn:!0})}setFullscreen(e){this.debug.log(this.TAG_NAME,"setFullscreen()");const t=!!e;this.player.fullscreen!==t&&(this.player.fullscreen=t)}screenshot(e,t,i,o){return this.debug.log(this.TAG_NAME,"screenshot()",e,t,i,o),this.player.video?this.player.video.screenshot(e,t,i,o):""}startRecord(e,t){return new Promise((i,o)=>{this.debug.log(this.TAG_NAME,"startRecord()",e,t),this.player.playing?(this.player.startRecord(e,t),i()):o()})}stopRecordAndSave(){this.debug.log(this.TAG_NAME,"stopRecordAndSave()"),this.player.recording&&this.player.stopRecordAndSave()}isPlaying(){return!!this.player&&this.player.playing}isMute(){return!this.player.audio||this.player.audio.isMute}isRecording(){return this.player.recorder.recording}_checkHasCreated(e){if(!e)return!1;const t=function(e,t){return e?e.dataset?e.dataset[t]:e.getAttribute("data-"+t):""}(e,h);return!!t}toggleControlBar(e){this.isDestroyed()||(this.debug.log(this.TAG_NAME,"toggleControlBar()",e),this.player&&this.player.toggleControlBar(e))}getControlBarShow(){if(this.isDestroyed())return!1;let e=!1;return this.player&&(e=this.player.getControlBarShow()),e}kbps2Speed(e){return Number((1e3*e/8/1024).toFixed(2))}_clearInitDecoderWorkerTimeout(){this.initDecoderWorkerTimeout&&(clearTimeout(this.initDecoderWorkerTimeout),this.initDecoderWorkerTimeout=null)}_checkInitDecoderWorkerTimeout(){this._clearInitDecoderWorkerTimeout(),this.initDecoderWorkerTimeout=setTimeout(()=>{this._handleInitDecoderWorkerTimeout()},1e3*this.player._opt.loadingDecoderWorkerTimeout)}_handleInitDecoderWorkerTimeout(){this.pause().then(()=>{this.debug.log("Jessibuca","init decoder worker timeout and pause play")}).catch(e=>{this.debug.warn("Jessibuca","init decoder worker timeout and pause play error",e)})}}return a(St,"ERROR",O),a(St,"TIMEOUT",{loadingTimeout:F.loadingTimeout,delayTimeout:F.delayTimeout}),window.Jessibuca=St,St}); diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 0000000..ec9032c --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,11 @@ + + + diff --git a/web/src/api/cloudRecord.js b/web/src/api/cloudRecord.js new file mode 100644 index 0000000..4c3f785 --- /dev/null +++ b/web/src/api/cloudRecord.js @@ -0,0 +1,131 @@ +import request from '@/utils/request' + +// 云端录像API + +export function getPlayPath(id) { + return request({ + method: 'get', + url: `/api/cloud/record/play/path`, + params: { + recordId: id + } + }) +} + +export function queryListByData(params) { + const { app, stream, year, month, mediaServerId } = params + return request({ + method: 'get', + url: `/api/cloud/record/date/list`, + params: { + app: app, + stream: stream, + year: year, + month: month, + mediaServerId: mediaServerId + } + }) +} + +export function loadRecord(params) { + const { app, stream, cloudRecordId } = params + return request({ + method: 'get', + url: `/api/cloud/record/loadRecord`, + params: { + app: app, + stream: stream, + cloudRecordId: cloudRecordId + } + }) +} + +export function seek(params) { + const { mediaServerId, app, stream, seek, schema } = params + return request({ + method: 'get', + url: `/api/cloud/record/seek`, + params: { + mediaServerId: mediaServerId, + app: app, + stream: stream, + seek: seek, + schema: schema + } + }) +} + +export function speed(params) { + const { mediaServerId, app, stream, speed, schema } = params + return request({ + method: 'get', + url: `/api/cloud/record/speed`, + params: { + mediaServerId: mediaServerId, + app: app, + stream: stream, + speed: speed, + schema: schema + } + }) +} + +export function addTask(params) { + const { app, stream, mediaServerId, startTime, endTime } = params + return request({ + method: 'get', + url: `/api/cloud/record/task/add`, + params: { + app: app, + stream: stream, + mediaServerId: mediaServerId, + startTime: startTime, + endTime: endTime + } + }) +} + +export function queryTaskList(params) { + const { mediaServerId, isEnd } = params + return request({ + method: 'get', + url: `/api/cloud/record/task/list`, + params: { + mediaServerId: mediaServerId, + isEnd: isEnd + } + + }) +} + +export function deleteRecord(ids) { + return request({ + method: 'delete', + url: `/api/cloud/record/delete`, + data: { + ids: ids + } + + }) +} + +export function queryList(params) { + const { app, stream, query, callId, startTime, endTime, mediaServerId, page, count, ascOrder } = params + return request({ + method: 'get', + url: `/api/cloud/record/list`, + params: { + app: app, + stream: stream, + query: query, + callId: callId, + startTime: startTime, + endTime: endTime, + mediaServerId: mediaServerId, + page: page, + count: count, + ascOrder: ascOrder + } + }) +} + diff --git a/web/src/api/commonChannel.js b/web/src/api/commonChannel.js new file mode 100644 index 0000000..4070f43 --- /dev/null +++ b/web/src/api/commonChannel.js @@ -0,0 +1,658 @@ +import request from '@/utils/request' + +// 通用通道API + +export function queryOne(id) { + return request({ + method: 'get', + url: '/api/common/channel/one', + params: { + id: id + } + }) +} + +export function getIndustryList() { + return request({ + method: 'get', + url: '/api/common/channel/industry/list' + }) +} + +export function getTypeList() { + return request({ + method: 'get', + url: '/api/common/channel/type/list' + }) +} + +export function getNetworkIdentificationList() { + return request({ + method: 'get', + url: '/api/common/channel/network/identification/list' + }) +} + +export function update(data) { + return request({ + method: 'post', + url: '/api/common/channel/update', + data: data + }) +} + +export function reset(data) { + return request({ + method: 'post', + url: '/api/common/channel/reset', + data: data + }) +} + +export function add(data) { + return request({ + method: 'post', + url: '/api/common/channel/add', + data: data + }) +} + +export function getList(params) { + const { page, count, query, online, hasRecordPlan, channelType, civilCode, parentDeviceId } = params + return request({ + method: 'get', + url: '/api/common/channel/list', + params: { + page: page, + count: count, + channelType: channelType, + query: query, + online: online, + hasRecordPlan: hasRecordPlan, + civilCode: civilCode, + parentDeviceId: parentDeviceId + } + }) +} + +export function getCivilCodeList(params) { + const { page, count, channelType, query, online, civilCode } = params + return request({ + method: 'get', + url: '/api/common/channel/civilcode/list', + params: { + page: page, + count: count, + channelType: channelType, + query: query, + online: online, + civilCode: civilCode + } + }) +} + +export function getUnusualCivilCodeList(params) { + const { page, count, channelType, query, online } = params + return request({ + method: 'get', + url: '/api/common/channel/civilCode/unusual/list', + params: { + page: page, + count: count, + channelType: channelType, + query: query, + online: online + } + }) +} + +export function getUnusualParentList(params) { + const { page, count, channelType, query, online } = params + return request({ + method: 'get', + url: '/api/common/channel/parent/unusual/list', + params: { + page: page, + count: count, + channelType: channelType, + query: query, + online: online + } + }) +} + +export function clearUnusualCivilCodeList(params) { + const { all, channelIds } = params + return request({ + method: 'post', + url: '/api/common/channel/civilCode/unusual/clear', + data: { + all: all, + channelIds: channelIds + } + }) +} + +export function clearUnusualParentList(params) { + const { all, channelIds } = params + return request({ + method: 'post', + url: '/api/common/channel/parent/unusual/clear', + data: { + all: all, + channelIds: channelIds + } + }) +} + +export function getParentList(params) { + const { page, count, channelType, query, online, groupDeviceId } = params + return request({ + method: 'get', + url: '/api/common/channel/parent/list', + params: { + page: page, + count: count, + channelType: channelType, + query: query, + online: online, + groupDeviceId: groupDeviceId + } + }) +} + +export function addToRegion(params) { + const { civilCode, channelIds } = params + return request({ + method: 'post', + url: '/api/common/channel/region/add', + data: { + civilCode: civilCode, + channelIds: channelIds + } + }) +} + +export function deleteFromRegion(channels) { + return request({ + method: 'post', + url: '/api/common/channel/region/delete', + data: { + channelIds: channels + } + }) +} + +export function addDeviceToRegion(params) { + const { civilCode, deviceIds } = params + return request({ + method: 'post', + url: '/api/common/channel/region/device/add', + data: { + civilCode: civilCode, + deviceIds: deviceIds + } + }) +} + +export function deleteDeviceFromRegion(deviceIds) { + return request({ + method: 'post', + url: '/api/common/channel/region/device/delete', + data: { + deviceIds: deviceIds + } + }) +} + +export function addToGroup(params) { + const { parentId, businessGroup, channelIds } = params + return request({ + method: 'post', + url: '/api/common/channel/group/add', + data: { + parentId: parentId, + businessGroup: businessGroup, + channelIds: channelIds + } + }) +} + +export function deleteFromGroup(channels) { + return request({ + method: 'post', + url: '/api/common/channel/group/delete', + data: { + channelIds: channels + } + }) +} + +export function addDeviceToGroup(params) { + const { parentId, businessGroup, deviceIds } = params + return request({ + method: 'post', + url: '/api/common/channel/group/device/add', + data: { + parentId: parentId, + businessGroup: businessGroup, + deviceIds: deviceIds + } + }) +} + +export function deleteDeviceFromGroup(deviceIds) { + return request({ + method: 'post', + url: '/api/common/channel/group/device/delete', + data: { + deviceIds: deviceIds + } + }) +} + +export function playChannel(channelId) { + return request({ + method: 'get', + url: '/api/common/channel/play', + params: { + channelId: channelId + } + }) +} +export function stopPlayChannel(channelId) { + return request({ + method: 'get', + url: '/api/common/channel/play/stop', + params: { + channelId: channelId + } + }) +} + + +// 前端控制 + +export function setSpeedForScan({ channelId, scanId, speed }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/scan/set/speed', + params: { + channelId: channelId, + scanId: scanId, + speed: speed + } + }) +} + +export function setLeftForScan({ channelId, scanId }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/scan/set/left', + params: { + channelId: channelId, + scanId: scanId + } + }) +} + +export function setRightForScan({ channelId, scanId }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/scan/set/right', + params: { + channelId: channelId, + scanId: scanId + } + + }) +} + +export function startScan({ channelId, scanId }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/scan/start', + params: { + channelId: channelId, + scanId: scanId + } + }) +} + +export function stopScan({ channelId, scanId }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/scan/stop', + params: { + channelId: channelId, + scanId: scanId + } + + }) +} + +export function queryPreset(channelId) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/preset/query', + params: { + channelId: channelId + } + }) +} + +export function addPointForCruise({ channelId, tourId, presetId }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/tour/point/add', + params: { + channelId: channelId, + tourId: tourId, + presetId: presetId + } + }) +} + +export function deletePointForCruise({ channelId, tourId, presetId }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/tour/point/delete', + params: { + channelId: channelId, + tourId: tourId, + presetId: presetId + } + }) +} + +export function setCruiseSpeed({ channelId, tourId, presetId , speed }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/tour/speed', + params: { + channelId: channelId, + tourId: tourId, + presetId: presetId, + speed: speed + } + }) +} + +export function setCruiseTime({ channelId, tourId, presetId, time }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/tour/time', + params: { + channelId: channelId, + tourId: tourId, + presetId: presetId, + time: time + } + }) +} + +export function startCruise({ channelId, tourId }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/tour/start', + params: { + channelId: channelId, + tourId: tourId + } + }) +} + +export function stopCruise({ channelId, tourId }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/tour/stop', + params: { + channelId: channelId, + tourId: tourId + } + }) +} + +export function addPreset({ channelId, presetId, presetName }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/preset/add', + params: { + channelId: channelId, + presetId: presetId, + presetName: presetName + } + }) +} + +export function callPreset({ channelId, presetId }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/preset/call', + params: { + channelId: channelId, + presetId: presetId + } + }) +} + +export function deletePreset({ channelId, presetId }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/preset/delete', + params: { + channelId: channelId, + presetId: presetId + } + }) +} + +/** + * command: on 开启, off 关闭 + */ +export function auxiliary({ channelId, command, auxiliaryId }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/auxiliary', + params: { + channelId: channelId, + command: command, + auxiliaryId: auxiliaryId + } + }) +} +/** + * command: on 开启, off 关闭 + */ +export function wiper({ channelId, command }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/wiper', + params: { + channelId: channelId, + command: command + } + }) +} + +export function ptz({ channelId, command, panSpeed, tiltSpeed, zoomSpeed }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/ptz', + params: { + channelId: channelId, + command: command, + panSpeed: panSpeed, + tiltSpeed: tiltSpeed, + zoomSpeed: zoomSpeed + } + }) +} + +export function iris({ channelId, command, speed }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/fi/iris', + params: { + channelId: channelId, + command: command, + speed: speed + } + }) +} + +export function focus({ channelId, command, speed }) { + return request({ + method: 'get', + url: '/api/common/channel/front-end/fi/focus', + params: { + channelId: channelId, + command: command, + speed: speed + } + }) +} +export function queryRecord({ channelId, startTime, endTime }) { + return request({ + method: 'get', + url: '/api/common/channel/playback/query', + params: { + channelId: channelId, + startTime: startTime, + endTime: endTime + } + }) +} +export function playback({ channelId, startTime, endTime }) { + return request({ + method: 'get', + url: '/api/common/channel/playback', + params: { + channelId: channelId, + startTime: startTime, + endTime: endTime + } + }) +} +export function stopPlayback({ channelId, stream }) { + return request({ + method: 'get', + url: '/api/common/channel/playback/stop', + params: { + channelId: channelId, + stream: stream + } + }) +} +export function pausePlayback({ channelId, stream}) { + return request({ + method: 'get', + url: '/api/common/channel/playback/pause', + params: { + channelId: channelId, + stream: stream + } + }) +} +export function resumePlayback({ channelId, stream}) { + return request({ + method: 'get', + url: '/api/common/channel/playback/resume', + params: { + channelId: channelId, + stream: stream + } + }) +} +export function seekPlayback({ channelId, stream, seekTime}) { + return request({ + method: 'get', + url: '/api/common/channel/playback/seek', + params: { + channelId: channelId, + stream: stream, + seekTime: seekTime + } + }) +} +export function speedPlayback({ channelId, stream, speed}) { + return request({ + method: 'get', + url: '/api/common/channel/playback/speed', + params: { + channelId: channelId, + stream: stream, + speed: speed + } + }) +} +export function getAllForMap({ query, online, hasRecordPlan, channelType }) { + return request({ + method: 'get', + url: '/api/common/channel/map/list', + params: { + query: query, + online: online, + hasRecordPlan: hasRecordPlan, + channelType: channelType + } + }) +} +export function saveLevel(data) { + return request({ + method: 'post', + url: '/api/common/channel/map/save-level', + data: data + }) +} +export function resetLevel() { + return request({ + method: 'post', + url: '/api/common/channel/map/reset-level' + }) +} +export function clearThin(id) { + return request({ + method: 'get', + url: '/api/common/channel/map/thin/clear', + params: { + id: id + } + }) +} +export function thinProgress(id) { + return request({ + method: 'get', + url: '/api/common/channel/map/thin/progress', + params: { + id: id + } + }) +} +export function saveThin(id) { + return request({ + method: 'get', + url: '/api/common/channel/map/thin/save', + params: { + id: id + } + }) +} +export function drawThin(data) { + return request({ + method: 'post', + url: '/api/common/channel/map/thin/draw', + data: data + }) +} +export function test() { + return request({ + method: 'get', + url: '/api/sy/camera/list/ids', + params: { + deviceIds: ['a', 'b', 'c'].join(','), + geoCoordSys: 'GCJ02', + traditional: true + } + }) +} diff --git a/web/src/api/device.js b/web/src/api/device.js new file mode 100644 index 0000000..fce3f30 --- /dev/null +++ b/web/src/api/device.js @@ -0,0 +1,243 @@ +import request from '@/utils/request' + +// 国标设备API + +export function queryDeviceSyncStatus(deviceId) { + return request({ + method: 'get', + url: `/api/device/query/${deviceId}/sync_status` + }) +} + +export function queryDevices(params) { + const { page, count, query, status } = params + return request({ + method: 'get', + url: `/api/device/query/devices`, + params: { + page: page, + count: count, + query: query, + status: status + } + }) +} + +export function deleteDevice(deviceId) { + return request({ + method: 'delete', + url: `/api/device/query/devices/${deviceId}/delete` + }) +} + +export function sync(deviceId) { + return request({ + method: 'get', + url: `/api/device/query/devices/${deviceId}/sync` + }) +} + +export function updateDeviceTransport(deviceId, streamMode) { + return request({ + method: 'post', + url: `/api/device/query/transport/${deviceId}/${streamMode}` + }) +} + +export function setGuard(deviceId) { + return request({ + method: 'get', + url: `/api/device/control/guard`, + params: { + deviceId: deviceId, + guardCmd: 'SetGuard' + } + }) +} + +export function resetGuard(deviceId) { + return request({ + method: 'get', + url: `/api/device/control/guard`, + params: { + deviceId: deviceId, + guardCmd: 'ResetGuard' + } + }) +} + +export function subscribeCatalog(params) { + const { id, cycle } = params + return request({ + method: 'get', + url: `/api/device/query/subscribe/catalog`, + params: { + id: id, + cycle: cycle + } + }) +} + +export function subscribeMobilePosition(params) { + const { id, cycle, interval } = params + return request({ + method: 'get', + url: `/api/device/query/subscribe/mobile-position`, + params: { + id: id, + cycle: cycle, + interval: interval + } + }) +} + +export function queryBasicParam(deviceId) { + return request({ + method: 'get', + url: `/api/device/config/query/${deviceId}/BasicParam` + }) +} + +export function queryChannelOne(params) { + const { deviceId, channelDeviceId } = params + return request({ + method: 'get', + url: '/api/device/query/channel/one', + params: { + deviceId: deviceId, + channelDeviceId: channelDeviceId + } + }) +} + +export function queryChannels(deviceId, params) { + const { page, count, query, online, channelType, catalogUnderDevice } = params + return request({ + method: 'get', + url: `/api/device/query/devices/${deviceId}/channels`, + params: { + page: page, + count: count, + query: query, + online: online, + channelType: channelType, + catalogUnderDevice: catalogUnderDevice + } + }) +} + +export function queryHasStreamChannels(params) { + const {page, count, query} = params + return request({ + method: 'get', + url: `/api/device/query/streams`, + params: { + page: page, + count: count, + query: query + } + }) +} + +export function deviceRecord(params) { + const { deviceId, channelId, recordCmdStr } = params + return request({ + method: 'get', + url: `/api/device/control/record`, + params: { + deviceId: deviceId, + channelId: channelId, + recordCmdStr: recordCmdStr + } + }) +} + +export function querySubChannels(params, deviceId, parentChannelId) { + const { page, count, query, online, channelType } = params + return request({ + method: 'get', + url: `/api/device/query/sub_channels/${deviceId}/${parentChannelId}/channels`, + params: { + page: page, + count: count, + query: query, + online: online, + channelType: channelType + } + }) +} + +export function queryChannelTree(params) { + const { parentId, page, count } = params + return request({ + method: 'get', + url: `/api/device/query/tree/channel/${this.deviceId}`, + params: { + parentId: parentId, + page: page, + count: count + } + }) +} + +export function changeChannelAudio(params) { + const { channelId, audio } = params + return request({ + method: 'post', + url: `/api/device/query/channel/audio`, + params: { + channelId: channelId, + audio: audio + } + }) +} + +export function updateChannelStreamIdentification(params) { + const { deviceDbId, streamIdentification, id } = params + return request({ + method: 'post', + url: `/api/device/query/channel/stream/identification/update/`, + params: { + deviceDbId: deviceDbId, + id: id, + streamIdentification: streamIdentification + } + }) +} + +export function update(data) { + return request({ + method: 'post', + url: `/api/device/query/device/update`, + data: data + }) +} +export function add(data) { + return request({ + method: 'post', + url: `/api/device/query/device/add`, + data: data + }) +} + +export function queryDeviceOne(deviceId) { + return request({ + method: 'get', + url: `/api/device/query/devices/${deviceId}` + }) +} + +export function queryDeviceTree(params, deviceId) { + const { page, count, parentId, onlyCatalog } = params + return request({ + method: 'get', + url: `/api/device/query/tree/${deviceId}`, + params: { + page: page, + count: count, + parentId: parentId, + onlyCatalog: onlyCatalog + } + }) +} + diff --git a/web/src/api/frontEnd.js b/web/src/api/frontEnd.js new file mode 100644 index 0000000..47c5132 --- /dev/null +++ b/web/src/api/frontEnd.js @@ -0,0 +1,218 @@ +import request from '@/utils/request' + +// 前端控制 + +export function setSpeedForScan([deviceId, channelDeviceId, scanId, speed]) { + return request({ + method: 'get', + url: `/api/front-end/scan/set/speed/${deviceId}/${channelDeviceId}`, + params: { + scanId: scanId, + speed: speed + } + }) +} + +export function setLeftForScan([deviceId, channelDeviceId, scanId]) { + return request({ + method: 'get', + url: `/api/front-end/scan/set/left/${deviceId}/${channelDeviceId}`, + params: { + scanId: scanId + } + }) +} + +export function setRightForScan([deviceId, channelDeviceId, scanId]) { + return request({ + method: 'get', + url: `/api/front-end/scan/set/right/${deviceId}/${channelDeviceId}`, + params: { + scanId: scanId + } + + }) +} + +export function startScan([deviceId, channelDeviceId, scanId]) { + return request({ + method: 'get', + url: `/api/front-end/scan/start/${deviceId}/${channelDeviceId}`, + params: { + scanId: scanId + } + }) +} + +export function stopScan([deviceId, channelDeviceId, scanId]) { + return request({ + method: 'get', + url: `/api/front-end/scan/stop/${deviceId}/${channelDeviceId}`, + params: { + scanId: scanId + } + + }) +} + +export function queryPreset([deviceId, channelDeviceId]) { + return request({ + method: 'get', + url: `/api/front-end/preset/query/${deviceId}/${channelDeviceId}` + }) +} + +export function addPointForCruise([deviceId, channelDeviceId, cruiseId, presetId]) { + return request({ + method: 'get', + url: `/api/front-end/cruise/point/add/${deviceId}/${channelDeviceId}`, + params: { + cruiseId: cruiseId, + presetId: presetId + } + }) +} + +export function deletePointForCruise([deviceId, channelDeviceId, cruiseId, presetId]) { + return request({ + method: 'get', + url: `/api/front-end/cruise/point/delete/${deviceId}/${channelDeviceId}`, + params: { + cruiseId: cruiseId, + presetId: presetId + } + }) +} + +export function setCruiseSpeed([deviceId, channelDeviceId, cruiseId, cruiseSpeed]) { + return request({ + method: 'get', + url: `/api/front-end/cruise/speed/${deviceId}/${channelDeviceId}`, + params: { + cruiseId: cruiseId, + speed: cruiseSpeed + } + }) +} + +export function setCruiseTime([deviceId, channelDeviceId, cruiseId, cruiseTime]) { + return request({ + method: 'get', + url: `/api/front-end/cruise/time/${deviceId}/${channelDeviceId}`, + params: { + cruiseId: cruiseId, + time: cruiseTime + } + }) +} + +export function startCruise([deviceId, channelDeviceId, cruiseId]) { + return request({ + method: 'get', + url: `/api/front-end/cruise/start/${deviceId}/${channelDeviceId}`, + params: { + cruiseId: cruiseId + } + }) +} + +export function stopCruise([deviceId, channelDeviceId, cruiseId]) { + return request({ + method: 'get', + url: `/api/front-end/cruise/stop/${deviceId}/${channelDeviceId}`, + params: { + cruiseId: cruiseId + } + }) +} + +export function addPreset([deviceId, channelDeviceId, presetId]) { + return request({ + method: 'get', + url: `/api/front-end/preset/add/${deviceId}/${channelDeviceId}`, + params: { + presetId: presetId + } + }) +} + +export function callPreset([deviceId, channelDeviceId, presetId]) { + return request({ + method: 'get', + url: `/api/front-end/preset/call/${deviceId}/${channelDeviceId}`, + params: { + presetId: presetId + } + }) +} + +export function deletePreset([deviceId, channelDeviceId, presetId]) { + return request({ + method: 'get', + url: `/api/front-end/preset/delete/${deviceId}/${channelDeviceId}`, + params: { + presetId: presetId + } + }) +} + +/** + * command: on 开启, off 关闭 + */ +export function auxiliary([deviceId, channelDeviceId, command, switchId]) { + return request({ + method: 'get', + url: `/api/front-end/auxiliary/${deviceId}/${channelDeviceId}`, + params: { + command: command, + switchId: switchId + } + }) +} +/** + * command: on 开启, off 关闭 + */ +export function wiper([deviceId, channelDeviceId, command]) { + return request({ + method: 'get', + url: `/api/front-end/wiper/${deviceId}/${channelDeviceId}`, + params: { + command: command + } + }) +} + +export function ptz([deviceId, channelId, command, horizonSpeed, verticalSpeed, zoomSpeed]) { + return request({ + method: 'get', + url: `/api/front-end/ptz/${deviceId}/${channelId}`, + params: { + command: command, + horizonSpeed: horizonSpeed, + verticalSpeed: verticalSpeed, + zoomSpeed: zoomSpeed + } + }) +} + +export function iris([deviceId, channelId, command, speed]) { + return request({ + method: 'get', + url: `/api/front-end/fi/iris/${deviceId}/${channelId}`, + params: { + command: command, + speed: speed + } + }) +} + +export function focus([deviceId, channelDeviceId, command, speed]) { + return request({ + method: 'get', + url: `/api/front-end/fi/focus/${deviceId}/${channelDeviceId}`, + params: { + command: command, + speed: speed + } + }) +} diff --git a/web/src/api/gbRecord.js b/web/src/api/gbRecord.js new file mode 100644 index 0000000..e78da4e --- /dev/null +++ b/web/src/api/gbRecord.js @@ -0,0 +1,33 @@ +import request from '@/utils/request' + +export function query([deviceId, channelId, startTime, endTime]) { + return request({ + method: 'get', + url: '/api/gb_record/query/' + deviceId + '/' + channelId + '?startTime=' + startTime + '&endTime=' + endTime + + }) +} + +export function startDownLoad([deviceId, channelId, startTime, endTime, downloadSpeed]) { + return request({ + url: '/api/gb_record/download/start/' + deviceId + '/' + channelId + '?startTime=' + startTime + '&endTime=' + + endTime + '&downloadSpeed=' + downloadSpeed + + }) +} + +export function stopDownLoad(deviceId, channelId, streamId) { + return request({ + method: 'get', + url: '/api/gb_record/download/stop/' + deviceId + '/' + channelId + '/' + streamId + + }) +} + +export function queryDownloadProgress([deviceId, channelId, streamId]) { + return request({ + method: 'get', + url: `/api/gb_record/download/progress/${deviceId}/${channelId}/${streamId}` + }) +} + diff --git a/web/src/api/group.js b/web/src/api/group.js new file mode 100644 index 0000000..a62404c --- /dev/null +++ b/web/src/api/group.js @@ -0,0 +1,69 @@ +import request from '@/utils/request' + +// 分组API + +export function update(data) { + return request({ + method: 'post', + url: '/api/group/update', + data: data + }) +} +export function add(data) { + return request({ + method: 'post', + url: '/api/group/add', + data: data + }) +} +export function getTreeList({ query, parent, hasChannel, page, count }) { + return request({ + method: 'get', + url: `/api/group/tree/list`, + params: { + query: query, + parent: parent, + hasChannel: hasChannel, + page: page, + count: count + } + }) +} +export function deleteGroup(id) { + return request({ + method: 'delete', + url: `/api/group/delete`, + params: { + id: id + } + }) +} +export function getPath(params) { + const { deviceId, businessGroup } = params + return request({ + method: 'get', + url: `/api/group/path`, + params: { + deviceId: deviceId, + businessGroup: businessGroup + } + }) +} +export function queryTree(params) { + const { page, count, query } = params + return request({ + method: 'get', + url: `/api/group/tree/query`, + params: { + query: query, + page: page, + count: count + } + }) +} +export function sync() { + return request({ + method: 'get', + url: `/api/group/sync`, + }) +} diff --git a/web/src/api/jtDevice.js b/web/src/api/jtDevice.js new file mode 100644 index 0000000..4f5210c --- /dev/null +++ b/web/src/api/jtDevice.js @@ -0,0 +1,371 @@ +import request from '@/utils/request' + +// 部标设备API + +export function queryDevices({ page, count, query, online }) { + return request({ + method: 'get', + url: '/api/jt1078/terminal/list', + params: { + page: page, + count: count, + query: query, + online: online + } + }) +} + +export function queryDeviceById(deviceId) { + return request({ + method: 'get', + url: '/api/jt1078/terminal/query', + params: { + deviceId: deviceId + } + }) +} + +export function update(form) { + return request({ + method: 'post', + url: '/api/jt1078/terminal/update', + params: form + }) +} + +export function add(form) { + return request({ + method: 'post', + url: '/api/jt1078/terminal/add', + params: form + }) +} + +export function deleteDevice(phoneNumber) { + return request({ + method: 'delete', + url: '/api/jt1078/terminal/delete', + params: { + phoneNumber: phoneNumber + } + }) +} + +export function queryChannels(params) { + const { page, count, query, deviceId } = params + return request({ + method: 'get', + url: '/api/jt1078/terminal/channel/list', + params: { + page: page, + count: count, + query: query, + deviceId: deviceId + } + }) +} + +export function play(params) { + const { phoneNumber, channelId, type } = params + return request({ + method: 'get', + url: '/api/jt1078/live/start', + params: { + phoneNumber: phoneNumber, + channelId: channelId, + type: type + } + }) +} +export function stopPlay(params) { + const { phoneNumber, channelId } = params + return request({ + method: 'get', + url: '/api/jt1078/live/stop', + params: { + phoneNumber: phoneNumber, + channelId: channelId + } + }) +} +export function updateChannel(data) { + return request({ + method: 'post', + url: '/api/jt1078/terminal/channel/update', + data: data + }) +} +export function addChannel(data) { + return request({ + method: 'post', + url: '/api/jt1078/terminal/channel/add', + data: data + }) +} + +export function ptz(params) { + const { phoneNumber, channelId, command, speed } = params + return request({ + method: 'get', + url: '/api/jt1078/ptz', + params: { + phoneNumber: phoneNumber, + channelId: channelId, + command: command, + speed: speed + } + }) +} +export function wiper(params) { + const { phoneNumber, channelId, command } = params + return request({ + method: 'get', + url: '/api/jt1078/wiper', + params: { + phoneNumber: phoneNumber, + channelId: channelId, + command: command + } + }) +} +export function fillLight(params) { + const { phoneNumber, channelId, command } = params + return request({ + method: 'get', + url: '/api/jt1078/fill-light', + params: { + phoneNumber: phoneNumber, + channelId: channelId, + command: command + } + }) +} +export function queryRecordList(params) { + const { phoneNumber, channelId, startTime, endTime } = params + return request({ + method: 'get', + url: '/api/jt1078/record/list', + params: { + phoneNumber: phoneNumber, + channelId: channelId, + startTime: startTime, + endTime: endTime + } + }) +} +export function startPlayback(params) { + const { phoneNumber, channelId, startTime, endTime, type, rate, playbackType, playbackSpeed } = params + return request({ + method: 'get', + url: '/api/jt1078/playback/start/', + params: { + phoneNumber: phoneNumber, + channelId: channelId, + startTime: startTime, + endTime: endTime, + type: type, + rate: rate, + playbackType: playbackType, + playbackSpeed: playbackSpeed + } + }) +} +export function getRecordTempUrl({ phoneNumber, channelId, startTime, endTime, alarmSign, mediaType, streamType, storageType }) { + return request({ + method: 'get', + url: '/api/jt1078/playback/downloadUrl', + params: { + phoneNumber: phoneNumber, + channelId: channelId, + startTime: startTime, + endTime: endTime, + alarmSign: alarmSign, + mediaType: mediaType, + streamType: streamType, + storageType: storageType + } + }) +} +export function controlPlayback(params) { + const { phoneNumber, channelId, command, playbackSpeed, time } = params + return request({ + method: 'get', + url: '/api/jt1078/playback/control', + params: { + phoneNumber: phoneNumber, + channelId: channelId, + command: command, + playbackSpeed: playbackSpeed, + time: time + } + }) +} +export function stopPlayback(params) { + const { phoneNumber, channelId, streamId } = params + return request({ + method: 'get', + url: '/api/jt1078/playback/stop/', + params: { + phoneNumber: phoneNumber, + channelId: channelId, + streamId: streamId + } + }) +} +export function queryConfig(phoneNumber) { + return request({ + method: 'get', + url: '/api/jt1078/config/get', + params: { + phoneNumber: phoneNumber + } + }) +} +export function setConfig(data) { + return request({ + method: 'post', + url: '/api/jt1078/config/set', + data: data + }) +} +export function queryAttribute(phoneNumber) { + return request({ + method: 'get', + url: '/api/jt1078/attribute', + params: { + phoneNumber: phoneNumber + } + }) +} +export function linkDetection(phoneNumber) { + return request({ + method: 'get', + url: '/api/jt1078/link-detection', + params: { + phoneNumber: phoneNumber + } + }) +} +export function queryPosition(phoneNumber) { + return request({ + method: 'get', + url: '/api/jt1078/position-info', + params: { + phoneNumber: phoneNumber + } + }) +} +export function sendTextMessage(data) { + return request({ + method: 'post', + url: '/api/jt1078/text-msg', + data: data + }) +} +export function telephoneCallback({ phoneNumber, sign, destPhoneNumber }) { + return request({ + method: 'get', + url: '/api/jt1078/telephone-callback', + params: { + phoneNumber: phoneNumber, + sign: sign, + destPhoneNumber: destPhoneNumber + } + }) +} +export function queryDriverInfo(phoneNumber) { + return request({ + method: 'get', + url: '/api/jt1078/driver-information', + params: { + phoneNumber: phoneNumber + } + }) +} +export function factoryReset(phoneNumber) { + return request({ + method: 'post', + url: '/api/jt1078/control/factory-reset', + params: { + phoneNumber: phoneNumber + } + }) +} +export function reset(phoneNumber) { + return request({ + method: 'post', + url: '/api/jt1078/control/reset', + params: { + phoneNumber: phoneNumber + } + }) +} +export function connection(data) { + return request({ + method: 'post', + url: '/api/jt1078/control/connection', + data: data + }) +} +export function controlDoor({ phoneNumber, open}) { + return request({ + method: 'get', + url: '/api/jt1078/control/door', + params: { + phoneNumber: phoneNumber, + open: open + } + }) +} +export function queryMediaAttribute(phoneNumber) { + return request({ + method: 'get', + url: '/api/jt1078/media/attribute', + params: { + phoneNumber: phoneNumber + } + }) +} +export function queryMediaData(data) { + return request({ + method: 'post', + url: '/api/jt1078/media/list', + data: data + }) +} +export function setPhoneBook(data) { + return request({ + method: 'post', + url: '/api/jt1078/set-phone-book', + data: data + }) +} +export function shooting(data) { + return request({ + method: 'post', + url: '/api/jt1078/shooting', + data: data + }) +} +export function startTalk({ phoneNumber, channelId }) { + return request({ + method: 'get', + url: '/api/jt1078/talk/start', + params: { + phoneNumber: phoneNumber, + channelId: channelId + } + }) +} +export function stopTalk({ phoneNumber, channelId }) { + return request({ + method: 'get', + url: '/api/jt1078/talk/stop', + params: { + phoneNumber: phoneNumber, + channelId: channelId + } + }) +} + + diff --git a/web/src/api/log.js b/web/src/api/log.js new file mode 100644 index 0000000..7385675 --- /dev/null +++ b/web/src/api/log.js @@ -0,0 +1,15 @@ +import request from '@/utils/request' + +export function queryList(params) { + const { query, startTime, endTime } = params + return request({ + method: 'get', + url: `/api/log/list`, + params: { + query: query, + startTime: startTime, + endTime: endTime + } + }) +} + diff --git a/web/src/api/platform.js b/web/src/api/platform.js new file mode 100644 index 0000000..5a684b3 --- /dev/null +++ b/web/src/api/platform.js @@ -0,0 +1,142 @@ +import request from '@/utils/request' + +export function update(data) { + return request({ + method: 'post', + url: '/api/platform/update', + data: data + }) +} + +export function add(data) { + return request({ + method: 'post', + url: '/api/platform/add', + data: data + }) +} + +export function exit(deviceGbId) { + return request({ + method: 'get', + url: `/api/platform/exit/${deviceGbId}` + }) +} + +export function remove(id) { + return request({ + method: 'delete', + url: `/api/platform/delete`, + params: { + id: id + } + }) +} + +export function pushChannel(id) { + return request({ + method: 'get', + url: `/api/platform/channel/push`, + params: { + id: id + } + }) +} + +export function getServerConfig() { + return request({ + method: 'get', + url: `/api/platform/server_config` + }) +} + +export function query(params) { + const { count, page, query } = params + return request({ + method: 'get', + url: `/api/platform/query`, + params: { + count: count, + page: page, + query: query + } + + }) +} + +export function getChannelList(params) { + const { page, count, query, online, channelType, platformId, hasShare } = params + return request({ + method: 'get', + url: `/api/platform/channel/list`, + params: { + page: page, + count: count, + query: query, + online: online, + channelType: channelType, + platformId: platformId, + hasShare: hasShare + } + }) +} + +export function addChannel(params) { + const { platformId, channelIds, all } = params + return request({ + method: 'post', + url: `/api/platform/channel/add`, + data: { + platformId: platformId, + channelIds: channelIds, + all: all + } + + }) +} + +export function addChannelByDevice(params) { + const { platformId, deviceIds } = params + return request({ + method: 'post', + url: `/api/platform/channel/device/add`, + data: { + platformId: platformId, + deviceIds: deviceIds + } + }) +} + +export function removeChannelByDevice(params) { + const { platformId, deviceIds } = params + return request({ + method: 'post', + url: `/api/platform/channel/device/remove`, + data: { + platformId: platformId, + deviceIds: deviceIds + } + }) +} + +export function removeChannel(params) { + const { platformId, channelIds, all } = params + return request({ + method: 'delete', + url: `/api/platform/channel/remove`, + data: { + platformId: platformId, + channelIds: channelIds, + all: all + } + }) +} + +export function updateCustomChannel(data) { + return request({ + method: 'post', + url: `/api/platform/channel/custom/update`, + data: data + }) +} + diff --git a/web/src/api/play.js b/web/src/api/play.js new file mode 100644 index 0000000..f24483d --- /dev/null +++ b/web/src/api/play.js @@ -0,0 +1,28 @@ +import request from '@/utils/request' + +// 实时流播放API + +export function play(deviceId, channelId) { + return request({ + method: 'get', + url: '/api/play/start/' + deviceId + '/' + channelId + }) +} +export function stop(deviceId, channelId) { + return request({ + method: 'get', + url: '/api/play/stop/' + deviceId + "/" + channelId, + }) +} +export function broadcastStart(deviceId, channelId, broadcastMode) { + return request({ + method: 'get', + url: '/api/play/broadcast/' + deviceId + '/' + channelId + "?timeout=30&broadcastMode=" + broadcastMode + }) +} +export function broadcastStop(deviceId, channelId, ) { + return request({ + method: 'get', + url: '/api/play/broadcast/stop/' + deviceId + '/' + channelId + }) +} diff --git a/web/src/api/playback.js b/web/src/api/playback.js new file mode 100644 index 0000000..25e3655 --- /dev/null +++ b/web/src/api/playback.js @@ -0,0 +1,34 @@ +import request from '@/utils/request' + +// 回放流播放API + +export function play([deviceId, channelId, startTime, endTime]) { + return request({ + method: 'get', + url: '/api/playback/start/' + deviceId + '/' + channelId + '?startTime=' + startTime + '&endTime=' + endTime + }) +} +export function resume(streamId) { + return request({ + method: 'get', + url: '/api/playback/resume/' + streamId + }) +} +export function pause(streamId) { + return request({ + method: 'get', + url: '/api/playback/pause/' + streamId + }) +} +export function setSpeed([streamId, speed]) { + return request({ + method: 'get', + url: `/api/playback/speed/${streamId}/${speed}` + }) +} +export function stop(deviceId, channelId, streamId) { + return request({ + method: 'get', + url: '/api/playback/stop/' + deviceId + '/' + channelId + '/' + streamId + }) +} diff --git a/web/src/api/recordPlan.js b/web/src/api/recordPlan.js new file mode 100644 index 0000000..88fe3a5 --- /dev/null +++ b/web/src/api/recordPlan.js @@ -0,0 +1,85 @@ +import request from '@/utils/request' + +export function getPlan(id) { + return request({ + method: 'get', + url: '/api/record/plan/get', + params: { + planId: id + } + }) +} + +export function addPlan(params) { + const { name, planList } = params + return request({ + + method: 'post', + url: '/api/record/plan/add', + data: { + name: name, + planItemList: planList + } + }) +} + +export function update(params) { + const { id, name, planList } = params + return request({ + method: 'post', + url: '/api/record/plan/update', + data: { + id: id, + name: name, + planItemList: planList + } + }) +} + +export function queryList(params) { + const { page, count, query } = params + return request({ + method: 'get', + url: `/api/record/plan/query`, + params: { + page: page, + count: count, + query: query + } + }) +} + +export function deletePlan(id) { + return request({ + method: 'delete', + url: '/api/record/plan/delete', + params: { + planId: id + } + }) +} + +export function queryChannelList(params) { + const { page, count, channelType, query, online, planId , hasLink } = params + return request({ + method: 'get', + url: `/api/record/plan/channel/list`, + params: { + page: page, + count: count, + query: query, + online: online, + channelType: channelType, + planId: planId, + hasLink: hasLink + } + }) +} + +export function linkPlan(data) { + return request({ + method: 'post', + url: `/api/record/plan/link`, + data: data + }) +} diff --git a/web/src/api/region.js b/web/src/api/region.js new file mode 100644 index 0000000..de5f994 --- /dev/null +++ b/web/src/api/region.js @@ -0,0 +1,96 @@ +import request from '@/utils/request' + +// 行政区划API + +export function getTreeList(params) { + const {query, parent, hasChannel} = params + return request({ + method: 'get', + url: `/api/region/tree/list`, + params: { + query: query, + parent: parent, + hasChannel: hasChannel + } + }) +} + +export function deleteRegion(id) { + return request({ + method: "delete", + url: `/api/region/delete`, + params: { + id: id, + } + }) +} + +export function description(civilCode) { + return request({ + method: 'get', + url: `/api/region/description`, + params: { + civilCode: civilCode, + } + }) +} + +export function addByCivilCode(civilCode) { + return request({ + method: 'get', + url: `/api/region/addByCivilCode`, + params: { + civilCode: civilCode, + } + }) +} + +export function queryChildListInBase(parent) { + return request({ + method: 'get', + url: "/api/region/base/child/list", + params: { + parent: parent, + } + }) +} + +export function update(data) { + return request({ + method: 'post', + url: "/api/region/update", + data: data + + }) +} + +export function add(data) { + return request({ + method: 'post', + url: "/api/region/add", + data: data + }) +} + +export function queryPath(deviceId) { + return request({ + method: 'get', + url: `/api/region/path`, + params: { + deviceId: deviceId, + } + }) +} +export function queryTree(params) { + const { page, count, query } = params + return request({ + method: 'get', + url: `/api/region/tree/query`, + params: { + query: query, + page: page, + count: count + } + }) +} + diff --git a/web/src/api/role.js b/web/src/api/role.js new file mode 100644 index 0000000..eba83a9 --- /dev/null +++ b/web/src/api/role.js @@ -0,0 +1,11 @@ +import request from '@/utils/request' + +// 云端录像API + +export function getAll() { + return request({ + method: 'get', + url: '/api/role/all' + }) +} + diff --git a/web/src/api/server.js b/web/src/api/server.js new file mode 100644 index 0000000..1bb4dc9 --- /dev/null +++ b/web/src/api/server.js @@ -0,0 +1,130 @@ +import request from '@/utils/request' + +// 服务API + +export function getOnlineMediaServerList() { + return request({ + method: 'get', + url: `/api/server/media_server/online/list` + }) +} + +export function getMediaServerList() { + return request({ + method: 'get', + url: `/api/server/media_server/list` + }) +} + +export function getMediaServer(id) { + return request({ + method: 'get', + url: `/api/server/media_server/one/` + id + }) +} + +export function checkMediaServer(params) { + const { ip, httpPort, secret, type } = params + return request({ + method: 'get', + url: `/api/server/media_server/check`, + params: { + ip: ip, + port: httpPort, + secret: secret, + type: type + } + }) +} + +export function checkMediaServerRecord(params) { + const { ip, port } = params + return request({ + method: 'get', + url: `/api/server/media_server/record/check`, + params: { + ip: ip, + port: port + } + }) +} + +export function saveMediaServer(formData) { + return request({ + method: 'post', + url: `/api/server/media_server/save`, + data: formData + }) +} + +export function deleteMediaServer(id) { + return request({ + method: 'delete', + url: `/api/server/media_server/delete`, + params: { + id: id + } + }) +} + +export function getSystemConfig() { + return request({ + method: 'get', + url: `/api/server/system/configInfo` + }) +} + +export function getMediaInfo(params) { + const { app, stream, mediaServerId } = params + return request({ + method: 'get', + url: `/api/server/media_server/media_info`, + params: { + app: app, + stream: stream, + mediaServerId: mediaServerId + } + }) +} + +export function getSystemInfo() { + return request({ + method: 'get', + url: `/api/server/system/info` + }) +} + +export function getMediaServerLoad() { + return request({ + method: 'get', + url: `/api/server/media_server/load` + }) +} + +export function getResourceInfo() { + return request({ + method: 'get', + url: `/api/server/resource/info` + }) +} + +export function info() { + return request({ + method: 'get', + url: `/api/server/info` + }) +} + +export function getMapConfig() { + return request({ + method: 'get', + url: `/api/server/map/config` + }) +} +export function getModelList() { + return request({ + method: 'get', + url: `/api/server/map/model-icon/list` + }) +} + diff --git a/web/src/api/streamProxy.js b/web/src/api/streamProxy.js new file mode 100644 index 0000000..729d49d --- /dev/null +++ b/web/src/api/streamProxy.js @@ -0,0 +1,85 @@ +import request from '@/utils/request' + +// 拉流代理API + +export function queryFfmpegCmdList(mediaServerId) { + return request({ + method: 'get', + url: `/api/proxy/ffmpeg_cmd/list`, + params: { + mediaServerId: mediaServerId + } + }) +} + +export function save(data) { + return request({ + method: 'post', + url: `/api/proxy/save`, + data: data + + }) +} + +export function update(data) { + return request({ + method: 'post', + url: `/api/proxy/update`, + data: data + }) +} + +export function add(data) { + return request({ + method: 'post', + url: `/api/proxy/add`, + data: data + }) +} + +export function queryList(params) { + const { page, count, query, pulling, mediaServerId } = params + return request({ + method: 'get', + url: `/api/proxy/list`, + params: { + page: page, + count: count, + query: query, + pulling: pulling, + mediaServerId: mediaServerId + } + }) +} + +export function play(id) { + return request({ + method: 'get', + url: `/api/proxy/start`, + params: { + id: id + } + }) +} + +export function stopPlay(id) { + return request({ + method: 'get', + url: `/api/proxy/stop`, + params: { + id: id + } + }) +} + +export function remove(id) { + return request({ + method: 'delete', + url: '/api/proxy/delete', + params: { + id: id + } + + }) +} + diff --git a/web/src/api/streamPush.js b/web/src/api/streamPush.js new file mode 100644 index 0000000..aaf4ae3 --- /dev/null +++ b/web/src/api/streamPush.js @@ -0,0 +1,80 @@ +import request from '@/utils/request' + +// 推流列表API + +export function saveToGb(data) { + return request({ + method: 'post', + url: `/api/push/save_to_gb`, + data: data + }) +} + +export function add(data) { + return request({ + method: 'post', + url: `/api/push/add`, + data: data + }) +} + +export function update(data) { + return request({ + method: 'post', + url: '/api/push/update', + data: data + }) +} + +export function queryList(params) { + const { page, count, query, pushing, mediaServerId } = params + return request({ + method: 'get', + url: `/api/push/list`, + params: { + page: page, + count: count, + query: query, + pushing: pushing, + mediaServerId: mediaServerId + } + }) +} + +export function play(id) { + return request({ + method: 'get', + url: '/api/push/start', + params: { + id: id + } + }) +} + +export function remove(id) { + return request({ + method: 'post', + url: '/api/push/remove', + params: { + id: id + } + }) +} + +export function removeFormGb(data) { + return request({ + method: 'delete', + url: '/api/push/remove_form_gb', + data: data + }) +} + +export function batchRemove(ids) { + return request({ + method: 'delete', + url: '/api/push/batchRemove', + data: { + ids: ids + } + }) +} diff --git a/web/src/api/table.js b/web/src/api/table.js new file mode 100644 index 0000000..2752f52 --- /dev/null +++ b/web/src/api/table.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +export function getList(params) { + return request({ + url: '/vue-admin-template/table/list', + method: 'get', + params + }) +} diff --git a/web/src/api/user.js b/web/src/api/user.js new file mode 100644 index 0000000..a64d221 --- /dev/null +++ b/web/src/api/user.js @@ -0,0 +1,91 @@ +import request from '@/utils/request' + +export function login(params) { + return request({ + url: '/api/user/login', + method: 'get', + params: params + }) +} + +export function logout() { + return request({ + url: '/api/user/logout', + method: 'get' + }) +} +export function getUserInfo() { + return request({ + method: 'post', + url: '/api/user/userInfo' + }) +} + +export function changePushKey(params) { + const { pushKey, userId } = params + return request({ + method: 'post', + url: '/api/user/changePushKey', + params: { + pushKey: pushKey, + userId: userId + } + }) +} + +export function queryList(params) { + const { page, count } = params + return request({ + method: 'get', + url: `/api/user/users`, + params: { + page: page, + count: count + } + }) +} + +export function removeById(id) { + return request({ + method: 'delete', + url: `/api/user/delete?id=${id}` + + }) +} + +export function add(params) { + const { username, password, roleId } = params + return request({ + method: 'post', + url: '/api/user/add', + params: { + username: username, + password: password, + roleId: roleId + } + }) +} + +export function changePassword(params) { + const { oldPassword, password } = params + return request({ + method: 'post', + url: '/api/user/changePassword', + params: { + oldPassword: oldPassword, + password: password + } + }) +} + +export function changePasswordForAdmin(params) { + const { password, userId } = params + return request({ + method: 'post', + url: '/api/user/changePasswordForAdmin', + params: { + password: password, + userId: userId + } + }) +} diff --git a/web/src/api/userApiKey.js b/web/src/api/userApiKey.js new file mode 100644 index 0000000..7f0616c --- /dev/null +++ b/web/src/api/userApiKey.js @@ -0,0 +1,69 @@ +import request from '@/utils/request' + +export function remark(params) { + const { id, remark } = params + return request({ + method: 'post', + url: '/api/userApiKey/remark', + params: { + id: id, + remark: remark + } + }) +} + +export function queryList(params) { + const { page, count } = params + return request({ + method: 'get', + url: `/api/userApiKey/userApiKeys`, + params: { + page: page, + count: count + } + }) +} + +export function enable(id) { + return request({ + method: 'post', + url: `/api/userApiKey/enable?id=${id}` + + }) +} + +export function disable(id) { + return request({ + method: 'post', + url: `/api/userApiKey/disable?id=${id}` + }) +} + +export function reset(id) { + return request({ + method: 'post', + url: `/api/userApiKey/reset?id=${id}` + }) +} + +export function remove(id) { + return request({ + method: 'delete', + url: `/api/userApiKey/delete?id=${id}` + }) +} + +export function add(params) { + const { userId, app, enable, expiresAt, remark } = params + return request({ + method: 'post', + url: '/api/userApiKey/add', + params: { + userId: userId, + app: app, + enable: enable, + expiresAt: expiresAt, + remark: remark + } + }) +} diff --git a/web/src/assets/404_images/404.png b/web/src/assets/404_images/404.png new file mode 100644 index 0000000..3d8e230 Binary files /dev/null and b/web/src/assets/404_images/404.png differ diff --git a/web/src/assets/404_images/404_cloud.png b/web/src/assets/404_images/404_cloud.png new file mode 100644 index 0000000..c6281d0 Binary files /dev/null and b/web/src/assets/404_images/404_cloud.png differ diff --git a/web/src/assets/abl-logo.jpg b/web/src/assets/abl-logo.jpg new file mode 100644 index 0000000..82a564d Binary files /dev/null and b/web/src/assets/abl-logo.jpg differ diff --git a/web/src/assets/bg.jpg b/web/src/assets/bg.jpg new file mode 100644 index 0000000..0fe7b10 Binary files /dev/null and b/web/src/assets/bg.jpg differ diff --git a/web/src/assets/icons.png b/web/src/assets/icons.png new file mode 100755 index 0000000..9ed8102 Binary files /dev/null and b/web/src/assets/icons.png differ diff --git a/web/src/assets/loading.png b/web/src/assets/loading.png new file mode 100755 index 0000000..fa490e6 Binary files /dev/null and b/web/src/assets/loading.png differ diff --git a/web/src/assets/login-bg.jpg b/web/src/assets/login-bg.jpg new file mode 100755 index 0000000..ee27d8e Binary files /dev/null and b/web/src/assets/login-bg.jpg differ diff --git a/web/src/assets/login-cloud.png b/web/src/assets/login-cloud.png new file mode 100755 index 0000000..02b1958 Binary files /dev/null and b/web/src/assets/login-cloud.png differ diff --git a/web/src/assets/logo.png b/web/src/assets/logo.png new file mode 100755 index 0000000..c5da2d4 Binary files /dev/null and b/web/src/assets/logo.png differ diff --git a/web/src/assets/play.png b/web/src/assets/play.png new file mode 100755 index 0000000..e4b33f3 Binary files /dev/null and b/web/src/assets/play.png differ diff --git a/web/src/assets/zlm-logo.png b/web/src/assets/zlm-logo.png new file mode 100755 index 0000000..5f492dc Binary files /dev/null and b/web/src/assets/zlm-logo.png differ diff --git a/web/src/components/Breadcrumb/index.vue b/web/src/components/Breadcrumb/index.vue new file mode 100644 index 0000000..4d8fc23 --- /dev/null +++ b/web/src/components/Breadcrumb/index.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/web/src/components/Hamburger/index.vue b/web/src/components/Hamburger/index.vue new file mode 100644 index 0000000..368b002 --- /dev/null +++ b/web/src/components/Hamburger/index.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/web/src/components/SvgIcon/index.vue b/web/src/components/SvgIcon/index.vue new file mode 100644 index 0000000..b07ded2 --- /dev/null +++ b/web/src/components/SvgIcon/index.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/web/src/directive/el-drag-dialog/drag.js b/web/src/directive/el-drag-dialog/drag.js new file mode 100644 index 0000000..299e985 --- /dev/null +++ b/web/src/directive/el-drag-dialog/drag.js @@ -0,0 +1,77 @@ +export default { + bind(el, binding, vnode) { + const dialogHeaderEl = el.querySelector('.el-dialog__header') + const dragDom = el.querySelector('.el-dialog') + dialogHeaderEl.style.cssText += ';cursor:move;' + dragDom.style.cssText += ';top:0px;' + + // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null); + const getStyle = (function() { + if (window.document.currentStyle) { + return (dom, attr) => dom.currentStyle[attr] + } else { + return (dom, attr) => getComputedStyle(dom, false)[attr] + } + })() + + dialogHeaderEl.onmousedown = (e) => { + // 鼠标按下,计算当前元素距离可视区的距离 + const disX = e.clientX - dialogHeaderEl.offsetLeft + const disY = e.clientY - dialogHeaderEl.offsetTop + + const dragDomWidth = dragDom.offsetWidth + const dragDomHeight = dragDom.offsetHeight + + const screenWidth = document.body.clientWidth + const screenHeight = document.body.clientHeight + + const minDragDomLeft = dragDom.offsetLeft + const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth + + const minDragDomTop = dragDom.offsetTop + const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight + + // 获取到的值带px 正则匹配替换 + let styL = getStyle(dragDom, 'left') + let styT = getStyle(dragDom, 'top') + + if (styL.includes('%')) { + styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100) + styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100) + } else { + styL = +styL.replace(/\px/g, '') + styT = +styT.replace(/\px/g, '') + } + + document.onmousemove = function(e) { + // 通过事件委托,计算移动的距离 + let left = e.clientX - disX + let top = e.clientY - disY + + // 边界处理 + if (-(left) > minDragDomLeft) { + left = -minDragDomLeft + } else if (left > maxDragDomLeft) { + left = maxDragDomLeft + } + + if (-(top) > minDragDomTop) { + top = -minDragDomTop + } else if (top > maxDragDomTop) { + top = maxDragDomTop + } + + // 移动当前元素 + dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;` + + // emit onDrag event + vnode.child.$emit('dragDialog') + } + + document.onmouseup = function(e) { + document.onmousemove = null + document.onmouseup = null + } + } + } +} diff --git a/web/src/directive/el-drag-dialog/index.js b/web/src/directive/el-drag-dialog/index.js new file mode 100644 index 0000000..29facbf --- /dev/null +++ b/web/src/directive/el-drag-dialog/index.js @@ -0,0 +1,13 @@ +import drag from './drag' + +const install = function(Vue) { + Vue.directive('el-drag-dialog', drag) +} + +if (window.Vue) { + window['el-drag-dialog'] = drag + Vue.use(install); // eslint-disable-line +} + +drag.install = install +export default drag diff --git a/web/src/icons/index.js b/web/src/icons/index.js new file mode 100644 index 0000000..2c6b309 --- /dev/null +++ b/web/src/icons/index.js @@ -0,0 +1,9 @@ +import Vue from 'vue' +import SvgIcon from '@/components/SvgIcon'// svg component + +// register globally +Vue.component('svg-icon', SvgIcon) + +const req = require.context('./svg', false, /\.svg$/) +const requireAll = requireContext => requireContext.keys().map(requireContext) +requireAll(req) diff --git a/web/src/icons/svg/channelManger.svg b/web/src/icons/svg/channelManger.svg new file mode 100644 index 0000000..a6b2c95 --- /dev/null +++ b/web/src/icons/svg/channelManger.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/cloudRecord.svg b/web/src/icons/svg/cloudRecord.svg new file mode 100644 index 0000000..8d38fd9 --- /dev/null +++ b/web/src/icons/svg/cloudRecord.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/dashboard.svg b/web/src/icons/svg/dashboard.svg new file mode 100644 index 0000000..e2486bc --- /dev/null +++ b/web/src/icons/svg/dashboard.svg @@ -0,0 +1,4 @@ + + + diff --git a/web/src/icons/svg/device.svg b/web/src/icons/svg/device.svg new file mode 100644 index 0000000..e614bcb --- /dev/null +++ b/web/src/icons/svg/device.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/devices.svg b/web/src/icons/svg/devices.svg new file mode 100644 index 0000000..c7e7fc1 --- /dev/null +++ b/web/src/icons/svg/devices.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/devices1.svg b/web/src/icons/svg/devices1.svg new file mode 100644 index 0000000..10ded36 --- /dev/null +++ b/web/src/icons/svg/devices1.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/example.svg b/web/src/icons/svg/example.svg new file mode 100644 index 0000000..46f42b5 --- /dev/null +++ b/web/src/icons/svg/example.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/icons/svg/eye-open.svg b/web/src/icons/svg/eye-open.svg new file mode 100644 index 0000000..88dcc98 --- /dev/null +++ b/web/src/icons/svg/eye-open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/icons/svg/eye.svg b/web/src/icons/svg/eye.svg new file mode 100644 index 0000000..16ed2d8 --- /dev/null +++ b/web/src/icons/svg/eye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/icons/svg/form.svg b/web/src/icons/svg/form.svg new file mode 100644 index 0000000..dcbaa18 --- /dev/null +++ b/web/src/icons/svg/form.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/icons/svg/group.svg b/web/src/icons/svg/group.svg new file mode 100644 index 0000000..5fc4aa6 --- /dev/null +++ b/web/src/icons/svg/group.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/historyLog.svg b/web/src/icons/svg/historyLog.svg new file mode 100644 index 0000000..68eb16a --- /dev/null +++ b/web/src/icons/svg/historyLog.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/jtDevice.svg b/web/src/icons/svg/jtDevice.svg new file mode 100644 index 0000000..300fee6 --- /dev/null +++ b/web/src/icons/svg/jtDevice.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/link.svg b/web/src/icons/svg/link.svg new file mode 100644 index 0000000..48197ba --- /dev/null +++ b/web/src/icons/svg/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/icons/svg/live.svg b/web/src/icons/svg/live.svg new file mode 100644 index 0000000..8d9e7ec --- /dev/null +++ b/web/src/icons/svg/live.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/map.svg b/web/src/icons/svg/map.svg new file mode 100644 index 0000000..48479ae --- /dev/null +++ b/web/src/icons/svg/map.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/mediaServerList.svg b/web/src/icons/svg/mediaServerList.svg new file mode 100644 index 0000000..b6ed26b --- /dev/null +++ b/web/src/icons/svg/mediaServerList.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/nested.svg b/web/src/icons/svg/nested.svg new file mode 100644 index 0000000..06713a8 --- /dev/null +++ b/web/src/icons/svg/nested.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/icons/svg/operations.svg b/web/src/icons/svg/operations.svg new file mode 100644 index 0000000..a093c10 --- /dev/null +++ b/web/src/icons/svg/operations.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/password.svg b/web/src/icons/svg/password.svg new file mode 100644 index 0000000..e291d85 --- /dev/null +++ b/web/src/icons/svg/password.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/icons/svg/platform.svg b/web/src/icons/svg/platform.svg new file mode 100644 index 0000000..37e55a6 --- /dev/null +++ b/web/src/icons/svg/platform.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/realLog.svg b/web/src/icons/svg/realLog.svg new file mode 100644 index 0000000..b0c30bb --- /dev/null +++ b/web/src/icons/svg/realLog.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/recordPlan.svg b/web/src/icons/svg/recordPlan.svg new file mode 100644 index 0000000..b260f10 --- /dev/null +++ b/web/src/icons/svg/recordPlan.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/region.svg b/web/src/icons/svg/region.svg new file mode 100644 index 0000000..019b42f --- /dev/null +++ b/web/src/icons/svg/region.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/setting.svg b/web/src/icons/svg/setting.svg new file mode 100644 index 0000000..c74433b --- /dev/null +++ b/web/src/icons/svg/setting.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/streamProxy.svg b/web/src/icons/svg/streamProxy.svg new file mode 100644 index 0000000..9707a29 --- /dev/null +++ b/web/src/icons/svg/streamProxy.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/streamPush.svg b/web/src/icons/svg/streamPush.svg new file mode 100644 index 0000000..a776727 --- /dev/null +++ b/web/src/icons/svg/streamPush.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/systemInfo.svg b/web/src/icons/svg/systemInfo.svg new file mode 100644 index 0000000..f5fe622 --- /dev/null +++ b/web/src/icons/svg/systemInfo.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/table.svg b/web/src/icons/svg/table.svg new file mode 100644 index 0000000..0e3dc9d --- /dev/null +++ b/web/src/icons/svg/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/icons/svg/tree.svg b/web/src/icons/svg/tree.svg new file mode 100644 index 0000000..dd4b7dd --- /dev/null +++ b/web/src/icons/svg/tree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/icons/svg/user.svg b/web/src/icons/svg/user.svg new file mode 100644 index 0000000..0ba0716 --- /dev/null +++ b/web/src/icons/svg/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/icons/svgo.yml b/web/src/icons/svgo.yml new file mode 100644 index 0000000..d11906a --- /dev/null +++ b/web/src/icons/svgo.yml @@ -0,0 +1,22 @@ +# replace default config + +# multipass: true +# full: true + +plugins: + + # - name + # + # or: + # - name: false + # - name: true + # + # or: + # - name: + # param1: 1 + # param2: 2 + +- removeAttrs: + attrs: + - 'fill' + - 'fill-rule' diff --git a/web/src/layout/components/AppMain.vue b/web/src/layout/components/AppMain.vue new file mode 100644 index 0000000..01cc9a5 --- /dev/null +++ b/web/src/layout/components/AppMain.vue @@ -0,0 +1,45 @@ + + + + + + + diff --git a/web/src/layout/components/Navbar.vue b/web/src/layout/components/Navbar.vue new file mode 100644 index 0000000..2fca02b --- /dev/null +++ b/web/src/layout/components/Navbar.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/web/src/layout/components/Sidebar/FixiOSBug.js b/web/src/layout/components/Sidebar/FixiOSBug.js new file mode 100644 index 0000000..bc14856 --- /dev/null +++ b/web/src/layout/components/Sidebar/FixiOSBug.js @@ -0,0 +1,26 @@ +export default { + computed: { + device() { + return this.$store.state.app.device + } + }, + mounted() { + // In order to fix the click on menu on the ios device will trigger the mouseleave bug + // https://github.com/PanJiaChen/vue-element-admin/issues/1135 + this.fixBugIniOS() + }, + methods: { + fixBugIniOS() { + const $subMenu = this.$refs.subMenu + if ($subMenu) { + const handleMouseleave = $subMenu.handleMouseleave + $subMenu.handleMouseleave = (e) => { + if (this.device === 'mobile') { + return + } + handleMouseleave(e) + } + } + } + } +} diff --git a/web/src/layout/components/Sidebar/Item.vue b/web/src/layout/components/Sidebar/Item.vue new file mode 100644 index 0000000..aa1f5da --- /dev/null +++ b/web/src/layout/components/Sidebar/Item.vue @@ -0,0 +1,41 @@ + + + diff --git a/web/src/layout/components/Sidebar/Link.vue b/web/src/layout/components/Sidebar/Link.vue new file mode 100644 index 0000000..530b3d5 --- /dev/null +++ b/web/src/layout/components/Sidebar/Link.vue @@ -0,0 +1,43 @@ + + + diff --git a/web/src/layout/components/Sidebar/Logo.vue b/web/src/layout/components/Sidebar/Logo.vue new file mode 100644 index 0000000..cda7da2 --- /dev/null +++ b/web/src/layout/components/Sidebar/Logo.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/web/src/layout/components/Sidebar/SidebarItem.vue b/web/src/layout/components/Sidebar/SidebarItem.vue new file mode 100644 index 0000000..dc2ee00 --- /dev/null +++ b/web/src/layout/components/Sidebar/SidebarItem.vue @@ -0,0 +1,99 @@ + + + diff --git a/web/src/layout/components/Sidebar/index.vue b/web/src/layout/components/Sidebar/index.vue new file mode 100644 index 0000000..da39034 --- /dev/null +++ b/web/src/layout/components/Sidebar/index.vue @@ -0,0 +1,56 @@ + + + diff --git a/web/src/layout/components/TagsView/ScrollPane.vue b/web/src/layout/components/TagsView/ScrollPane.vue new file mode 100644 index 0000000..bb753a1 --- /dev/null +++ b/web/src/layout/components/TagsView/ScrollPane.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/web/src/layout/components/TagsView/index.vue b/web/src/layout/components/TagsView/index.vue new file mode 100644 index 0000000..c82555d --- /dev/null +++ b/web/src/layout/components/TagsView/index.vue @@ -0,0 +1,292 @@ + + + + + + + diff --git a/web/src/layout/components/dialog/changePassword.vue b/web/src/layout/components/dialog/changePassword.vue new file mode 100755 index 0000000..8a4b7b6 --- /dev/null +++ b/web/src/layout/components/dialog/changePassword.vue @@ -0,0 +1,119 @@ + + + diff --git a/web/src/layout/components/index.js b/web/src/layout/components/index.js new file mode 100644 index 0000000..9fc98d6 --- /dev/null +++ b/web/src/layout/components/index.js @@ -0,0 +1,4 @@ +export { default as Navbar } from './Navbar' +export { default as Sidebar } from './Sidebar' +export { default as AppMain } from './AppMain' +export { default as TagsView } from './TagsView' diff --git a/web/src/layout/index.vue b/web/src/layout/index.vue new file mode 100644 index 0000000..142ee74 --- /dev/null +++ b/web/src/layout/index.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/web/src/layout/mixin/ResizeHandler.js b/web/src/layout/mixin/ResizeHandler.js new file mode 100644 index 0000000..e8d0df8 --- /dev/null +++ b/web/src/layout/mixin/ResizeHandler.js @@ -0,0 +1,45 @@ +import store from '@/store' + +const { body } = document +const WIDTH = 992 // refer to Bootstrap's responsive design + +export default { + watch: { + $route(route) { + if (this.device === 'mobile' && this.sidebar.opened) { + store.dispatch('app/closeSideBar', { withoutAnimation: false }) + } + } + }, + beforeMount() { + window.addEventListener('resize', this.$_resizeHandler) + }, + beforeDestroy() { + window.removeEventListener('resize', this.$_resizeHandler) + }, + mounted() { + const isMobile = this.$_isMobile() + if (isMobile) { + store.dispatch('app/toggleDevice', 'mobile') + store.dispatch('app/closeSideBar', { withoutAnimation: true }) + } + }, + methods: { + // use $_ for mixins properties + // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential + $_isMobile() { + const rect = body.getBoundingClientRect() + return rect.width - 1 < WIDTH + }, + $_resizeHandler() { + if (!document.hidden) { + const isMobile = this.$_isMobile() + store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') + + if (isMobile) { + store.dispatch('app/closeSideBar', { withoutAnimation: true }) + } + } + } + } +} diff --git a/web/src/main.js b/web/src/main.js new file mode 100644 index 0000000..f4b9f07 --- /dev/null +++ b/web/src/main.js @@ -0,0 +1,54 @@ +import Vue from 'vue' + +import 'normalize.css/normalize.css' // A modern alternative to CSS resets + +import ElementUI from 'element-ui' +import 'element-ui/lib/theme-chalk/index.css' +import locale from 'element-ui/lib/locale/lang/en' // lang i18n + +import '@/styles/index.scss' // global css + +import App from './App' +import store from './store' +import router from './router' + +import '@/icons' // icon +import '@/permission' // permission control + +import VueClipboards from 'vue-clipboards' +import Contextmenu from 'vue-contextmenujs' +import VueClipboard from 'vue-clipboard2' + +/** + * If you don't want to use mock-server + * you want to use MockJs for mock api + * you can execute: mockXHR() + * + * Currently MockJs will be used in the production environment, + * please remove it before going online ! ! ! + */ +if (process.env.NODE_ENV === 'production') { + const { mockXHR } = require('../mock') + mockXHR() +} + +Vue.use(ElementUI) +Vue.use(VueClipboards) +Vue.use(Contextmenu) +Vue.use(VueClipboard) + +Vue.config.productionTip = false + +Vue.prototype.$channelTypeList = { + 1: { id: 1, name: '国标设备', style: { color: '#409eff', borderColor: '#b3d8ff' } }, + 2: { id: 2, name: '推流设备', style: { color: '#67c23a', borderColor: '#c2e7b0' } }, + 3: { id: 3, name: '拉流代理', style: { color: '#e6a23c', borderColor: '#f5dab1' } }, + 200: { id: 200, name: '部标设备', style: { color: '#fa6436', borderColor: '#f4997c' } } +} + +new Vue({ + el: '#app', + router, + store, + render: h => h(App) +}) diff --git a/web/src/permission.js b/web/src/permission.js new file mode 100644 index 0000000..0117d86 --- /dev/null +++ b/web/src/permission.js @@ -0,0 +1,52 @@ +import router from './router' +import store from './store' +import NProgress from 'nprogress' // progress bar +import 'nprogress/nprogress.css' // progress bar style +import { getToken, getName, getServerId } from '@/utils/auth' // get token from cookie +import getPageTitle from '@/utils/get-page-title' + +NProgress.configure({ showSpinner: false }) // NProgress Configuration + +const whiteList = ['/login'] // no redirect whitelist + +router.beforeEach(async(to, from, next) => { + // start progress bar + NProgress.start() + + // set page title + document.title = getPageTitle(to.meta.title) + + // determine whether the user has logged in + const hasToken = getToken() + + if (hasToken) { + if (to.path === '/login') { + // if is logged in, redirect to the home page + next({ path: '/' }) + NProgress.done() + } else { + const hasGetUserInfo = store.getters.name + if (!hasGetUserInfo) { + store.commit('user/SET_NAME', getName()) + store.commit('user/SET_SERVER_ID', getServerId()) + } + next() + } + } else { + /* has no token*/ + + if (whiteList.indexOf(to.path) !== -1) { + // in the free login whitelist, go directly + next() + } else { + // other pages that do not have permission to access are redirected to the login page. + next(`/login?redirect=${to.path}`) + NProgress.done() + } + } +}) + +router.afterEach(() => { + // finish progress bar + NProgress.done() +}) diff --git a/web/src/router/index.js b/web/src/router/index.js new file mode 100644 index 0000000..1d844fa --- /dev/null +++ b/web/src/router/index.js @@ -0,0 +1,306 @@ +import Vue from 'vue' +import Router from 'vue-router' + +Vue.use(Router) + +/* Layout */ +import Layout from '@/layout' + +/** + * Note: sub-menu only appear when route children.length >= 1 + * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html + * + * hidden: true if set true, item will not show in the sidebar(default is false) + * alwaysShow: true if set true, will always show the root menu + * if not set alwaysShow, when item has more than one children route, + * it will becomes nested mode, otherwise not show the root menu + * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb + * name:'router-name' the name is used by (must set!!!) + * meta : { + roles: ['admin','editor'] control the page roles (you can set multiple roles) + title: 'title' the name show in sidebar and breadcrumb (recommend set) + icon: 'svg-name'/'el-icon-x' the icon show in the sidebar + breadcrumb: false if set false, the item will hidden in breadcrumb(default is true) + activeMenu: '/example/list' if set path, the sidebar will highlight the path you set + } + */ + +/** + * constantRoutes + * a base page that does not have permission requirements + * all roles can be accessed + */ +export const constantRoutes = [ + { + path: '/login', + component: () => import('@/views/login/index'), + hidden: true + }, + + { + path: '/404', + component: () => import('@/views/404'), + hidden: true + }, + + { + path: '/', + component: Layout, + redirect: '/dashboard', + children: [{ + path: 'dashboard', + name: '控制台', + component: () => import('@/views/dashboard/index'), + meta: { title: '控制台', icon: 'dashboard', affix: true } + }] + }, + + { + path: '/live', + component: Layout, + redirect: '/live', + children: [{ + path: '', + name: 'Live', + component: () => import('@/views/live/index'), + meta: { title: '分屏监控', icon: 'live' } + }] + }, + { + path: '/channel', + component: Layout, + redirect: '/channel', + onlyIndex: 0, + children: [{ + path: '/channel', + name: 'Channel', + component: () => import('@/views/channel/index'), + meta: { title: '通道列表', icon: 'channelManger'} + }, + { + path: '/channel/record/:channelId', + name: 'CommonRecord', + component: () => import('@/views/channel/record'), + meta: { title: '设备录像' } + } + ] + }, + { + path: '/map', + component: Layout, + redirect: '/map', + children: [{ + path: '', + name: 'Map', + component: () => import('@/views/map/index'), + meta: { title: '电子地图', icon: 'map' } + }] + }, + { + path: '/device', + component: Layout, + name: '设备接入', + meta: { title: '设备接入', icon: 'devices' }, + children: [ + { + path: '/device', + name: 'Device', + component: () => import('@/views/device/index'), + meta: { title: '国标设备', icon: 'device' } + }, + { + hidden: true, + path: '/device/record/:deviceId/:channelDeviceId', + name: 'DeviceRecord', + component: () => import('@/views/device/channel/record'), + meta: { title: '国标录像' } + }, + { + path: '/jtDevice', + name: 'JTDevice', + component: () => import('@/views/jtDevice/index'), + meta: { title: '部标设备', icon: 'jtDevice' } + }, + { + hidden: true, + path: '/jtDevice/record/:phoneNumber/:channelId', + name: 'JTDeviceRecord', + component: () => import('@/views/jtDevice/channel/record'), + meta: { title: '部标录像' } + }, + { + path: '/push', + name: 'PushList', + component: () => import('@/views/streamPush/index'), + meta: { title: '推流列表', icon: 'streamPush' } + }, + { + path: '/proxy', + name: 'Proxy', + component: () => import('@/views/streamProxy/index'), + meta: { title: '拉流代理', icon: 'streamProxy' } + } + ] + }, + { + path: '/commonChannel', + component: Layout, + redirect: '/commonChannel/region', + name: '组织结构', + meta: { title: '组织结构', icon: 'tree' }, + children: [ + { + path: 'region', + name: 'Region', + component: () => import('@/views/channel/region/index'), + meta: { title: '行政区划', icon: 'region' } + }, + { + path: 'group', + name: 'Group', + component: () => import('@/views/channel/group/index'), + meta: { title: '业务分组', icon: 'tree' } + } + ] + }, + { + path: '/recordPlan', + component: Layout, + redirect: '/recordPlan', + children: [ + { + path: '', + name: 'RecordPlan', + component: () => import('@/views/recordPlan/index'), + meta: { title: '录制计划', icon: 'recordPlan' } + } + ] + }, + { + path: '/cloudRecord', + component: Layout, + redirect: '/cloudRecord', + onlyIndex: 0, + children: [ + { + path: '/cloudRecord', + name: 'CloudRecord', + component: () => import('@/views/cloudRecord/index'), + meta: { title: '云端录像', icon: 'cloudRecord' } + }, + { + path: '/cloudRecord/detail/:app/:stream', + name: 'CloudRecordDetail', + component: () => import('@/views/cloudRecord/detail'), + meta: { title: '云端录像详情' } + } + ] + }, + { + path: '/mediaServer', + component: Layout, + redirect: '/mediaServer', + children: [ + { + path: '', + name: 'MediaServer', + component: () => import('@/views/mediaServer/index'), + meta: { title: '媒体节点', icon: 'mediaServerList' } + } + ] + }, + { + path: '/platform', + component: Layout, + redirect: '/platform', + children: [ + { + path: '', + name: 'Platform', + component: () => import('@/views/platform/index'), + meta: { title: '国标级联', icon: 'platform' } + } + ] + }, + { + path: '/user', + component: Layout, + redirect: '/user', + children: [ + { + path: '', + name: 'User', + component: () => import('@/views/user/index'), + meta: { title: '用户管理', icon: 'user' } + } + ] + }, + // { + // path: '/setting', + // component: Layout, + // redirect: '/setting', + // children: [ + // { + // path: '', + // name: '系统设置', + // component: () => import('@/views/platform/index'), + // meta: { title: '系统设置', icon: 'setting' } + // } + // ] + // }, + { + path: '/operations', + component: Layout, + meta: { title: '运维中心', icon: 'operations' }, + redirect: '/operations/systemInfo', + children: [ + { + path: '/operations/systemInfo', + name: 'OperationsSystemInfo', + component: () => import('@/views/operations/systemInfo'), + meta: { title: '平台信息', icon: 'systemInfo' } + }, + { + path: '/operations/historyLog', + name: 'OperationsHistoryLog', + component: () => import('@/views/operations/historyLog'), + meta: { title: '历史日志', icon: 'historyLog' } + }, + { + path: '/operations/realLog', + name: 'OperationsRealLog', + component: () => import('@/views/operations/realLog'), + meta: { title: '实时日志', icon: 'realLog' } + } + ] + }, + { + path: '/play/wasm/:url', + name: 'wasmPlayer', + hidden: true, + component: () => import('@/views/common/jessibuca.vue') + }, + { + path: '/play/rtc/:url', + name: 'rtcPlayer', + component: () => import('@/views/common/rtcPlayer.vue') + }, + // 404 page must be placed at the end !!! + { path: '*', redirect: '/404', hidden: true } +] + +const createRouter = () => new Router({ + // mode: 'history', // require service support + scrollBehavior: () => ({ y: 0 }), + routes: constantRoutes +}) + +const router = createRouter() + +// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 +export function resetRouter() { + const newRouter = createRouter() + router.matcher = newRouter.matcher // reset router +} + +export default router diff --git a/web/src/settings.js b/web/src/settings.js new file mode 100644 index 0000000..300dac7 --- /dev/null +++ b/web/src/settings.js @@ -0,0 +1,18 @@ +module.exports = { + + title: 'WVP视频平台', + + /** + * @type {boolean} true | false + * @description Whether fix the header + */ + fixedHeader: false, + + /** + * @type {boolean} true | false + * @description Whether show the logo in sidebar + */ + sidebarLogo: false, + + tagsView: true +} diff --git a/web/src/store/getters.js b/web/src/store/getters.js new file mode 100644 index 0000000..df46aa2 --- /dev/null +++ b/web/src/store/getters.js @@ -0,0 +1,11 @@ +const getters = { + sidebar: state => state.app.sidebar, + device: state => state.app.device, + token: state => state.user.token, + showConfirmBoxForLoginLose: state => state.user.showConfirmBoxForLoginLose, + serverId: state => state.user.serverId, + name: state => state.user.name, + visitedViews: state => state.tagsView.visitedViews, + cachedViews: state => state.tagsView.cachedViews +} +export default getters diff --git a/web/src/store/index.js b/web/src/store/index.js new file mode 100644 index 0000000..138c4ab --- /dev/null +++ b/web/src/store/index.js @@ -0,0 +1,57 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import getters from './getters' +import app from './modules/app' +import settings from './modules/settings' +import user from './modules/user' +import tagsView from './modules/tagsView' +import commonChanel from './modules/commonChanel' +import region from './modules/region' +import device from './modules/device' +import group from './modules/group' +import server from './modules/server' +import play from './modules/play' +import playback from './modules/playback' +import streamPush from './modules/streamPush' +import streamProxy from './modules/streamProxy' +import recordPlan from './modules/recordPlan' +import cloudRecord from './modules/cloudRecord' +import platform from './modules/platform' +import role from './modules/role' +import userApiKeys from './modules/userApiKeys' +import gbRecord from './modules/gbRecord' +import log from './modules/log' +import frontEnd from './modules/frontEnd' +import jtDevice from './modules/jtDevice' + +Vue.use(Vuex) + +const store = new Vuex.Store({ + modules: { + app, + settings, + user, + tagsView, + commonChanel, + region, + device, + group, + server, + play, + playback, + streamPush, + streamProxy, + recordPlan, + cloudRecord, + platform, + role, + userApiKeys, + gbRecord, + log, + frontEnd, + jtDevice + }, + getters +}) + +export default store diff --git a/web/src/store/modules/app.js b/web/src/store/modules/app.js new file mode 100644 index 0000000..7ea7e33 --- /dev/null +++ b/web/src/store/modules/app.js @@ -0,0 +1,48 @@ +import Cookies from 'js-cookie' + +const state = { + sidebar: { + opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, + withoutAnimation: false + }, + device: 'desktop' +} + +const mutations = { + TOGGLE_SIDEBAR: state => { + state.sidebar.opened = !state.sidebar.opened + state.sidebar.withoutAnimation = false + if (state.sidebar.opened) { + Cookies.set('sidebarStatus', 1) + } else { + Cookies.set('sidebarStatus', 0) + } + }, + CLOSE_SIDEBAR: (state, withoutAnimation) => { + Cookies.set('sidebarStatus', 0) + state.sidebar.opened = false + state.sidebar.withoutAnimation = withoutAnimation + }, + TOGGLE_DEVICE: (state, device) => { + state.device = device + } +} + +const actions = { + toggleSideBar({ commit }) { + commit('TOGGLE_SIDEBAR') + }, + closeSideBar({ commit }, { withoutAnimation }) { + commit('CLOSE_SIDEBAR', withoutAnimation) + }, + toggleDevice({ commit }, device) { + commit('TOGGLE_DEVICE', device) + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/web/src/store/modules/cloudRecord.js b/web/src/store/modules/cloudRecord.js new file mode 100644 index 0000000..c27d925 --- /dev/null +++ b/web/src/store/modules/cloudRecord.js @@ -0,0 +1,109 @@ +import { + addTask, deleteRecord, + getPlayPath, + loadRecord, + queryList, + queryListByData, + queryTaskList, + seek, + speed +} from '@/api/cloudRecord' + +const actions = { + getPlayPath({ commit }, id) { + return new Promise((resolve, reject) => { + getPlayPath(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + loadRecord({ commit }, params) { + return new Promise((resolve, reject) => { + loadRecord(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + seek({ commit }, params) { + return new Promise((resolve, reject) => { + seek(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + speed({ commit }, params) { + return new Promise((resolve, reject) => { + speed(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryListByData({ commit }, params) { + return new Promise((resolve, reject) => { + queryListByData(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + addTask({ commit }, params) { + return new Promise((resolve, reject) => { + addTask(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryTaskList({ commit }, params) { + return new Promise((resolve, reject) => { + queryTaskList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryList({ commit }, params) { + return new Promise((resolve, reject) => { + queryList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + deleteRecord({ commit }, ids) { + return new Promise((resolve, reject) => { + deleteRecord(ids).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/commonChanel.js b/web/src/store/modules/commonChanel.js new file mode 100644 index 0000000..04143b4 --- /dev/null +++ b/web/src/store/modules/commonChanel.js @@ -0,0 +1,652 @@ +import { + update, + add, + reset, + queryOne, + addDeviceToGroup, + deleteDeviceFromGroup, + addDeviceToRegion, + deleteDeviceFromRegion, + getCivilCodeList, + getParentList, + getUnusualParentList, + clearUnusualParentList, + getUnusualCivilCodeList, + clearUnusualCivilCodeList, + getIndustryList, + getTypeList, + getNetworkIdentificationList, + playChannel, + addToRegion, + deleteFromRegion, + addToGroup, + deleteFromGroup, + getList, + addPointForCruise, + addPreset, + auxiliary, + callPreset, + deletePointForCruise, + deletePreset, + focus, + iris, + ptz, + queryPreset, + setCruiseSpeed, + setCruiseTime, + setLeftForScan, + setRightForScan, + setSpeedForScan, + startCruise, + startScan, + stopCruise, + stopScan, + wiper, + stopPlayChannel, + queryRecord, + playback, + stopPlayback, + pausePlayback, + resumePlayback, + seekPlayback, speedPlayback, getAllForMap, test, saveLevel, resetLevel, clearThin, thinProgress, drawThin, saveThin +} from '@/api/commonChannel' + +const actions = { + update({ commit }, formData) { + return new Promise((resolve, reject) => { + update(formData).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + add({ commit }, formData) { + return new Promise((resolve, reject) => { + add(formData).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + reset({ commit }, data) { + return new Promise((resolve, reject) => { + reset(data).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryOne({ commit }, id) { + return new Promise((resolve, reject) => { + queryOne(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + addDeviceToGroup({ commit }, params) { + return new Promise((resolve, reject) => { + addDeviceToGroup(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + addToGroup({ commit }, params) { + return new Promise((resolve, reject) => { + addToGroup(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + deleteDeviceFromGroup({ commit }, deviceIds) { + return new Promise((resolve, reject) => { + deleteDeviceFromGroup(deviceIds).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + deleteFromGroup({ commit }, channels) { + return new Promise((resolve, reject) => { + deleteFromGroup(channels).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + addDeviceToRegion({ commit }, params) { + return new Promise((resolve, reject) => { + addDeviceToRegion(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + addToRegion({ commit }, params) { + return new Promise((resolve, reject) => { + addToRegion(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + deleteDeviceFromRegion({ commit }, deviceIds) { + return new Promise((resolve, reject) => { + deleteDeviceFromRegion(deviceIds).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + deleteFromRegion({ commit }, channels) { + return new Promise((resolve, reject) => { + deleteFromRegion(channels).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getCivilCodeList({ commit }, params) { + return new Promise((resolve, reject) => { + getCivilCodeList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getParentList({ commit }, params) { + return new Promise((resolve, reject) => { + getParentList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getUnusualParentList({ commit }, params) { + return new Promise((resolve, reject) => { + getUnusualParentList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + clearUnusualParentList({ commit }, params) { + return new Promise((resolve, reject) => { + clearUnusualParentList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getUnusualCivilCodeList({ commit }, params) { + return new Promise((resolve, reject) => { + getUnusualCivilCodeList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + clearUnusualCivilCodeList({ commit }, params) { + return new Promise((resolve, reject) => { + clearUnusualCivilCodeList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getIndustryList({ commit }) { + return new Promise((resolve, reject) => { + getIndustryList().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getTypeList({ commit }) { + return new Promise((resolve, reject) => { + getTypeList().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getNetworkIdentificationList({ commit }) { + return new Promise((resolve, reject) => { + getNetworkIdentificationList().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + playChannel({ commit }, channelId) { + return new Promise((resolve, reject) => { + playChannel(channelId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + stopPlayChannel({ commit }, channelId) { + return new Promise((resolve, reject) => { + stopPlayChannel(channelId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getList({ commit }, param) { + return new Promise((resolve, reject) => { + getList(param).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + setSpeedForScan({ commit }, params) { + return new Promise((resolve, reject) => { + setSpeedForScan(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + setLeftForScan({ commit }, params) { + return new Promise((resolve, reject) => { + setLeftForScan(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + setRightForScan({ commit }, params) { + return new Promise((resolve, reject) => { + setRightForScan(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + startScan({ commit }, params) { + return new Promise((resolve, reject) => { + startScan(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + stopScan({ commit }, params) { + return new Promise((resolve, reject) => { + stopScan(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + addPointForCruise({ commit }, params) { + return new Promise((resolve, reject) => { + addPointForCruise(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + deletePointForCruise({ commit }, params) { + return new Promise((resolve, reject) => { + deletePointForCruise(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + setCruiseSpeed({ commit }, params) { + return new Promise((resolve, reject) => { + setCruiseSpeed(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + setCruiseTime({ commit }, params) { + return new Promise((resolve, reject) => { + setCruiseTime(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + startCruise({ commit }, params) { + return new Promise((resolve, reject) => { + startCruise(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + stopCruise({ commit }, params) { + return new Promise((resolve, reject) => { + stopCruise(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + addPreset({ commit }, params) { + return new Promise((resolve, reject) => { + addPreset(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryPreset({ commit }, params) { + return new Promise((resolve, reject) => { + queryPreset(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + callPreset({ commit }, params) { + return new Promise((resolve, reject) => { + callPreset(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + deletePreset({ commit }, params) { + return new Promise((resolve, reject) => { + deletePreset(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + auxiliary({ commit }, params) { + return new Promise((resolve, reject) => { + auxiliary(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + wiper({ commit }, params) { + return new Promise((resolve, reject) => { + wiper(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + ptz({ commit }, params) { + return new Promise((resolve, reject) => { + ptz(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + iris({ commit }, params) { + return new Promise((resolve, reject) => { + iris(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + focus({ commit }, params) { + return new Promise((resolve, reject) => { + focus(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryRecord({ commit }, params) { + return new Promise((resolve, reject) => { + queryRecord(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + playback({ commit }, params) { + return new Promise((resolve, reject) => { + playback(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + stopPlayback({ commit }, params) { + return new Promise((resolve, reject) => { + stopPlayback(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + pausePlayback({ commit }, params) { + return new Promise((resolve, reject) => { + pausePlayback(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + resumePlayback({ commit }, params) { + return new Promise((resolve, reject) => { + resumePlayback(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + seekPlayback({ commit }, params) { + return new Promise((resolve, reject) => { + seekPlayback(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + speedPlayback({ commit }, params) { + return new Promise((resolve, reject) => { + speedPlayback(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getAllForMap({ commit }, params) { + return new Promise((resolve, reject) => { + getAllForMap(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + saveLevel({ commit }, data) { + return new Promise((resolve, reject) => { + saveLevel(data).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + resetLevel({ commit }) { + return new Promise((resolve, reject) => { + resetLevel().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + clearThin({ commit }, id) { + return new Promise((resolve, reject) => { + clearThin(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + thinProgress({ commit }, id) { + return new Promise((resolve, reject) => { + thinProgress(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + saveThin({ commit }, id) { + return new Promise((resolve, reject) => { + saveThin(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + drawThin({ commit }, param) { + return new Promise((resolve, reject) => { + drawThin(param).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + + test({ commit }) { + return new Promise((resolve, reject) => { + test().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/device.js b/web/src/store/modules/device.js new file mode 100644 index 0000000..d926524 --- /dev/null +++ b/web/src/store/modules/device.js @@ -0,0 +1,252 @@ +import { + add, + changeChannelAudio, + deleteDevice, + deviceRecord, + queryBasicParam, + queryChannelOne, + queryChannels, + queryChannelTree, + queryDeviceOne, + queryDevices, + queryDeviceSyncStatus, + queryDeviceTree, + queryHasStreamChannels, + resetGuard, + setGuard, + subscribeCatalog, + subscribeMobilePosition, + sync, + update, + updateChannelStreamIdentification, + updateDeviceTransport +} from '@/api/device' + +const actions = { + queryDeviceSyncStatus({ commit }, deviceId) { + return new Promise((resolve, reject) => { + queryDeviceSyncStatus(deviceId).then(response => { + // const {data, code, msg} = response + resolve(response) + }).catch(error => { + reject(error) + }) + }) + }, + queryDevices({ commit }, params) { + return new Promise((resolve, reject) => { + queryDevices(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + sync({ commit }, deviceId) { + return new Promise((resolve, reject) => { + sync(deviceId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + updateDeviceTransport({ commit }, [deviceId, streamMode]) { + return new Promise((resolve, reject) => { + updateDeviceTransport(deviceId, streamMode).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + setGuard({ commit }, deviceId) { + return new Promise((resolve, reject) => { + setGuard(deviceId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + resetGuard({ commit }, deviceId) { + return new Promise((resolve, reject) => { + resetGuard(deviceId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + subscribeCatalog({ commit }, params) { + return new Promise((resolve, reject) => { + subscribeCatalog(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + subscribeMobilePosition({ commit }, params) { + return new Promise((resolve, reject) => { + subscribeMobilePosition(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryBasicParam({ commit }, deviceId) { + return new Promise((resolve, reject) => { + queryBasicParam(deviceId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryChannelOne({ commit }, params) { + return new Promise((resolve, reject) => { + queryChannelOne(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryChannels({ commit }, [deviceId, params]) { + return new Promise((resolve, reject) => { + queryChannels(deviceId, params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryHasStreamChannels({commit}, params) { + return new Promise((resolve, reject) => { + queryHasStreamChannels(params).then(response => { + const {data} = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + deviceRecord({ commit }, params) { + return new Promise((resolve, reject) => { + deviceRecord(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + querySubChannels({ commit }, [params, deviceId, parentChannelId]) { + return new Promise((resolve, reject) => { + deviceRecord(params, deviceId, parentChannelId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryChannelTree({ commit }, params) { + return new Promise((resolve, reject) => { + queryChannelTree(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + changeChannelAudio({ commit }, params) { + return new Promise((resolve, reject) => { + changeChannelAudio(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + updateChannelStreamIdentification({ commit }, params) { + return new Promise((resolve, reject) => { + updateChannelStreamIdentification(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + update({ commit }, formData) { + return new Promise((resolve, reject) => { + update(formData).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + add({ commit }, formData) { + return new Promise((resolve, reject) => { + add(formData).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryDeviceOne({ commit }, deviceId) { + return new Promise((resolve, reject) => { + queryDeviceOne(deviceId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryDeviceTree({ commit }, params, deviceId) { + return new Promise((resolve, reject) => { + queryDeviceTree(params, deviceId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + deleteDevice({ commit }, deviceId) { + return new Promise((resolve, reject) => { + deleteDevice(deviceId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/frontEnd.js b/web/src/store/modules/frontEnd.js new file mode 100644 index 0000000..262fdb8 --- /dev/null +++ b/web/src/store/modules/frontEnd.js @@ -0,0 +1,218 @@ +import { + addPointForCruise, addPreset, auxiliary, callPreset, deletePointForCruise, deletePreset, focus, iris, ptz, + queryPreset, setCruiseSpeed, setCruiseTime, + setLeftForScan, + setRightForScan, + setSpeedForScan, startCruise, + startScan, stopCruise, + stopScan, wiper +} from '@/api/frontEnd' + +const actions = { + setSpeedForScan({ commit }, params) { + return new Promise((resolve, reject) => { + setSpeedForScan(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + setLeftForScan({ commit }, params) { + return new Promise((resolve, reject) => { + setLeftForScan(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + setRightForScan({ commit }, params) { + return new Promise((resolve, reject) => { + setRightForScan(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + startScan({ commit }, params) { + return new Promise((resolve, reject) => { + startScan(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + stopScan({ commit }, params) { + return new Promise((resolve, reject) => { + stopScan(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + addPointForCruise({ commit }, params) { + return new Promise((resolve, reject) => { + addPointForCruise(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + deletePointForCruise({ commit }, params) { + return new Promise((resolve, reject) => { + deletePointForCruise(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + setCruiseSpeed({ commit }, params) { + return new Promise((resolve, reject) => { + setCruiseSpeed(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + setCruiseTime({ commit }, params) { + return new Promise((resolve, reject) => { + setCruiseTime(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + startCruise({ commit }, params) { + return new Promise((resolve, reject) => { + startCruise(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + stopCruise({ commit }, params) { + return new Promise((resolve, reject) => { + stopCruise(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + addPreset({ commit }, params) { + return new Promise((resolve, reject) => { + addPreset(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryPreset({ commit }, params) { + return new Promise((resolve, reject) => { + queryPreset(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + callPreset({ commit }, params) { + return new Promise((resolve, reject) => { + callPreset(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + deletePreset({ commit }, params) { + return new Promise((resolve, reject) => { + deletePreset(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + auxiliary({ commit }, params) { + return new Promise((resolve, reject) => { + auxiliary(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + wiper({ commit }, params) { + return new Promise((resolve, reject) => { + wiper(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + ptz({ commit }, params) { + return new Promise((resolve, reject) => { + ptz(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + iris({ commit }, params) { + return new Promise((resolve, reject) => { + iris(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + focus({ commit }, params) { + return new Promise((resolve, reject) => { + focus(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/gbRecord.js b/web/src/store/modules/gbRecord.js new file mode 100644 index 0000000..30c4460 --- /dev/null +++ b/web/src/store/modules/gbRecord.js @@ -0,0 +1,51 @@ + +import { query, queryDownloadProgress, startDownLoad, stopDownLoad } from '@/api/gbRecord' + +const actions = { + query({ commit }, param) { + return new Promise((resolve, reject) => { + query(param).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + startDownLoad({ commit }, param) { + return new Promise((resolve, reject) => { + startDownLoad(param).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + stopDownLoad({ commit }, deviceId, channelId, streamId) { + return new Promise((resolve, reject) => { + stopDownLoad(deviceId, channelId, streamId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryDownloadProgress({ commit }, param) { + return new Promise((resolve, reject) => { + queryDownloadProgress(param).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/group.js b/web/src/store/modules/group.js new file mode 100644 index 0000000..e57a6d7 --- /dev/null +++ b/web/src/store/modules/group.js @@ -0,0 +1,84 @@ +import { + getTreeList, + update, + add, deleteGroup, getPath, queryTree, sync +} from '@/api/group' + +const actions = { + update({ commit }, formData) { + return new Promise((resolve, reject) => { + update(formData).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + add({ commit }, formData) { + return new Promise((resolve, reject) => { + add(formData).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getTreeList({ commit }, params) { + return new Promise((resolve, reject) => { + getTreeList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + deleteGroup({ commit }, id) { + return new Promise((resolve, reject) => { + deleteGroup(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getPath({ commit }, params) { + return new Promise((resolve, reject) => { + getPath(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryTree({ commit }, param) { + return new Promise((resolve, reject) => { + queryTree(param).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + sync({ commit }) { + return new Promise((resolve, reject) => { + sync().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/jtDevice.js b/web/src/store/modules/jtDevice.js new file mode 100644 index 0000000..65810b8 --- /dev/null +++ b/web/src/store/modules/jtDevice.js @@ -0,0 +1,402 @@ +import { + add, + addChannel, + connection, + controlDoor, + controlPlayback, + deleteDevice, + factoryReset, + fillLight, + getRecordTempUrl, + linkDetection, + play, + ptz, + queryAttribute, + queryChannels, + queryConfig, + queryDeviceById, + queryDevices, + queryDriverInfo, + queryMediaAttribute, queryMediaData, + queryPosition, + queryRecordList, + reset, + sendTextMessage, + setConfig, setPhoneBook, shooting, + startPlayback, startTalk, + stopPlay, + stopPlayback, stopTalk, + telephoneCallback, + update, + updateChannel, + wiper +} from '@/api/jtDevice' + +const actions = { + queryDevices({ commit }, params) { + return new Promise((resolve, reject) => { + queryDevices(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + add({ commit }, params) { + return new Promise((resolve, reject) => { + add(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + update({ commit }, params) { + return new Promise((resolve, reject) => { + update(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryDeviceById({ commit }, deviceId) { + return new Promise((resolve, reject) => { + queryDeviceById(deviceId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + deleteDevice({ commit }, phoneNumber) { + return new Promise((resolve, reject) => { + deleteDevice(phoneNumber).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryChannels({ commit }, params) { + return new Promise((resolve, reject) => { + queryChannels(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + play({ commit }, params) { + return new Promise((resolve, reject) => { + play(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + stopPlay({ commit }, params) { + return new Promise((resolve, reject) => { + stopPlay(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + updateChannel({ commit }, data) { + return new Promise((resolve, reject) => { + updateChannel(data).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + addChannel({ commit }, data) { + return new Promise((resolve, reject) => { + addChannel(data).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + ptz({ commit }, params) { + return new Promise((resolve, reject) => { + ptz(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + wiper({ commit }, params) { + return new Promise((resolve, reject) => { + wiper(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + fillLight({ commit }, params) { + return new Promise((resolve, reject) => { + fillLight(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryConfig({ commit }, phoneNumber) { + return new Promise((resolve, reject) => { + queryConfig(phoneNumber).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + setConfig({ commit }, data) { + return new Promise((resolve, reject) => { + setConfig(data).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryRecordList({ commit }, params) { + return new Promise((resolve, reject) => { + queryRecordList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + startPlayback({ commit }, params) { + return new Promise((resolve, reject) => { + startPlayback(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + controlPlayback({ commit }, params) { + return new Promise((resolve, reject) => { + controlPlayback(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + stopPlayback({ commit }, params) { + return new Promise((resolve, reject) => { + stopPlayback(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getRecordTempUrl({ commit }, params) { + return new Promise((resolve, reject) => { + getRecordTempUrl(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryAttribute({ commit }, phoneNumber) { + return new Promise((resolve, reject) => { + queryAttribute(phoneNumber).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + linkDetection({ commit }, phoneNumber) { + return new Promise((resolve, reject) => { + linkDetection(phoneNumber).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryPosition({ commit }, phoneNumber) { + return new Promise((resolve, reject) => { + queryPosition(phoneNumber).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + sendTextMessage({ commit }, data) { + return new Promise((resolve, reject) => { + sendTextMessage(data).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + telephoneCallback({ commit }, param) { + return new Promise((resolve, reject) => { + telephoneCallback(param).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryDriverInfo({ commit }, phoneNumber) { + return new Promise((resolve, reject) => { + queryDriverInfo(phoneNumber).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + factoryReset({ commit }, phoneNumber) { + return new Promise((resolve, reject) => { + factoryReset(phoneNumber).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + reset({ commit }, phoneNumber) { + return new Promise((resolve, reject) => { + reset(phoneNumber).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + connection({ commit }, data) { + return new Promise((resolve, reject) => { + connection(data).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + controlDoor({ commit }, param) { + return new Promise((resolve, reject) => { + controlDoor(param).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryMediaAttribute({ commit }, phoneNumber) { + return new Promise((resolve, reject) => { + queryMediaAttribute(phoneNumber).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + setPhoneBook({ commit }, data) { + return new Promise((resolve, reject) => { + setPhoneBook(data).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryMediaData({ commit }, data) { + return new Promise((resolve, reject) => { + queryMediaData(data).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + shooting({ commit }, data) { + return new Promise((resolve, reject) => { + shooting(data).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + startTalk({ commit }, param) { + return new Promise((resolve, reject) => { + startTalk(param).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + stopTalk({ commit }, param) { + return new Promise((resolve, reject) => { + stopTalk(param).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/log.js b/web/src/store/modules/log.js new file mode 100644 index 0000000..65616a7 --- /dev/null +++ b/web/src/store/modules/log.js @@ -0,0 +1,20 @@ +import { queryList } from '@/api/log' + +const actions = { + queryList({ commit }, params) { + return new Promise((resolve, reject) => { + queryList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/platform.js b/web/src/store/modules/platform.js new file mode 100644 index 0000000..d2ab6ff --- /dev/null +++ b/web/src/store/modules/platform.js @@ -0,0 +1,150 @@ +import { + add, + addChannel, addChannelByDevice, + exit, + getChannelList, + getServerConfig, + pushChannel, + query, + remove, removeChannel, removeChannelByDevice, + update, updateCustomChannel +} from '@/api/platform' + +const actions = { + update({ commit }, data) { + return new Promise((resolve, reject) => { + update(data).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + add({ commit }, data) { + return new Promise((resolve, reject) => { + add(data).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + exit({ commit }, deviceGbId) { + return new Promise((resolve, reject) => { + exit(deviceGbId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + remove({ commit }, id) { + return new Promise((resolve, reject) => { + remove(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + pushChannel({ commit }, id) { + return new Promise((resolve, reject) => { + pushChannel(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getServerConfig({ commit }) { + return new Promise((resolve, reject) => { + getServerConfig().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + query({ commit }, params) { + return new Promise((resolve, reject) => { + query(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getChannelList({ commit }, params) { + return new Promise((resolve, reject) => { + getChannelList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + addChannel({ commit }, params) { + return new Promise((resolve, reject) => { + addChannel(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + addChannelByDevice({ commit }, params) { + return new Promise((resolve, reject) => { + addChannelByDevice(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + removeChannelByDevice({ commit }, params) { + return new Promise((resolve, reject) => { + removeChannelByDevice(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + removeChannel({ commit }, params) { + return new Promise((resolve, reject) => { + removeChannel(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + updateCustomChannel({ commit }, data) { + return new Promise((resolve, reject) => { + updateCustomChannel(data).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/play.js b/web/src/store/modules/play.js new file mode 100644 index 0000000..9ec08a9 --- /dev/null +++ b/web/src/store/modules/play.js @@ -0,0 +1,50 @@ +import { broadcastStart, broadcastStop, play, stop } from '@/api/play' + +const actions = { + play({ commit }, [deviceId, channelId]) { + return new Promise((resolve, reject) => { + play(deviceId, channelId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + stop({ commit }, [deviceId, channelId]) { + return new Promise((resolve, reject) => { + stop(deviceId, channelId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + broadcastStart({ commit }, [deviceId, channelId, broadcastMode]) { + return new Promise((resolve, reject) => { + broadcastStart(deviceId, channelId, broadcastMode).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + broadcastStop({ commit }, [deviceId, channelId]) { + return new Promise((resolve, reject) => { + broadcastStop(deviceId, channelId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/playback.js b/web/src/store/modules/playback.js new file mode 100644 index 0000000..5eb8224 --- /dev/null +++ b/web/src/store/modules/playback.js @@ -0,0 +1,60 @@ +import { pause, play, resume, setSpeed, stop } from '@/api/playback' + +const actions = { + play({ commit }, data) { + return new Promise((resolve, reject) => { + play(data).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + resume({ commit }, streamId) { + return new Promise((resolve, reject) => { + resume(streamId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + pause({ commit }, streamId) { + return new Promise((resolve, reject) => { + pause(streamId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + setSpeed({ commit }, param) { + return new Promise((resolve, reject) => { + setSpeed(param).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + stop({ commit }, [deviceId, channelId, streamId]) { + return new Promise((resolve, reject) => { + stop(deviceId, channelId, streamId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/recordPlan.js b/web/src/store/modules/recordPlan.js new file mode 100644 index 0000000..a20d2ee --- /dev/null +++ b/web/src/store/modules/recordPlan.js @@ -0,0 +1,80 @@ +import { addPlan, deletePlan, getPlan, linkPlan, queryChannelList, queryList, update } from '@/api/recordPlan' + +const actions = { + getPlan({ commit }, id) { + return new Promise((resolve, reject) => { + getPlan(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + addPlan({ commit }, params) { + return new Promise((resolve, reject) => { + addPlan(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + update({ commit }, params) { + return new Promise((resolve, reject) => { + update(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryList({ commit }, params) { + return new Promise((resolve, reject) => { + queryList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + deletePlan({ commit }, id) { + return new Promise((resolve, reject) => { + deletePlan(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryChannelList({ commit }, params) { + return new Promise((resolve, reject) => { + queryChannelList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + linkPlan({ commit }, data) { + return new Promise((resolve, reject) => { + linkPlan(data).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/region.js b/web/src/store/modules/region.js new file mode 100644 index 0000000..f28bbb3 --- /dev/null +++ b/web/src/store/modules/region.js @@ -0,0 +1,109 @@ +import { + getTreeList, + deleteRegion, + description, + addByCivilCode, + queryChildListInBase, + update, + add, + queryPath, queryTree +} from '@/api/region' + +const actions = { + getTreeList({ commit }, data) { + return new Promise((resolve, reject) => { + getTreeList(data).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + deleteRegion({ commit }, id) { + return new Promise((resolve, reject) => { + deleteRegion(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + description({ commit }, civilCode) { + return new Promise((resolve, reject) => { + description(civilCode).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + addByCivilCode({ commit }, civilCode) { + return new Promise((resolve, reject) => { + addByCivilCode(civilCode).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryChildListInBase({ commit }, parent) { + return new Promise((resolve, reject) => { + queryChildListInBase(parent).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + update({ commit }, formData) { + return new Promise((resolve, reject) => { + update(formData).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + add({ commit }, formData) { + return new Promise((resolve, reject) => { + add(formData).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryPath({ commit }, deviceId) { + return new Promise((resolve, reject) => { + queryPath(deviceId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryTree({ commit }, param) { + return new Promise((resolve, reject) => { + queryTree(param).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/role.js b/web/src/store/modules/role.js new file mode 100644 index 0000000..cddbf94 --- /dev/null +++ b/web/src/store/modules/role.js @@ -0,0 +1,20 @@ +import { getAll } from '@/api/role' + +const actions = { + getAll({ commit }) { + return new Promise((resolve, reject) => { + getAll().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/server.js b/web/src/store/modules/server.js new file mode 100644 index 0000000..2628ea2 --- /dev/null +++ b/web/src/store/modules/server.js @@ -0,0 +1,166 @@ +import { + checkMediaServer, + checkMediaServerRecord, deleteMediaServer, getMapConfig, getMediaInfo, + getMediaServer, + getMediaServerList, getMediaServerLoad, getModelList, + getOnlineMediaServerList, getResourceInfo, getSystemConfig, getSystemInfo, info, saveMediaServer +} from '@/api/server' + +const actions = { + getOnlineMediaServerList({ commit }) { + return new Promise((resolve, reject) => { + getOnlineMediaServerList().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getMediaServerList({ commit }) { + return new Promise((resolve, reject) => { + getMediaServerList().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getMediaServer({ commit }, id) { + return new Promise((resolve, reject) => { + getMediaServer(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + checkMediaServer({ commit }, params) { + return new Promise((resolve, reject) => { + checkMediaServer(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + checkMediaServerRecord({ commit }, params) { + return new Promise((resolve, reject) => { + checkMediaServerRecord(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + saveMediaServer({ commit }, formData) { + return new Promise((resolve, reject) => { + saveMediaServer(formData).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + deleteMediaServer({ commit }, id) { + return new Promise((resolve, reject) => { + deleteMediaServer(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getSystemConfig({ commit }) { + return new Promise((resolve, reject) => { + getSystemConfig().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getMediaInfo({ commit }, params) { + return new Promise((resolve, reject) => { + getMediaInfo(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getSystemInfo({ commit }) { + return new Promise((resolve, reject) => { + getSystemInfo().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getMediaServerLoad({ commit }) { + return new Promise((resolve, reject) => { + getMediaServerLoad().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getResourceInfo({ commit }) { + return new Promise((resolve, reject) => { + getResourceInfo().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + info({ commit }) { + return new Promise((resolve, reject) => { + info().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getMapConfig({ commit }) { + return new Promise((resolve, reject) => { + getMapConfig().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getModelList({ commit }) { + return new Promise((resolve, reject) => { + getModelList().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/settings.js b/web/src/store/modules/settings.js new file mode 100644 index 0000000..cf15e20 --- /dev/null +++ b/web/src/store/modules/settings.js @@ -0,0 +1,33 @@ +import defaultSettings from '@/settings' + +const { showSettings, fixedHeader, sidebarLogo, tagsViews } = defaultSettings + +const state = { + showSettings: showSettings, + fixedHeader: fixedHeader, + sidebarLogo: sidebarLogo, + tagsViews: tagsViews +} + +const mutations = { + CHANGE_SETTING: (state, { key, value }) => { + // eslint-disable-next-line no-prototype-builtins + if (state.hasOwnProperty(key)) { + state[key] = value + } + } +} + +const actions = { + changeSetting({ commit }, data) { + commit('CHANGE_SETTING', data) + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} + diff --git a/web/src/store/modules/streamProxy.js b/web/src/store/modules/streamProxy.js new file mode 100644 index 0000000..616ee03 --- /dev/null +++ b/web/src/store/modules/streamProxy.js @@ -0,0 +1,90 @@ +import { add, play, queryFfmpegCmdList, queryList, remove, save, stopPlay, update } from '@/api/streamProxy' + +const actions = { + queryFfmpegCmdList({ commit }, mediaServerId) { + return new Promise((resolve, reject) => { + queryFfmpegCmdList(mediaServerId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + save({ commit }, formData) { + return new Promise((resolve, reject) => { + save(formData).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + update({ commit }, formData) { + return new Promise((resolve, reject) => { + update(formData).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + add({ commit }, formData) { + return new Promise((resolve, reject) => { + add(formData).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryList({ commit }, params) { + return new Promise((resolve, reject) => { + queryList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + play({ commit }, id) { + return new Promise((resolve, reject) => { + play(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + stopPlay({ commit }, id) { + return new Promise((resolve, reject) => { + stopPlay(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + remove({ commit }, id) { + return new Promise((resolve, reject) => { + remove(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/streamPush.js b/web/src/store/modules/streamPush.js new file mode 100644 index 0000000..b379a81 --- /dev/null +++ b/web/src/store/modules/streamPush.js @@ -0,0 +1,90 @@ +import { saveToGb, add, update, queryList, play, remove, removeFormGb, batchRemove } from '@/api/streamPush' + +const actions = { + saveToGb({ commit }, formData) { + return new Promise((resolve, reject) => { + saveToGb(formData).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + add({ commit }, formData) { + return new Promise((resolve, reject) => { + add(formData).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + update({ commit }, formData) { + return new Promise((resolve, reject) => { + update(formData).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryList({ commit }, params) { + return new Promise((resolve, reject) => { + queryList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + play({ commit }, id) { + return new Promise((resolve, reject) => { + play(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + remove({ commit }, id) { + return new Promise((resolve, reject) => { + remove(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + removeFormGb({ commit }, formData) { + return new Promise((resolve, reject) => { + removeFormGb(formData).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + batchRemove({ commit }, ids) { + return new Promise((resolve, reject) => { + batchRemove(ids).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/store/modules/tagsView.js b/web/src/store/modules/tagsView.js new file mode 100644 index 0000000..57e7242 --- /dev/null +++ b/web/src/store/modules/tagsView.js @@ -0,0 +1,160 @@ +const state = { + visitedViews: [], + cachedViews: [] +} + +const mutations = { + ADD_VISITED_VIEW: (state, view) => { + if (state.visitedViews.some(v => v.path === view.path)) return + state.visitedViews.push( + Object.assign({}, view, { + title: view.meta.title || 'no-name' + }) + ) + }, + ADD_CACHED_VIEW: (state, view) => { + if (state.cachedViews.includes(view.name)) return + if (!view.meta.noCache) { + state.cachedViews.push(view.name) + } + }, + + DEL_VISITED_VIEW: (state, view) => { + for (const [i, v] of state.visitedViews.entries()) { + if (v.path === view.path) { + state.visitedViews.splice(i, 1) + break + } + } + }, + DEL_CACHED_VIEW: (state, view) => { + const index = state.cachedViews.indexOf(view.name) + index > -1 && state.cachedViews.splice(index, 1) + }, + + DEL_OTHERS_VISITED_VIEWS: (state, view) => { + state.visitedViews = state.visitedViews.filter(v => { + return v.meta.affix || v.path === view.path + }) + }, + DEL_OTHERS_CACHED_VIEWS: (state, view) => { + const index = state.cachedViews.indexOf(view.name) + if (index > -1) { + state.cachedViews = state.cachedViews.slice(index, index + 1) + } else { + // if index = -1, there is no cached tags + state.cachedViews = [] + } + }, + + DEL_ALL_VISITED_VIEWS: state => { + // keep affix tags + const affixTags = state.visitedViews.filter(tag => tag.meta.affix) + state.visitedViews = affixTags + }, + DEL_ALL_CACHED_VIEWS: state => { + state.cachedViews = [] + }, + + UPDATE_VISITED_VIEW: (state, view) => { + for (let v of state.visitedViews) { + if (v.path === view.path) { + v = Object.assign(v, view) + break + } + } + } +} + +const actions = { + addView({ dispatch }, view) { + dispatch('addVisitedView', view) + dispatch('addCachedView', view) + }, + addVisitedView({ commit }, view) { + commit('ADD_VISITED_VIEW', view) + }, + addCachedView({ commit }, view) { + commit('ADD_CACHED_VIEW', view) + }, + + delView({ dispatch, state }, view) { + return new Promise(resolve => { + dispatch('delVisitedView', view) + dispatch('delCachedView', view) + resolve({ + visitedViews: [...state.visitedViews], + cachedViews: [...state.cachedViews] + }) + }) + }, + delVisitedView({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_VISITED_VIEW', view) + resolve([...state.visitedViews]) + }) + }, + delCachedView({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_CACHED_VIEW', view) + resolve([...state.cachedViews]) + }) + }, + + delOthersViews({ dispatch, state }, view) { + return new Promise(resolve => { + dispatch('delOthersVisitedViews', view) + dispatch('delOthersCachedViews', view) + resolve({ + visitedViews: [...state.visitedViews], + cachedViews: [...state.cachedViews] + }) + }) + }, + delOthersVisitedViews({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_OTHERS_VISITED_VIEWS', view) + resolve([...state.visitedViews]) + }) + }, + delOthersCachedViews({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_OTHERS_CACHED_VIEWS', view) + resolve([...state.cachedViews]) + }) + }, + + delAllViews({ dispatch, state }, view) { + return new Promise(resolve => { + dispatch('delAllVisitedViews', view) + dispatch('delAllCachedViews', view) + resolve({ + visitedViews: [...state.visitedViews], + cachedViews: [...state.cachedViews] + }) + }) + }, + delAllVisitedViews({ commit, state }) { + return new Promise(resolve => { + commit('DEL_ALL_VISITED_VIEWS') + resolve([...state.visitedViews]) + }) + }, + delAllCachedViews({ commit, state }) { + return new Promise(resolve => { + commit('DEL_ALL_CACHED_VIEWS') + resolve([...state.cachedViews]) + }) + }, + + updateVisitedView({ commit }, view) { + commit('UPDATE_VISITED_VIEW', view) + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/web/src/store/modules/user.js b/web/src/store/modules/user.js new file mode 100644 index 0000000..3eb67af --- /dev/null +++ b/web/src/store/modules/user.js @@ -0,0 +1,188 @@ +import crypto from 'crypto' +import { + add, + changePassword, + changePasswordForAdmin, + changePushKey, + getUserInfo, + login, + logout, + queryList, + removeById +} from '@/api/user' +import { + getToken, + setToken, + setName, + removeToken, + removeName, + setServerId, + removeServerId +} from '@/utils/auth' +import { resetRouter } from '@/router' + +const getDefaultState = () => { + return { + token: getToken(), + name: '', + serverId: '', + showConfirmBoxForLoginLose: true + } +} + +const state = getDefaultState() + +const mutations = { + RESET_STATE: (state) => { + Object.assign(state, getDefaultState()) + }, + SET_TOKEN: (state, token) => { + state.token = token + }, + SET_NAME: (state, name) => { + state.name = name + }, + SET_SERVER_ID: (state, serverId) => { + state.serverId = serverId + }, + SET_CONFIRM_BOX: (state, status) => { + state.showConfirmBoxForLoginLose = status + } +} + +const actions = { + // user login + login({ commit }, userInfo) { + const { username, password } = userInfo + return new Promise((resolve, reject) => { + login({ + username: username.trim(), + password: crypto.createHash('md5').update(password, 'utf8').digest('hex') + }).then(response => { + const { data } = response + commit('SET_TOKEN', data.accessToken) + commit('SET_NAME', data.username) + commit('SET_SERVER_ID', data.serverId) + commit('SET_CONFIRM_BOX', true) + setToken(data.accessToken) + setName(data.username) + setServerId(data.serverId) + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + // user logout + logout({ commit, state }) { + return new Promise((resolve, reject) => { + logout(state.token).then(() => { + removeToken() + removeServerId() + removeName() + resetRouter() + commit('RESET_STATE') + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + + // remove token + resetToken({ commit }) { + return new Promise(resolve => { + removeToken() // must remove token first + commit('RESET_STATE') + resolve() + }) + }, + + getUserInfo({ commit }) { + return new Promise((resolve, reject) => { + getUserInfo().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + + changePushKey({ commit }, params) { + return new Promise((resolve, reject) => { + changePushKey(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + + queryList({ commit }, params) { + return new Promise((resolve, reject) => { + queryList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + + removeById({ commit }, id) { + return new Promise((resolve, reject) => { + removeById(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + + add({ commit }, params) { + return new Promise((resolve, reject) => { + add(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + + changePassword({ commit }, params) { + return new Promise((resolve, reject) => { + changePassword(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + + changePasswordForAdmin({ commit }, params) { + return new Promise((resolve, reject) => { + changePasswordForAdmin(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + closeConfirmBoxForLoginLose({ commit }) { + commit('SET_CONFIRM_BOX', false) + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} + diff --git a/web/src/store/modules/userApiKeys.js b/web/src/store/modules/userApiKeys.js new file mode 100644 index 0000000..4ed1a72 --- /dev/null +++ b/web/src/store/modules/userApiKeys.js @@ -0,0 +1,80 @@ +import { add, disable, enable, queryList, remark, remove, reset } from '@/api/userApiKey' + +const actions = { + remark({ commit }, params) { + return new Promise((resolve, reject) => { + remark(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryList({ commit }, params) { + return new Promise((resolve, reject) => { + queryList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + enable({ commit }, id) { + return new Promise((resolve, reject) => { + enable(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + disable({ commit }, id) { + return new Promise((resolve, reject) => { + disable(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + reset({ commit }, id) { + return new Promise((resolve, reject) => { + reset(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + remove({ commit }, id) { + return new Promise((resolve, reject) => { + remove(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + add({ commit }, params) { + return new Promise((resolve, reject) => { + add(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} + diff --git a/web/src/styles/element-ui.scss b/web/src/styles/element-ui.scss new file mode 100644 index 0000000..0062411 --- /dev/null +++ b/web/src/styles/element-ui.scss @@ -0,0 +1,49 @@ +// cover some element-ui styles + +.el-breadcrumb__inner, +.el-breadcrumb__inner a { + font-weight: 400 !important; +} + +.el-upload { + input[type="file"] { + display: none !important; + } +} + +.el-upload__input { + display: none; +} + + +// to fixed https://github.com/ElemeFE/element/issues/2461 +.el-dialog { + transform: none; + left: 0; + position: relative; + margin: 0 auto; +} + +// refine element ui upload +.upload-container { + .el-upload { + width: 100%; + + .el-upload-dragger { + width: 100%; + height: 200px; + } + } +} + +// dropdown +.el-dropdown-menu { + a { + display: block + } +} + +// to fix el-date-picker css style +.el-range-separator { + box-sizing: content-box; +} diff --git a/web/src/styles/iconfont.scss b/web/src/styles/iconfont.scss new file mode 100644 index 0000000..5f661da --- /dev/null +++ b/web/src/styles/iconfont.scss @@ -0,0 +1,2229 @@ +@font-face { + font-family: "iconfont"; /* Project id 1291092 */ + src: url('iconfont.woff2?t=1758784486763') format('woff2') +} + +.iconfont { + font-family: "iconfont" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-mti-duobianxingxuan:before { + content: "\e9e7"; +} + +.icon-mti-fengwotu:before { + content: "\e9e8"; +} + +.icon-mti-fangda:before { + content: "\e9e9"; +} + +.icon-mti-huizhi:before { + content: "\e9ea"; +} + +.icon-mti-guijihuifang:before { + content: "\e9eb"; +} + +.icon-mti-huodongguiji:before { + content: "\e9ed"; +} + +.icon-mti-juliceliang:before { + content: "\e9ee"; +} + +.icon-mti-jutai:before { + content: "\e9ef"; +} + +.icon-mti-kuangxuan:before { + content: "\e9f0"; +} + +.icon-mti-lukuang:before { + content: "\e9f1"; +} + +.icon-a-mti-qiehuanshijiaomti-sanwei:before { + content: "\e9f2"; +} + +.icon-mti-mianjiceliang:before { + content: "\e9f3"; +} + +.icon-mti-lukuang2:before { + content: "\e9f4"; +} + +.icon-mti-sandian:before { + content: "\e9f7"; +} + +.icon-mti-relitu:before { + content: "\e9f8"; +} + +.icon-mti-sanwei:before { + content: "\e9f9"; +} + +.icon-mti-quanxuan:before { + content: "\e9fa"; +} + +.icon-mti-suoxiao:before { + content: "\e9fb"; +} + +.icon-mti-tucengfenlei:before { + content: "\e9fc"; +} + +.icon-mti-tianjiadingweiquyu:before { + content: "\e9fd"; +} + +.icon-mti-tianjiadingwei:before { + content: "\e9fe"; +} + +.icon-mti-tujingdian:before { + content: "\e9ff"; +} + +.icon-mti-xinzengdian:before { + content: "\ea00"; +} + +.icon-mti-xianxuan:before { + content: "\ea01"; +} + +.icon-mti-xuanze:before { + content: "\ea02"; +} + +.icon-mti-xinzengxian:before { + content: "\ea03"; +} + +.icon-mti-yangshi:before { + content: "\ea04"; +} + +.icon-mti-yingyantu:before { + content: "\ea05"; +} + +.icon-mti-yunhangguiji:before { + content: "\ea06"; +} + +.icon-mti-ziyuanbukong:before { + content: "\ea08"; +} + +.icon-mti-zhongdianquyu:before { + content: "\ea09"; +} + +.icon-mti-zongheceliang:before { + content: "\ea0a"; +} + +.icon-mti-POIxinxi:before { + content: "\e9e2"; +} + +.icon-mti-celiang:before { + content: "\e9e3"; +} + +.icon-mti-dituqiehuan:before { + content: "\e9e4"; +} + +.icon-mti-diliweilan:before { + content: "\e9e5"; +} + +.icon-mti-erwei:before { + content: "\e9e6"; +} + +.icon-tuceng:before { + content: "\e7f2"; +} + +.icon-a-bofangqi1:before { + content: "\ec17"; +} + +.icon-sanjiaoxing:before { + content: "\e7f1"; +} + +.icon-icon_gps:before { + content: "\e7f0"; +} + +.icon-yidingdaoweizhuangtai:before { + content: "\e7ef"; +} + +.icon-gps:before { + content: "\e8b6"; +} + +.icon-tongdao:before { + content: "\e7ee"; +} + +.icon-xiazailiebiao:before { + content: "\e7ed"; +} + +.icon-zoom-in:before { + content: "\e7eb"; +} + +.icon-zoom-out:before { + content: "\e7ec"; +} + +.icon-bianjiao-suoxiao:before { + content: "\e8c8"; +} + +.icon-bianjiao-fangda:before { + content: "\e8c9"; +} + +.icon-guangquan-:before { + content: "\e7e9"; +} + +.icon-guangquan:before { + content: "\e7ea"; +} + +.icon-a-mti-1fenpingshi:before { + content: "\e7e5"; +} + +.icon-a-mti-4fenpingshi:before { + content: "\e7e6"; +} + +.icon-a-mti-6fenpingshi:before { + content: "\e7e7"; +} + +.icon-a-mti-9fenpingshi:before { + content: "\e7e8"; +} + +.icon-shexiangtou01:before { + content: "\e7e1"; +} + +.icon-Group-:before { + content: "\e7e2"; +} + +.icon-shexiangtou2:before { + content: "\e7e3"; +} + +.icon-shexiangtou3:before { + content: "\e7e4"; +} + +.icon-slider:before { + content: "\e7e0"; +} + +.icon-slider-right:before { + content: "\ea19"; +} + +.icon-list:before { + content: "\e7de"; +} + +.icon-tree:before { + content: "\e7df"; +} + +.icon-shipin:before { + content: "\e7db"; +} + +.icon-shipin1:before { + content: "\e7dc"; +} + +.icon-shipin2:before { + content: "\e7dd"; +} + +.icon-LC_icon_gps_fill:before { + content: "\e7da"; +} + +.icon-jiedianleizhukongzhongxin1:before { + content: "\e9d0"; +} + +.icon-jiedianleizhukongzhongxin2:before { + content: "\e9d1"; +} + +.icon-jiedianleilianjipingtai:before { + content: "\e9d3"; +} + +.icon-jiedianleiquyu:before { + content: "\e9d4"; +} + +.icon-shebeileigis:before { + content: "\e9ec"; +} + +.icon-shebeileibanqiu:before { + content: "\e9f5"; +} + +.icon-shebeileibanqiugis:before { + content: "\e9f6"; +} + +.icon-shebeileijiankongdian:before { + content: "\ea07"; +} + +.icon-shebeileiqiangjitongdao:before { + content: "\ea15"; +} + +.icon-shebeileiqiuji:before { + content: "\ea17"; +} + +.icon-shebeileiqiujigis:before { + content: "\ea18"; +} + +.icon-xitongxinxi:before { + content: "\e7d6"; +} + +.icon-gbaojings:before { + content: "\e7d7"; +} + +.icon-gjichus:before { + content: "\e7d8"; +} + +.icon-gxunjians:before { + content: "\e7d9"; +} + +.icon-ziyuan:before { + content: "\e7d5"; +} + +.icon-shexiangtou1:before { + content: "\e7d4"; +} + +.icon-wxbzhuye:before { + content: "\e7d1"; +} + +.icon-mulu:before { + content: "\e7d2"; +} + +.icon-zhibo:before { + content: "\e8c1"; +} + +.icon-shexiangtou:before { + content: "\e7d3"; +} + +.icon-suoxiao:before { + content: "\e79a"; +} + +.icon-shanchu3:before { + content: "\e79b"; +} + +.icon-chehui:before { + content: "\e79c"; +} + +.icon-wenben:before { + content: "\e79d"; +} + +.icon-zhongzuo:before { + content: "\e79e"; +} + +.icon-jianqie:before { + content: "\e79f"; +} + +.icon-fangda:before { + content: "\e7a0"; +} + +.icon-fangdazhanshi:before { + content: "\e7a1"; +} + +.icon-qianjin:before { + content: "\e7a2"; +} + +.icon-houtui:before { + content: "\e7a3"; +} + +.icon-diyigeshipin:before { + content: "\e7a4"; +} + +.icon-kuaijin:before { + content: "\e7a5"; +} + +.icon-kaishi:before { + content: "\e7a7"; +} + +.icon-zuihouyigeshipin:before { + content: "\e7a8"; +} + +.icon-zanting:before { + content: "\e7a9"; +} + +.icon-zhankai:before { + content: "\e7aa"; +} + +.icon-bendisucai:before { + content: "\e7ab"; +} + +.icon-luzhi:before { + content: "\e7ac"; +} + +.icon-ossziyuan:before { + content: "\e7ad"; +} + +.icon-chuangjianzhinengfenxirenwu:before { + content: "\e7ae"; +} + +.icon-sousuo3:before { + content: "\e7af"; +} + +.icon-gengduo:before { + content: "\e7b0"; +} + +.icon-tianjia:before { + content: "\e7b1"; +} + +.icon-xiazai:before { + content: "\e7b2"; +} + +.icon-biaojibeifen:before { + content: "\e7b3"; +} + +.icon-bendisucaibeifen:before { + content: "\e7b4"; +} + +.icon-luzhibeifen:before { + content: "\e7b5"; +} + +.icon-ossziyuanbeifen:before { + content: "\e7b6"; +} + +.icon-bianji3:before { + content: "\e7b7"; +} + +.icon-cuti:before { + content: "\e7b8"; +} + +.icon-xieti:before { + content: "\e7b9"; +} + +.icon-xiahuaxian:before { + content: "\e7ba"; +} + +.icon-wuxiaoguo:before { + content: "\e7bb"; +} + +.icon-sousuo4:before { + content: "\e7bc"; +} + +.icon-gouwuche:before { + content: "\e7bd"; +} + +.icon-shuaxin2:before { + content: "\e7be"; +} + +.icon-xiaoxi:before { + content: "\e7bf"; +} + +.icon-wushouquan:before { + content: "\e7c0"; +} + +.icon-tishi2:before { + content: "\e7c1"; +} + +.icon-tishi1:before { + content: "\e7c2"; +} + +.icon-shouquanchenggong:before { + content: "\e7c3"; +} + +.icon-sousuo5:before { + content: "\e7c4"; +} + +.icon-shuaxin3:before { + content: "\e7c5"; +} + +.icon-xiazai1:before { + content: "\e7c6"; +} + +.icon-shangchuan:before { + content: "\e7c7"; +} + +.icon-guanbi:before { + content: "\e7c8"; +} + +.icon-wangye-loading:before { + content: "\e7c9"; +} + +.icon-bianzubeifen3:before { + content: "\e7ca"; +} + +.icon-xingzhuangbeifen:before { + content: "\e7cb"; +} + +.icon-bianzubeifen:before { + content: "\e7cc"; +} + +.icon-zhuanchang:before { + content: "\e7cd"; +} + +.icon-meizi:before { + content: "\e7ce"; +} + +.icon-daimabeifen:before { + content: "\e7cf"; +} + +.icon-suoxiao1:before { + content: "\e7d0"; +} + +.icon-ai19:before { + content: "\e799"; +} + +.icon-online:before { + content: "\e600"; +} + +.icon-xiangqing2:before { + content: "\e798"; +} + +.icon-record:before { + content: "\e7a6"; +} + +.icon-audio-mute:before { + content: "\e792"; +} + +.icon-audio-high:before { + content: "\e793"; +} + +.icon-record1:before { + content: "\e7f8"; +} + +.icon-audio-line:before { + content: "\e794"; +} + +.icon-record2:before { + content: "\e795"; +} + +.icon-audio-fill:before { + content: "\e796"; +} + +.icon-PTZ:before { + content: "\e797"; +} + +.icon-camera1196054easyiconnet:before { + content: "\e791"; +} + +.icon-weibiaoti10:before { + content: "\e78f"; +} + +.icon-weibiaoti11:before { + content: "\e790"; +} + +.icon-page-next1:before { + content: "\e69c"; +} + +.icon-page-last1:before { + content: "\e69d"; +} + +.icon-ptz-down1:before { + content: "\e69e"; +} + +.icon-file-search1:before { + content: "\e69f"; +} + +.icon-page-first1:before { + content: "\e6a0"; +} + +.icon-fork1:before { + content: "\e6a1"; +} + +.icon-ptz-middle1:before { + content: "\e6a2"; +} + +.icon-ptz-upright1:before { + content: "\e6a3"; +} + +.icon-ptz-downleft1:before { + content: "\e6a4"; +} + +.icon-window-restore1:before { + content: "\e6a5"; +} + +.icon-plus1:before { + content: "\e6a6"; +} + +.icon-ptz-right1:before { + content: "\e6a7"; +} + +.icon-stop:before { + content: "\e6a8"; +} + +.icon-refresh1:before { + content: "\e6a9"; +} + +.icon-tool-polyline1:before { + content: "\e6aa"; +} + +.icon-tool-point1:before { + content: "\e6ab"; +} + +.icon-minus1:before { + content: "\e6ac"; +} + +.icon-ptz-wiper1:before { + content: "\e6ad"; +} + +.icon-tool-select1:before { + content: "\e6ae"; +} + +.icon-tool-polygon1:before { + content: "\e6af"; +} + +.icon-settings1:before { + content: "\e6b0"; +} + +.icon-search1:before { + content: "\e6b1"; +} + +.icon-ir-vis1:before { + content: "\e6b2"; +} + +.icon-ptz-light1:before { + content: "\e6b3"; +} + +.icon-ptz-up1:before { + content: "\e6b4"; +} + +.icon-ptz-upleft1:before { + content: "\e6b5"; +} + +.icon-temp-stream1:before { + content: "\e6b6"; +} + +.icon-tool-mouse1:before { + content: "\e6b7"; +} + +.icon-zhongyingwenyingwen-01:before { + content: "\e6b8"; +} + +.icon-zhongyingwenyingwen02-01:before { + content: "\e6b9"; +} + +.icon-crop2:before { + content: "\e6ba"; +} + +.icon-expander-down2:before { + content: "\e6bb"; +} + +.icon-window-restore2:before { + content: "\e6bc"; +} + +.icon-file-jpg2:before { + content: "\e6bd"; +} + +.icon-asterisk3:before { + content: "\e6be"; +} + +.icon-ffc2:before { + content: "\e6bf"; +} + +.icon-file-record2:before { + content: "\e6c0"; +} + +.icon-file-stream2:before { + content: "\e6c1"; +} + +.icon-fork2:before { + content: "\e6c2"; +} + +.icon-file-mp42:before { + content: "\e6c3"; +} + +.icon-ir-vis2:before { + content: "\e6c4"; +} + +.icon-file-search2:before { + content: "\e6c5"; +} + +.icon-pause:before { + content: "\e6c6"; +} + +.icon-play1:before { + content: "\e6c7"; +} + +.icon-page-previous2:before { + content: "\e6c8"; +} + +.icon-page-next2:before { + content: "\e6c9"; +} + +.icon-minus2:before { + content: "\e6ca"; +} + +.icon-page-last2:before { + content: "\e6cb"; +} + +.icon-page-first2:before { + content: "\e6cc"; +} + +.icon-ptz-downleft2:before { + content: "\e6cd"; +} + +.icon-ptz-downright2:before { + content: "\e6ce"; +} + +.icon-ptz-middle2:before { + content: "\e6cf"; +} + +.icon-ptz-down2:before { + content: "\e6d0"; +} + +.icon-plus2:before { + content: "\e6d1"; +} + +.icon-ptz-left2:before { + content: "\e6d2"; +} + +.icon-ptz-up2:before { + content: "\e6d3"; +} + +.icon-ptz-right2:before { + content: "\e6d4"; +} + +.icon-ptz-light2:before { + content: "\e6d5"; +} + +.icon-ptz-wiper2:before { + content: "\e6d6"; +} + +.icon-ptz-upright2:before { + content: "\e6d7"; +} + +.icon-search2:before { + content: "\e6d8"; +} + +.icon-refresh2:before { + content: "\e6d9"; +} + +.icon-ptz-upleft2:before { + content: "\e6da"; +} + +.icon-stop1:before { + content: "\e6db"; +} + +.icon-tool-mouse2:before { + content: "\e6dc"; +} + +.icon-settings2:before { + content: "\e6dd"; +} + +.icon-tool-polygon2:before { + content: "\e6de"; +} + +.icon-tool-point2:before { + content: "\e6df"; +} + +.icon-temp-stream2:before { + content: "\e6e0"; +} + +.icon-tool-polyline2:before { + content: "\e6e1"; +} + +.icon-window-maximize2:before { + content: "\e6e2"; +} + +.icon-window-minimize2:before { + content: "\e6e3"; +} + +.icon-tool-select2:before { + content: "\e6e4"; +} + +.icon-video-stream2:before { + content: "\e6e5"; +} + +.icon-bianji1:before { + content: "\e6e6"; +} + +.icon-caidanzhankai1:before { + content: "\e6e7"; +} + +.icon-cha11:before { + content: "\e6e8"; +} + +.icon-caidanshouqi1:before { + content: "\e6e9"; +} + +.icon-zhongyingwen2zhongwen1:before { + content: "\e6ea"; +} + +.icon-bofang011:before { + content: "\e6eb"; +} + +.icon-zuo:before { + content: "\e6ec"; +} + +.icon-baojing1:before { + content: "\e6ed"; +} + +.icon-fuxuankuang-true1:before { + content: "\e6ee"; +} + +.icon-bofang2:before { + content: "\e6ef"; +} + +.icon-baojingshezhi1:before { + content: "\e6f0"; +} + +.icon-jiahao2:before { + content: "\e6f1"; +} + +.icon-huifangxuanzhong1:before { + content: "\e6f2"; +} + +.icon-cewen1:before { + content: "\e6f3"; +} + +.icon-baojingjilu2:before { + content: "\e6f4"; +} + +.icon-danxuan1:before { + content: "\e6f5"; +} + +.icon-pingmufenge1:before { + content: "\e6f6"; +} + +.icon-luxiangguanli1:before { + content: "\e6f7"; +} + +.icon-goukuang:before { + content: "\e6f8"; +} + +.icon-shanchu11:before { + content: "\e6f9"; +} + +.icon-cha02:before { + content: "\e6fa"; +} + +.icon-huifang1:before { + content: "\e6fb"; +} + +.icon-rili1:before { + content: "\e6fc"; +} + +.icon-quanping1:before { + content: "\e6fd"; +} + +.icon-jianhao1:before { + content: "\e6fe"; +} + +.icon-shijian1:before { + content: "\e6ff"; +} + +.icon-shishiyulanxuanzhong1:before { + content: "\e700"; +} + +.icon-shouji1:before { + content: "\e701"; +} + +.icon-shouyexuanzhong1:before { + content: "\e702"; +} + +.icon-luxiang01:before { + content: "\e703"; +} + +.icon-shishiyulan:before { + content: "\e704"; +} + +.icon-quxiao:before { + content: "\e601"; +} + +.icon-sousuo1:before { + content: "\e705"; +} + +.icon-file-record:before { + content: "\e602"; +} + +.icon-shebeiguanli1:before { + content: "\e706"; +} + +.icon-play:before { + content: "\e603"; +} + +.icon-suo1:before { + content: "\e707"; +} + +.icon-file-stream:before { + content: "\e604"; +} + +.icon-tuichudenglu1:before { + content: "\e708"; +} + +.icon-ptz-middle:before { + content: "\e606"; +} + +.icon-wenhao1:before { + content: "\e709"; +} + +.icon-minus:before { + content: "\e607"; +} + +.icon-shezhixuanzhong:before { + content: "\e70a"; +} + +.icon-fork:before { + content: "\e608"; +} + +.icon-shezhiweixuanzhong1:before { + content: "\e70b"; +} + +.icon-ptz-up:before { + content: "\e609"; +} + +.icon-shuju2:before { + content: "\e70c"; +} + +.icon-file-jpg:before { + content: "\e60a"; +} + +.icon-xiazai011:before { + content: "\e70d"; +} + +.icon-ptz-left:before { + content: "\e60b"; +} + +.icon-xiala11:before { + content: "\e70e"; +} + +.icon-ptz-down:before { + content: "\e60c"; +} + +.icon-shuaxin:before { + content: "\e70f"; +} + +.icon-file-search:before { + content: "\e60d"; +} + +.icon-pingmufenge01:before { + content: "\e710"; +} + +.icon-crop:before { + content: "\e60e"; +} + +.icon-yonghu1:before { + content: "\e711"; +} + +.icon-asterisk:before { + content: "\e60f"; +} + +.icon-wenhao01:before { + content: "\e712"; +} + +.icon-expander-down:before { + content: "\e610"; +} + +.icon-you:before { + content: "\e713"; +} + +.icon-ptz-right:before { + content: "\e611"; +} + +.icon-shujuxuanzhong1:before { + content: "\e714"; +} + +.icon-ptz-wiper:before { + content: "\e612"; +} + +.icon-kuangxuan1:before { + content: "\e715"; +} + +.icon-ir-vis:before { + content: "\e613"; +} + +.icon-yonghuguanli1:before { + content: "\e716"; +} + +.icon-ptz-upleft:before { + content: "\e614"; +} + +.icon-zhongyingwenyingwen:before { + content: "\e717"; +} + +.icon-ptz-downright:before { + content: "\e615"; +} + +.icon-xiala2:before { + content: "\e718"; +} + +.icon-search:before { + content: "\e616"; +} + +.icon-luxiang:before { + content: "\e719"; +} + +.icon-ptz-upright:before { + content: "\e617"; +} + +.icon-zanting2:before { + content: "\e71a"; +} + +.icon-ptz-downleft:before { + content: "\e618"; +} + +.icon-kefu:before { + content: "\e71b"; +} + +.icon-tool-point:before { + content: "\e619"; +} + +.icon-jiqiren:before { + content: "\e71c"; +} + +.icon-ptz-light:before { + content: "\e61a"; +} + +.icon-huanliuzhan:before { + content: "\e71d"; +} + +.icon-tool-polyline:before { + content: "\e61b"; +} + +.icon-shouji2:before { + content: "\e71e"; +} + +.icon-file-mp4:before { + content: "\e61c"; +} + +.icon-cangku:before { + content: "\e71f"; +} + +.icon-window-maximize:before { + content: "\e61d"; +} + +.icon-shuaxin11:before { + content: "\e720"; +} + +.icon-page-next:before { + content: "\e61e"; +} + +.icon-weixiu:before { + content: "\e721"; +} + +.icon-ffc:before { + content: "\e61f"; +} + +.icon-biandianzhan:before { + content: "\e722"; +} + +.icon-tool-mouse:before { + content: "\e620"; +} + +.icon-youxiang:before { + content: "\e723"; +} + +.icon-settings:before { + content: "\e621"; +} + +.icon-qq:before { + content: "\e724"; +} + +.icon-page-last:before { + content: "\e622"; +} + +.icon-dianhua01:before { + content: "\e725"; +} + +.icon-window-restore:before { + content: "\e624"; +} + +.icon-fasongyoujian:before { + content: "\e726"; +} + +.icon-tool-select:before { + content: "\e625"; +} + +.icon-gaotieyunhangcopy:before { + content: "\e727"; +} + +.icon-video-stream:before { + content: "\e627"; +} + +.icon-dizhi:before { + content: "\e728"; +} + +.icon-page-first:before { + content: "\e628"; +} + +.icon-anfangbaojingmian:before { + content: "\e729"; +} + +.icon-page-previous:before { + content: "\e629"; +} + +.icon-piliangcaozuo1:before { + content: "\e72a"; +} + +.icon-refresh:before { + content: "\e62a"; +} + +.icon-qiyeguanli1:before { + content: "\e72b"; +} + +.icon-temp-stream:before { + content: "\e62b"; +} + +.icon-luxiangguanli2:before { + content: "\e72c"; +} + +.icon-tool-polygon:before { + content: "\e62c"; +} + +.icon-quanxianguanli1:before { + content: "\e72d"; +} + +.icon-window-minimize:before { + content: "\e62d"; +} + +.icon-shezhi1:before { + content: "\e72e"; +} + +.icon-plus:before { + content: "\e62e"; +} + +.icon-shishi1:before { + content: "\e72f"; +} + +.icon-qiyeguanli:before { + content: "\e62f"; +} + +.icon-shujuquanxian1:before { + content: "\e730"; +} + +.icon-quanxianguanli:before { + content: "\e630"; +} + +.icon-shishiyulanxuanzhong2:before { + content: "\e731"; +} + +.icon-shujuquanxian:before { + content: "\e631"; +} + +.icon-renzheng:before { + content: "\e732"; +} + +.icon--_baojinglianxiren:before { + content: "\e632"; +} + +.icon-shuju3:before { + content: "\e733"; +} + +.icon-yuechi:before { + content: "\e633"; +} + +.icon-shouye1:before { + content: "\e734"; +} + +.icon-xitongguanli:before { + content: "\e634"; +} + +.icon-zuzhi1:before { + content: "\e735"; +} + +.icon-zuzhi:before { + content: "\e635"; +} + +.icon-zuzhiguanli1:before { + content: "\e736"; +} + +.icon-renzheng6:before { + content: "\e636"; +} + +.icon-xitongguanli1:before { + content: "\e737"; +} + +.icon-yonghuguanli01:before { + content: "\e637"; +} + +.icon-yuechi1:before { + content: "\e738"; +} + +.icon-baojingmoban:before { + content: "\e638"; +} + +.icon-baojinglianxiren:before { + content: "\e739"; +} + +.icon-zuzhiguanli:before { + content: "\e639"; +} + +.icon-baojingjilu3:before { + content: "\e73a"; +} + +.icon-yonghuguanli:before { + content: "\e63a"; +} + +.icon-huifangxuanzhong2:before { + content: "\e73b"; +} + +.icon-bumenguanli:before { + content: "\e63b"; +} + +.icon-caiwu1:before { + content: "\e73c"; +} + +.icon-shishi:before { + content: "\e63c"; +} + +.icon-baojingguize1:before { + content: "\e73d"; +} + +.icon-baojing:before { + content: "\e63d"; +} + +.icon-bumenguanli1:before { + content: "\e73e"; +} + +.icon-shezhi:before { + content: "\e63e"; +} + +.icon-baojing2:before { + content: "\e73f"; +} + +.icon-huifangxuanzhong:before { + content: "\e63f"; +} + +.icon-yonghuguanli2:before { + content: "\e740"; +} + +.icon-luxiangguanli:before { + content: "\e640"; +} + +.icon-huifang2:before { + content: "\e741"; +} + +.icon-huifang:before { + content: "\e642"; +} + +.icon-baojingmoban1:before { + content: "\e742"; +} + +.icon-shouye:before { + content: "\e643"; +} + +.icon-dingdanxiangqing1:before { + content: "\e743"; +} + +.icon-shishiyulanxuanzhong:before { + content: "\e644"; +} + +.icon-fapiaoguanli1:before { + content: "\e744"; +} + +.icon-caiwu:before { + content: "\e645"; +} + +.icon-shiyonggaikuang1:before { + content: "\e745"; +} + +.icon-baojingjilu:before { + content: "\e646"; +} + +.icon-zengzhifuwu1:before { + content: "\e746"; +} + +.icon-baojingguize:before { + content: "\e647"; +} + +.icon-yiguanzhu:before { + content: "\e747"; +} + +.icon-shuju:before { + content: "\e648"; +} + +.icon-baojingtuisongshezhi1:before { + content: "\e748"; +} + +.icon-piliangcaozuo:before { + content: "\e649"; +} + +.icon-quxiao1:before { + content: "\e749"; +} + +.icon-suo:before { + content: "\e64a"; +} + +.icon-xiangqing1:before { + content: "\e74a"; +} + +.icon-yonghu:before { + content: "\e64b"; +} + +.icon-xufei1:before { + content: "\e74b"; +} + +.icon-shouji:before { + content: "\e64c"; +} + +.icon-zhifu1:before { + content: "\e74c"; +} + +.icon-tianjiadian:before { + content: "\e64d"; +} + +.icon-kuang:before { + content: "\e74d"; +} + +.icon-tianjiaxian:before { + content: "\e64e"; +} + +.icon-shouzhimingxi:before { + content: "\e74e"; +} + +.icon-tianjiaxuanqu:before { + content: "\e64f"; +} + +.icon-shouzhimingxi1:before { + content: "\e74f"; +} + +.icon-xuanzeduixiang:before { + content: "\e650"; +} + +.icon-daochu:before { + content: "\e750"; +} + +.icon-baojing01:before { + content: "\e651"; +} + +.icon-daochu1:before { + content: "\e751"; +} + +.icon-baojingjilu1:before { + content: "\e652"; +} + +.icon-daping:before { + content: "\e752"; +} + +.icon-baojingshezhi:before { + content: "\e653"; +} + +.icon-shaixuan:before { + content: "\e753"; +} + +.icon-cewen:before { + content: "\e654"; +} + +.icon-zhifu2:before { + content: "\e754"; +} + +.icon-tuichudenglu:before { + content: "\e655"; +} + +.icon-shaixuan1:before { + content: "\e755"; +} + +.icon-shezhiweixuanzhong:before { + content: "\e656"; +} + +.icon-zhifu3:before { + content: "\e756"; +} + +.icon-shezhixuanzhong1:before { + content: "\e657"; +} + +.icon-xia:before { + content: "\e757"; +} + +.icon-shouyexuanzhong:before { + content: "\e658"; +} + +.icon-xia1:before { + content: "\e758"; +} + +.icon-shujuxuanzhong:before { + content: "\e659"; +} + +.icon-yanzhengma:before { + content: "\e759"; +} + +.icon-shuju1:before { + content: "\e65a"; +} + +.icon-tongxunlu:before { + content: "\e75a"; +} + +.icon-bianji:before { + content: "\e65b"; +} + +.icon-yanzhengma1:before { + content: "\e75b"; +} + +.icon-rili:before { + content: "\e65c"; +} + +.icon-tongxunlu1:before { + content: "\e75c"; +} + +.icon-shanchu:before { + content: "\e65d"; +} + +.icon-yingyongbangding:before { + content: "\e75d"; +} + +.icon-jiahao:before { + content: "\e65e"; +} + +.icon-yingyongbangding1:before { + content: "\e75e"; +} + +.icon-wenhao:before { + content: "\e65f"; +} + +.icon-yingyongbangding2:before { + content: "\e75f"; +} + +.icon-zhongyingwen:before { + content: "\e660"; +} + +.icon-dapingzhanshi:before { + content: "\e760"; +} + +.icon-kuangxuan:before { + content: "\e661"; +} + +.icon-jiankong:before { + content: "\e761"; +} + +.icon-cha1:before { + content: "\e662"; +} + +.icon-touxiang:before { + content: "\e762"; +} + +.icon-bofang01:before { + content: "\e663"; +} + +.icon-lou:before { + content: "\e763"; +} + +.icon-caidanzhankai:before { + content: "\e664"; +} + +.icon-jiankong1:before { + content: "\e764"; +} + +.icon-caidanshouqi:before { + content: "\e665"; +} + +.icon-lou1:before { + content: "\e765"; +} + +.icon-danxuan:before { + content: "\e666"; +} + +.icon-dapingzhanshi1:before { + content: "\e766"; +} + +.icon-fuxuankuangxuanzhong:before { + content: "\e667"; +} + +.icon-touxiang1:before { + content: "\e767"; +} + +.icon-fuxuankuang-true:before { + content: "\e668"; +} + +.icon-shebei:before { + content: "\e768"; +} + +.icon-jianhao:before { + content: "\e669"; +} + +.icon-shebeii:before { + content: "\e769"; +} + +.icon-shanchu1:before { + content: "\e66a"; +} + +.icon-bianji11:before { + content: "\e76a"; +} + +.icon-shijian:before { + content: "\e66b"; +} + +.icon-jilu:before { + content: "\e76b"; +} + +.icon-jiahao1:before { + content: "\e66c"; +} + +.icon-yun:before { + content: "\e76c"; +} + +.icon-sousuo:before { + content: "\e66d"; +} + +.icon-baojing3:before { + content: "\e76d"; +} + +.icon-zhongyingwen2zhongwen:before { + content: "\e66e"; +} + +.icon-zhinengyangan:before { + content: "\e76e"; +} + +.icon-xiala:before { + content: "\e66f"; +} + +.icon-yongdiananquan:before { + content: "\e76f"; +} + +.icon-xiala1:before { + content: "\e670"; +} + +.icon-zhinengmensuo:before { + content: "\e770"; +} + +.icon-xiazai01:before { + content: "\e671"; +} + +.icon-xiaokongyujing:before { + content: "\e771"; +} + +.icon-pingmufenge02:before { + content: "\e672"; +} + +.icon-zhinengdianbiao:before { + content: "\e772"; +} + +.icon-shezhi01:before { + content: "\e673"; +} + +.icon-zhinengshuibiao:before { + content: "\e773"; +} + +.icon-zuixiaohuaxi:before { + content: "\e674"; +} + +.icon-shuiyajiance01:before { + content: "\e774"; +} + +.icon-zuidahuaxi:before { + content: "\e675"; +} + +.icon-zhinengzhaoming:before { + content: "\e775"; +} + +.icon-huifuxi:before { + content: "\e676"; +} + +.icon-zhinengmenjin:before { + content: "\e776"; +} + +.icon-guanbixi:before { + content: "\e677"; +} + +.icon-tingchechang:before { + content: "\e777"; +} + +.icon-baocunJPG:before { + content: "\e678"; +} + +.icon-xiala3:before { + content: "\e778"; +} + +.icon-quxian:before { + content: "\e679"; +} + +.icon-zhinengkongtiao:before { + content: "\e779"; +} + +.icon-tingzhiyulan:before { + content: "\e67a"; +} + +.icon-sousuo2:before { + content: "\e77a"; +} + +.icon-wenduliuluzhi:before { + content: "\e67b"; +} + +.icon-shang1:before { + content: "\e77b"; +} + +.icon-shuaxin1:before { + content: "\e67c"; +} + +.icon-1_jingdianchuwuweixuanzhong:before { + content: "\e77c"; +} + +.icon-shangjiantou:before { + content: "\e67d"; +} + +.icon-dianti:before { + content: "\e77d"; +} + +.icon-shang:before { + content: "\e67e"; +} + +.icon-zhuangtai:before { + content: "\e77e"; +} + +.icon-zixun:before { + content: "\e67f"; +} + +.icon-keshi:before { + content: "\e77f"; +} + +.icon-youxiang01:before { + content: "\e680"; +} + +.icon-chongzhijilu:before { + content: "\e780"; +} + +.icon-QQ:before { + content: "\e681"; +} + +.icon-jingshi:before { + content: "\e781"; +} + +.icon-dianhua:before { + content: "\e682"; +} + +.icon-bianji2:before { + content: "\e782"; +} + +.icon-pingmufenge:before { + content: "\e683"; +} + +.icon-fuzhi:before { + content: "\e783"; +} + +.icon-gou:before { + content: "\e684"; +} + +.icon-guanyu:before { + content: "\e784"; +} + +.icon-dingdanxiangqing:before { + content: "\e685"; +} + +.icon-shishiyulan-01:before { + content: "\e785"; +} + +.icon-shiyonggaikuang:before { + content: "\e686"; +} + +.icon-shujuchakan:before { + content: "\e786"; +} + +.icon-fapiaoguanli:before { + content: "\e687"; +} + +.icon-shanchu2:before { + content: "\e787"; +} + +.icon-xiangqing:before { + content: "\e688"; +} + +.icon-xitongpeizhi:before { + content: "\e788"; +} + +.icon-baojingtuisongshezhi:before { + content: "\e689"; +} + +.icon-tezhengwendu:before { + content: "\e789"; +} + +.icon-zhifu:before { + content: "\e68a"; +} + +.icon-quanzhenwendu:before { + content: "\e78a"; +} + +.icon-zengzhifuwu:before { + content: "\e68b"; +} + +.icon-fenxiang:before { + content: "\e78b"; +} + +.icon-xufei:before { + content: "\e68c"; +} + +.icon-fenxiang01:before { + content: "\e78c"; +} + +.icon-asterisk1:before { + content: "\e68d"; +} + +.icon-wenhao2:before { + content: "\e78d"; +} + +.icon-window-maximize1:before { + content: "\e68e"; +} + +.icon-dian:before { + content: "\e78e"; +} + +.icon-crop1:before { + content: "\e68f"; +} + +.icon-asterisk2:before { + content: "\e690"; +} + +.icon-file-record1:before { + content: "\e691"; +} + +.icon-ffc1:before { + content: "\e692"; +} + +.icon-file-mp41:before { + content: "\e693"; +} + +.icon-window-minimize1:before { + content: "\e694"; +} + +.icon-ptz-downright1:before { + content: "\e695"; +} + +.icon-video-stream1:before { + content: "\e696"; +} + +.icon-file-jpg1:before { + content: "\e697"; +} + +.icon-file-stream1:before { + content: "\e698"; +} + +.icon-page-previous1:before { + content: "\e699"; +} + +.icon-expander-down1:before { + content: "\e69a"; +} + +.icon-ptz-left1:before { + content: "\e69b"; +} + +.icon-yinpinwenjian1:before { + content: "\e623"; +} + +.icon-yinpinwenjian2:before { + content: "\e626"; +} + +.icon-xiazaiyinpinwenjian:before { + content: "\e605"; +} + +.icon-yinpinwenjian:before { + content: "\e641"; +} + diff --git a/web/src/styles/iconfont.woff2 b/web/src/styles/iconfont.woff2 new file mode 100644 index 0000000..a64c517 Binary files /dev/null and b/web/src/styles/iconfont.woff2 differ diff --git a/web/src/styles/index.scss b/web/src/styles/index.scss new file mode 100644 index 0000000..d58f2d3 --- /dev/null +++ b/web/src/styles/index.scss @@ -0,0 +1,66 @@ +@import './variables.scss'; +@import './mixin.scss'; +@import './transition.scss'; +@import './element-ui.scss'; +@import './sidebar.scss'; +@import './iconfont.scss'; + +body { + height: 100%; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; +} + +label { + font-weight: 700; +} + +html { + height: 100%; + box-sizing: border-box; +} + +#app { + height: 100%; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +a:focus, +a:active { + outline: none; +} + +a, +a:focus, +a:hover { + cursor: pointer; + color: inherit; + text-decoration: none; +} + +div:focus { + outline: none; +} + +.clearfix { + &:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; + } +} + +// main-container global css +.app-container { + padding: 20px; +} diff --git a/web/src/styles/mixin.scss b/web/src/styles/mixin.scss new file mode 100644 index 0000000..36b74bb --- /dev/null +++ b/web/src/styles/mixin.scss @@ -0,0 +1,28 @@ +@mixin clearfix { + &:after { + content: ""; + display: table; + clear: both; + } +} + +@mixin scrollBar { + &::-webkit-scrollbar-track-piece { + background: #d3dce6; + } + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #99a9bf; + border-radius: 20px; + } +} + +@mixin relative { + position: relative; + width: 100%; + height: 100%; +} diff --git a/web/src/styles/sidebar.scss b/web/src/styles/sidebar.scss new file mode 100644 index 0000000..94760cc --- /dev/null +++ b/web/src/styles/sidebar.scss @@ -0,0 +1,226 @@ +#app { + + .main-container { + min-height: 100%; + transition: margin-left .28s; + margin-left: $sideBarWidth; + position: relative; + } + + .sidebar-container { + transition: width 0.28s; + width: $sideBarWidth !important; + background-color: $menuBg; + height: 100%; + position: fixed; + font-size: 0px; + top: 0; + bottom: 0; + left: 0; + z-index: 1001; + overflow: hidden; + + // reset element-ui css + .horizontal-collapse-transition { + transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; + } + + .scrollbar-wrapper { + overflow-x: hidden !important; + } + + .el-scrollbar__bar.is-vertical { + right: 0px; + } + + .el-scrollbar { + height: 100%; + } + + &.has-logo { + .el-scrollbar { + height: calc(100% - 50px); + } + } + + .is-horizontal { + display: none; + } + + a { + display: inline-block; + width: 100%; + overflow: hidden; + } + + .svg-icon { + margin-right: 16px; + } + + .sub-el-icon { + margin-right: 12px; + margin-left: -2px; + } + + .el-menu { + border: none; + height: 100%; + width: 100% !important; + } + + // menu hover + .submenu-title-noDropdown, + .el-submenu__title { + &:hover { + background-color: $menuHover !important; + } + } + + .is-active>.el-submenu__title { + color: $subMenuActiveText !important; + } + + & .nest-menu .el-submenu>.el-submenu__title, + & .el-submenu .el-menu-item { + min-width: $sideBarWidth !important; + background-color: $subMenuBg !important; + + &:hover { + background-color: $subMenuHover !important; + } + } + } + + .hideSidebar { + .sidebar-container { + width: 54px !important; + } + + .main-container { + margin-left: 54px; + } + + .submenu-title-noDropdown { + padding: 0 !important; + position: relative; + + .el-tooltip { + padding: 0 !important; + + .svg-icon { + margin-left: 20px; + } + + .sub-el-icon { + margin-left: 19px; + } + } + } + + .el-submenu { + overflow: hidden; + + &>.el-submenu__title { + padding: 0 !important; + + .svg-icon { + margin-left: 20px; + } + + .sub-el-icon { + margin-left: 19px; + } + + .el-submenu__icon-arrow { + display: none; + } + } + } + + .el-menu--collapse { + .el-submenu { + &>.el-submenu__title { + &>span { + height: 0; + width: 0; + overflow: hidden; + visibility: hidden; + display: inline-block; + } + } + } + } + } + + .el-menu--collapse .el-menu .el-submenu { + min-width: $sideBarWidth !important; + } + + // mobile responsive + .mobile { + .main-container { + margin-left: 0px; + } + + .sidebar-container { + transition: transform .28s; + width: $sideBarWidth !important; + } + + &.hideSidebar { + .sidebar-container { + pointer-events: none; + transition-duration: 0.3s; + transform: translate3d(-$sideBarWidth, 0, 0); + } + } + } + + .withoutAnimation { + + .main-container, + .sidebar-container { + transition: none; + } + } +} + +// when menu collapsed +.el-menu--vertical { + &>.el-menu { + .svg-icon { + margin-right: 16px; + } + .sub-el-icon { + margin-right: 12px; + margin-left: -2px; + } + } + + .nest-menu .el-submenu>.el-submenu__title, + .el-menu-item { + &:hover { + // you can use $subMenuHover + background-color: $menuHover !important; + } + } + + // the scroll bar appears when the subMenu is too long + >.el-menu--popup { + max-height: 100vh; + overflow-y: auto; + + &::-webkit-scrollbar-track-piece { + background: #d3dce6; + } + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #99a9bf; + border-radius: 20px; + } + } +} diff --git a/web/src/styles/transition.scss b/web/src/styles/transition.scss new file mode 100644 index 0000000..4cb27cc --- /dev/null +++ b/web/src/styles/transition.scss @@ -0,0 +1,48 @@ +// global transition css + +/* fade */ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.28s; +} + +.fade-enter, +.fade-leave-active { + opacity: 0; +} + +/* fade-transform */ +.fade-transform-leave-active, +.fade-transform-enter-active { + transition: all .5s; +} + +.fade-transform-enter { + opacity: 0; + transform: translateX(-30px); +} + +.fade-transform-leave-to { + opacity: 0; + transform: translateX(30px); +} + +/* breadcrumb transition */ +.breadcrumb-enter-active, +.breadcrumb-leave-active { + transition: all .5s; +} + +.breadcrumb-enter, +.breadcrumb-leave-active { + opacity: 0; + transform: translateX(20px); +} + +.breadcrumb-move { + transition: all .5s; +} + +.breadcrumb-leave-active { + position: absolute; +} diff --git a/web/src/styles/variables.scss b/web/src/styles/variables.scss new file mode 100644 index 0000000..bac3746 --- /dev/null +++ b/web/src/styles/variables.scss @@ -0,0 +1,25 @@ +// sidebar +$menuText:#bfcbd9; +$menuActiveText:#409EFF; +$subMenuActiveText: #f4f4f5; //https://github.com/ElemeFE/element/issues/12951 + +$menuBg: #304156; +$menuHover:#263445; + +$subMenuBg:#1f2d3d; +$subMenuHover:#001528; + +$sideBarWidth: 210px; + +// the :export directive is the magic sauce for webpack +// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass +:export { + menuText: $menuText; + menuActiveText: $menuActiveText; + subMenuActiveText: $subMenuActiveText; + menuBg: $menuBg; + menuHover: $menuHover; + subMenuBg: $subMenuBg; + subMenuHover: $subMenuHover; + sideBarWidth: $sideBarWidth; +} diff --git a/web/src/utils/auth.js b/web/src/utils/auth.js new file mode 100644 index 0000000..6e90501 --- /dev/null +++ b/web/src/utils/auth.js @@ -0,0 +1,43 @@ +import Cookies from 'js-cookie' + +const TokenKey = 'wvp_token' +const NameKey = 'wvp_username' +const serverIdKey = 'wvp_server_id' +const expires = 30 + +export function getToken() { + console.log('Getting token...') + return Cookies.get(TokenKey) +} + +export function setToken(token) { + return Cookies.set(TokenKey, token, {expires: expires}) +} + +export function removeToken() { + return Cookies.remove(TokenKey) +} + +export function getName() { + return Cookies.get(NameKey) +} + +export function setName(name) { + return Cookies.set(NameKey, name, {expires: expires}) +} + +export function removeName() { + return Cookies.remove(NameKey) +} + +export function getServerId() { + return Cookies.get(serverIdKey) +} + +export function setServerId(serverId) { + return Cookies.set(serverIdKey, serverId, {expires: expires}) +} + +export function removeServerId() { + return Cookies.remove(serverIdKey) +} diff --git a/web/src/utils/diff.js b/web/src/utils/diff.js new file mode 100644 index 0000000..223141f --- /dev/null +++ b/web/src/utils/diff.js @@ -0,0 +1,76 @@ +// utils/diff.js +// 返回 newObj 相对于 oldObj 的变化部分(新增/修改)。 +// 可选 includeRemoved=true 时把被删除字段以 removedValue 标记返回。 +// 使用示例: diff(oldObj, newObj) 或 diff(oldObj, newObj, { includeRemoved: true }) + +export function diff(oldObj, newObj, options = {}) { + const { includeRemoved = false, removedValue = null, comparator } = options + + function isObject(v) { + return v && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date) + } + function isDate(v) { + return v instanceof Date + } + + function equal(a, b) { + if (typeof comparator === 'function') return comparator(a, b) + if (isDate(a) && isDate(b)) return a.getTime() === b.getTime() + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false + for (let i = 0; i < a.length; i++) if (!equal(a[i], b[i])) return false + return true + } + if (isObject(a) && isObject(b)) { + const aKeys = Object.keys(a) + const bKeys = Object.keys(b) + if (aKeys.length !== bKeys.length) return false + for (const k of aKeys) { + if (!Object.prototype.hasOwnProperty.call(b, k) || !equal(a[k], b[k])) return false + } + return true + } + return a === b + } + + function inner(o, n) { + // Normalize undefined -> empty object for easier handling in object branch. + const oIsObj = isObject(o) + const nIsObj = isObject(n) + + if (equal(o, n)) return undefined + + if (oIsObj && nIsObj) { + const result = {} + const keys = new Set([...Object.keys(o || {}), ...Object.keys(n || {})]) + for (const k of keys) { + console.log(k) + const hasO = Object.prototype.hasOwnProperty.call(o || {}, k) + const hasN = Object.prototype.hasOwnProperty.call(n || {}, k) + + if (hasN) { + const sub = inner(hasO ? o[k] : undefined, n[k]) + if (sub !== undefined) result[k] = sub + } else if (hasO && includeRemoved) { + // key existed before but removed now + result[k] = removedValue + } + } + return Object.keys(result).length ? result : undefined + } + + if (Array.isArray(o) && Array.isArray(n)) { + return equal(o, n) ? undefined : n + } + + if (isDate(n)) return n + + // Different primitive or type change -> return new value + return n + } + + const res = inner(oldObj ?? {}, newObj ?? {}) + return res === undefined ? {} : res +} + +export default diff diff --git a/web/src/utils/get-page-title.js b/web/src/utils/get-page-title.js new file mode 100644 index 0000000..e1bcb85 --- /dev/null +++ b/web/src/utils/get-page-title.js @@ -0,0 +1,10 @@ +import defaultSettings from '@/settings' + +const title = defaultSettings.title || 'WVP视频平台' + +export default function getPageTitle(pageTitle) { + if (pageTitle) { + return `${pageTitle} - ${title}` + } + return `${title}` +} diff --git a/web/src/utils/index.js b/web/src/utils/index.js new file mode 100644 index 0000000..4830c04 --- /dev/null +++ b/web/src/utils/index.js @@ -0,0 +1,117 @@ +/** + * Created by PanJiaChen on 16/11/18. + */ + +/** + * Parse the time to string + * @param {(Object|string|number)} time + * @param {string} cFormat + * @returns {string | null} + */ +export function parseTime(time, cFormat) { + if (arguments.length === 0 || !time) { + return null + } + const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' + let date + if (typeof time === 'object') { + date = time + } else { + if ((typeof time === 'string')) { + if ((/^[0-9]+$/.test(time))) { + // support "1548221490638" + time = parseInt(time) + } else { + // support safari + // https://stackoverflow.com/questions/4310953/invalid-date-in-safari + time = time.replace(new RegExp(/-/gm), '/') + } + } + + if ((typeof time === 'number') && (time.toString().length === 10)) { + time = time * 1000 + } + date = new Date(time) + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay() + } + const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { + const value = formatObj[key] + // Note: getDay() returns 0 on Sunday + if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } + return value.toString().padStart(2, '0') + }) + return time_str +} + +/** + * @param {number} time + * @param {string} option + * @returns {string} + */ +export function formatTime(time, option) { + if (('' + time).length === 10) { + time = parseInt(time) * 1000 + } else { + time = +time + } + const d = new Date(time) + const now = Date.now() + + const diff = (now - d) / 1000 + + if (diff < 30) { + return '刚刚' + } else if (diff < 3600) { + // less 1 hour + return Math.ceil(diff / 60) + '分钟前' + } else if (diff < 3600 * 24) { + return Math.ceil(diff / 3600) + '小时前' + } else if (diff < 3600 * 24 * 2) { + return '1天前' + } + if (option) { + return parseTime(time, option) + } else { + return ( + d.getMonth() + + 1 + + '月' + + d.getDate() + + '日' + + d.getHours() + + '时' + + d.getMinutes() + + '分' + ) + } +} + +/** + * @param {string} url + * @returns {Object} + */ +export function param2Obj(url) { + const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') + if (!search) { + return {} + } + const obj = {} + const searchArr = search.split('&') + searchArr.forEach(v => { + const index = v.indexOf('=') + if (index !== -1) { + const name = v.substring(0, index) + const val = v.substring(index + 1, v.length) + obj[name] = val + } + }) + return obj +} diff --git a/web/src/utils/request.js b/web/src/utils/request.js new file mode 100644 index 0000000..7f7ce54 --- /dev/null +++ b/web/src/utils/request.js @@ -0,0 +1,102 @@ +import axios from 'axios' +import { MessageBox, Message } from 'element-ui' +import store from '@/store' +import { getToken } from '@/utils/auth' + +let showLoginConfirm = false + +// create an axios instance +const service = axios.create({ + baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url + // withCredentials: true, // send cookies when cross-domain requests + timeout: 30000 // request timeout +}) + +// request interceptor +service.interceptors.request.use( + config => { + // do something before request is sent + if (store.getters.token && config.url.indexOf('api/user/login') < 0) { + config.headers['access-token'] = getToken() + } + return config + }, + error => { + // do something with request error + console.log(error) // for debug + return Promise.reject(error) + } +) + +// response interceptor +service.interceptors.response.use( + /** + * If you want to get http information such as headers or status + * Please return response => response + */ + + /** + * Determine the request status by custom code + * Here is just an example + * You can also judge the status by HTTP Status Code + */ + response => { + if (response.config.url.indexOf('/api/user/logout') >= 0) { + return + } + const res = response.data + if (res.code && res.code !== 0) { + Message.error({ + message: res.msg, + duration: 5 * 1000 + }) + } else { + return res + } + }, + error => { + console.log(error) // for debug + if (error.response.status === 401) { + if (!showLoginConfirm && store.getters.showConfirmBoxForLoginLose) { + // to re-login + showLoginConfirm = true + MessageBox.confirm('登录已经到期, 是否重新登录', '登录确认', { + confirmButtonText: '重新登录', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + store.dispatch('user/resetToken').then(() => { + location.reload() + }) + }).catch(() => { + store.dispatch('user/closeConfirmBoxForLoginLose') + Message.warning({ + type: 'warning', + message: '登录过期提示已经关闭,请注销后重新登录' + }) + // 清除token, 后续请求不再继续 + + }) + } + }else { + if (!store.getters.showConfirmBoxForLoginLose) { + return + } + let data = error.response.data + if (data && data.msg) { + Message.error({ + message: data.msg, + showClose: true + }) + }else { + Message.error({ + message: error.message, + showClose: true + }) + } + } + // return Promise.reject(error) + } +) + +export default service diff --git a/web/src/utils/validate.js b/web/src/utils/validate.js new file mode 100644 index 0000000..87388b4 --- /dev/null +++ b/web/src/utils/validate.js @@ -0,0 +1,19 @@ +/** + * Created by PanJiaChen on 16/11/18. + */ + +/** + * @param {string} path + * @returns {Boolean} + */ +export function isExternal(path) { + return /^(https?:|mailto:|tel:)/.test(path) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validUsername(str) { + return str.length >= 0 +} diff --git a/web/src/views/404.vue b/web/src/views/404.vue new file mode 100644 index 0000000..1791f55 --- /dev/null +++ b/web/src/views/404.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/web/src/views/channel/edit.vue b/web/src/views/channel/edit.vue new file mode 100644 index 0000000..b1d5504 --- /dev/null +++ b/web/src/views/channel/edit.vue @@ -0,0 +1,30 @@ + + + diff --git a/web/src/views/channel/group/index.vue b/web/src/views/channel/group/index.vue new file mode 100755 index 0000000..92ddf63 --- /dev/null +++ b/web/src/views/channel/group/index.vue @@ -0,0 +1,321 @@ + + + diff --git a/web/src/views/channel/index.vue b/web/src/views/channel/index.vue new file mode 100755 index 0000000..76f53d1 --- /dev/null +++ b/web/src/views/channel/index.vue @@ -0,0 +1,500 @@ + + + diff --git a/web/src/views/channel/record.vue b/web/src/views/channel/record.vue new file mode 100755 index 0000000..bd01a32 --- /dev/null +++ b/web/src/views/channel/record.vue @@ -0,0 +1,622 @@ + + + + + diff --git a/web/src/views/channel/region/index.vue b/web/src/views/channel/region/index.vue new file mode 100755 index 0000000..2bee055 --- /dev/null +++ b/web/src/views/channel/region/index.vue @@ -0,0 +1,307 @@ + + + diff --git a/web/src/views/cloudRecord/cloudRecordPlayer.vue b/web/src/views/cloudRecord/cloudRecordPlayer.vue new file mode 100755 index 0000000..1962b42 --- /dev/null +++ b/web/src/views/cloudRecord/cloudRecordPlayer.vue @@ -0,0 +1,426 @@ + + + + + diff --git a/web/src/views/cloudRecord/detail.vue b/web/src/views/cloudRecord/detail.vue new file mode 100755 index 0000000..b3eed00 --- /dev/null +++ b/web/src/views/cloudRecord/detail.vue @@ -0,0 +1,453 @@ + + + + + diff --git a/web/src/views/cloudRecord/index.vue b/web/src/views/cloudRecord/index.vue new file mode 100755 index 0000000..e7e5001 --- /dev/null +++ b/web/src/views/cloudRecord/index.vue @@ -0,0 +1,364 @@ + + + + + diff --git a/web/src/views/cloudRecord/playerDialog.vue b/web/src/views/cloudRecord/playerDialog.vue new file mode 100644 index 0000000..8236472 --- /dev/null +++ b/web/src/views/cloudRecord/playerDialog.vue @@ -0,0 +1,61 @@ + + + diff --git a/web/src/views/common/CommonChannelEdit.vue b/web/src/views/common/CommonChannelEdit.vue new file mode 100644 index 0000000..79710a7 --- /dev/null +++ b/web/src/views/common/CommonChannelEdit.vue @@ -0,0 +1,446 @@ + + + + diff --git a/web/src/views/common/DeviceTree.vue b/web/src/views/common/DeviceTree.vue new file mode 100755 index 0000000..b7cb3d9 --- /dev/null +++ b/web/src/views/common/DeviceTree.vue @@ -0,0 +1,289 @@ + + + + + diff --git a/web/src/views/common/GroupTree.vue b/web/src/views/common/GroupTree.vue new file mode 100755 index 0000000..22b6e54 --- /dev/null +++ b/web/src/views/common/GroupTree.vue @@ -0,0 +1,577 @@ + + + + + diff --git a/web/src/views/common/MapComponent.vue b/web/src/views/common/MapComponent.vue new file mode 100755 index 0000000..16d3c70 --- /dev/null +++ b/web/src/views/common/MapComponent.vue @@ -0,0 +1,783 @@ + + + + + diff --git a/web/src/views/common/MapComponent_bak.vue b/web/src/views/common/MapComponent_bak.vue new file mode 100755 index 0000000..738d224 --- /dev/null +++ b/web/src/views/common/MapComponent_bak.vue @@ -0,0 +1,807 @@ + + + + + diff --git a/web/src/views/common/RegionTree.vue b/web/src/views/common/RegionTree.vue new file mode 100755 index 0000000..4e9d4c2 --- /dev/null +++ b/web/src/views/common/RegionTree.vue @@ -0,0 +1,558 @@ + + + + + diff --git a/web/src/views/common/VideoTimeLine/WindowListItem.vue b/web/src/views/common/VideoTimeLine/WindowListItem.vue new file mode 100644 index 0000000..f8f12f1 --- /dev/null +++ b/web/src/views/common/VideoTimeLine/WindowListItem.vue @@ -0,0 +1,195 @@ + + + + + diff --git a/web/src/views/common/VideoTimeLine/constant.js b/web/src/views/common/VideoTimeLine/constant.js new file mode 100644 index 0000000..4082722 --- /dev/null +++ b/web/src/views/common/VideoTimeLine/constant.js @@ -0,0 +1,89 @@ +// 一小时的毫秒数 +export const ONE_HOUR_STAMP = 60 * 60 * 1000 +// 时间分辨率,即整个时间轴表示的时间范围 +export const ZOOM = [0.5, 1, 2, 6, 12, 24, 72, 360, 720, 8760, 87600]// 半小时、1小时、2小时、6小时、12小时、1天、3天、15天、30天、365天、365*10天 +// 时间分辨率对应的每格小时数,即最小格代表多少小时 +export const ZOOM_HOUR_GRID = [1 / 60, 1 / 60, 2 / 60, 1 / 6, 0.25, 0.5, 1, 4, 4, 720, 7200] +export const MOBILE_ZOOM_HOUR_GRID = [ + 1 / 20, + 1 / 30, + 1 / 20, + 1 / 3, + 0.5, + 2, + 4, + 4, + 4, + 720, 7200 +] +// 时间分辨率对应的时间显示判断条件 +export const ZOOM_DATE_SHOW_RULE = [ + () => { // 全部显示 + return true + }, + date => { // 每五分钟显示 + return date.getMinutes() % 5 === 0 + }, + date => { // 每十分钟显示 + return date.getMinutes() % 10 === 0 + }, + date => { // 整点和半点显示 + return date.getMinutes() === 0 || date.getMinutes() === 30 + }, + date => { // 整点显示 + return date.getMinutes() === 0 + }, + date => { // 偶数整点的小时 + return date.getHours() % 2 === 0 && date.getMinutes() === 0 + }, + date => { // 每三小时小时 + return date.getHours() % 3 === 0 && date.getMinutes() === 0 + }, + date => { // 每12小时 + return date.getHours() % 12 === 0 && date.getMinutes() === 0 + }, + date => { // 全不显示 + return false + }, + date => { + return true + }, + date => { + return true + } +] +export const MOBILE_ZOOM_DATE_SHOW_RULE = [ + () => { // 全部显示 + return true + }, + date => { // 每五分钟显示 + return date.getMinutes() % 5 === 0 + }, + date => { // 每十分钟显示 + return date.getMinutes() % 10 === 0 + }, + date => { // 整点和半点显示 + return date.getMinutes() === 0 || date.getMinutes() === 30 + }, + date => { // 偶数整点的小时 + return date.getHours() % 2 === 0 && date.getMinutes() === 0 + }, + date => { // 偶数整点的小时 + return date.getHours() % 4 === 0 && date.getMinutes() === 0 + }, + date => { // 每三小时小时 + return date.getHours() % 3 === 0 && date.getMinutes() === 0 + }, + date => { // 每12小时 + return date.getHours() % 12 === 0 && date.getMinutes() === 0 + }, + date => { // 全不显示 + return false + }, + date => { + return true + }, + date => { + return true + } +] diff --git a/web/src/views/common/VideoTimeLine/index.vue b/web/src/views/common/VideoTimeLine/index.vue new file mode 100644 index 0000000..760d90b --- /dev/null +++ b/web/src/views/common/VideoTimeLine/index.vue @@ -0,0 +1,1048 @@ + + + + + diff --git a/web/src/views/common/channelPlayer/chooseChannelForJt.vue b/web/src/views/common/channelPlayer/chooseChannelForJt.vue new file mode 100755 index 0000000..e69de29 diff --git a/web/src/views/common/channelPlayer/index.vue b/web/src/views/common/channelPlayer/index.vue new file mode 100755 index 0000000..c1ac34f --- /dev/null +++ b/web/src/views/common/channelPlayer/index.vue @@ -0,0 +1,899 @@ + + + + + diff --git a/web/src/views/common/channelPlayer/jtDeviceEdit.vue b/web/src/views/common/channelPlayer/jtDeviceEdit.vue new file mode 100755 index 0000000..5cd0718 --- /dev/null +++ b/web/src/views/common/channelPlayer/jtDeviceEdit.vue @@ -0,0 +1,88 @@ + + + diff --git a/web/src/views/common/channelPlayer/jtDevicePlayer.vue b/web/src/views/common/channelPlayer/jtDevicePlayer.vue new file mode 100755 index 0000000..337080b --- /dev/null +++ b/web/src/views/common/channelPlayer/jtDevicePlayer.vue @@ -0,0 +1,913 @@ + + + + + diff --git a/web/src/views/common/channelPlayer/ptzCruising.vue b/web/src/views/common/channelPlayer/ptzCruising.vue new file mode 100644 index 0000000..bff51a4 --- /dev/null +++ b/web/src/views/common/channelPlayer/ptzCruising.vue @@ -0,0 +1,363 @@ + + + diff --git a/web/src/views/common/channelPlayer/ptzPreset.vue b/web/src/views/common/channelPlayer/ptzPreset.vue new file mode 100644 index 0000000..ca4e0dd --- /dev/null +++ b/web/src/views/common/channelPlayer/ptzPreset.vue @@ -0,0 +1,162 @@ + + + diff --git a/web/src/views/common/channelPlayer/ptzScan.vue b/web/src/views/common/channelPlayer/ptzScan.vue new file mode 100644 index 0000000..81a1310 --- /dev/null +++ b/web/src/views/common/channelPlayer/ptzScan.vue @@ -0,0 +1,233 @@ + + + + diff --git a/web/src/views/common/channelPlayer/ptzSwitch.vue b/web/src/views/common/channelPlayer/ptzSwitch.vue new file mode 100644 index 0000000..c363616 --- /dev/null +++ b/web/src/views/common/channelPlayer/ptzSwitch.vue @@ -0,0 +1,72 @@ + + + + diff --git a/web/src/views/common/channelPlayer/ptzWiper.vue b/web/src/views/common/channelPlayer/ptzWiper.vue new file mode 100644 index 0000000..10563ce --- /dev/null +++ b/web/src/views/common/channelPlayer/ptzWiper.vue @@ -0,0 +1,62 @@ + + + + diff --git a/web/src/views/common/h265web.vue b/web/src/views/common/h265web.vue new file mode 100644 index 0000000..cf7be7f --- /dev/null +++ b/web/src/views/common/h265web.vue @@ -0,0 +1,330 @@ + + + + + diff --git a/web/src/views/common/jessibuca.vue b/web/src/views/common/jessibuca.vue new file mode 100755 index 0000000..1e1581f --- /dev/null +++ b/web/src/views/common/jessibuca.vue @@ -0,0 +1,311 @@ + + + + + diff --git a/web/src/views/common/map/DragInteraction.js b/web/src/views/common/map/DragInteraction.js new file mode 100644 index 0000000..ee536ec --- /dev/null +++ b/web/src/views/common/map/DragInteraction.js @@ -0,0 +1,116 @@ +import PointerInteraction from 'ol/interaction/Pointer' +import { toLonLat } from './TransformLonLat' + +class DragInteraction extends PointerInteraction { + constructor() { + super({ + handleDownEvent: (evt) => { + const map = evt.map + + const feature = map.forEachFeatureAtPixel(evt.pixel, (feature) => { + if (this.featureIdMap_.has(feature.getId())) { + return feature + }else { + return null + } + }) + if (feature) { + this.coordinate_ = evt.coordinate + this.feature_ = feature + let eventCallback = this.featureIdMap_.get(this.feature_.getId()) + if (eventCallback && eventCallback.startEvent) { + eventCallback.startEvent(evt) + } + return !!feature + } + }, + handleDragEvent: (evt) => { + const deltaX = evt.coordinate[0] - this.coordinate_[0] + const deltaY = evt.coordinate[1] - this.coordinate_[1] + + const geometry = this.feature_.getGeometry() + geometry.translate(deltaX, deltaY) + + this.coordinate_[0] = evt.coordinate[0] + this.coordinate_[1] = evt.coordinate[1] + + let eventCallback = this.featureIdMap_.get(this.feature_.getId()) + if (eventCallback && eventCallback.moveEvent) { + eventCallback.moveEvent(evt) + } + + }, + handleMoveEvent: (evt) => { + if (this.cursor_) { + const map = evt.map + const feature = map.forEachFeatureAtPixel(evt.pixel, function(feature) { + return feature + }) + const element = evt.map.getTargetElement() + if (feature) { + if (element.style.cursor != this.cursor_) { + this.previousCursor_ = element.style.cursor + element.style.cursor = this.cursor_ + } + } else if (this.previousCursor_ !== undefined) { + element.style.cursor = this.previousCursor_ + this.previousCursor_ = undefined + } + } + }, + handleUpEvent: (evt) => { + let eventCallback = this.featureIdMap_.get(this.feature_.getId()) + if (eventCallback && eventCallback.endEvent) { + evt.lonLat = toLonLat(this.feature_.getGeometry().getCoordinates()) + eventCallback.endEvent(evt) + } + this.coordinate_ = null + this.feature_ = null + return false + } + }) + + /** + * @type {import('../src/ol/coordinate.js').Coordinate} + * @private + */ + this.coordinate_ = null + + /** + * @type {string|undefined} + * @private + */ + this.cursor_ = 'pointer' + + /** + * @type {Feature} + * @private + */ + this.feature_ = null + + /** + * @type {string|undefined} + * @private + */ + this.previousCursor_ = undefined + + this.featureIdMap_ = new Map() + + this.addFeatureId = (id, moveEndEvent) => { + if (this.featureIdMap_.has(id)) { + return + } + this.featureIdMap_.set(id, moveEndEvent) + } + + this.removeFeatureId= (id) => { + this.featureIdMap_.delete(id) + } + + this.hasFeatureId= (id) => { + this.featureIdMap_.has(id) + } + } +} + +export default DragInteraction diff --git a/web/src/views/common/map/TransformLonLat.js b/web/src/views/common/map/TransformLonLat.js new file mode 100644 index 0000000..8d25eca --- /dev/null +++ b/web/src/views/common/map/TransformLonLat.js @@ -0,0 +1,17 @@ +import { fromLonLat as projFromLonLat, toLonLat as projToLonLat } from 'ol/proj' +import gcoord from 'gcoord' + +export function fromLonLat(coordinate) { + if (window.coordinateSystem === 'GCJ02') { + return projFromLonLat(gcoord.transform(coordinate, gcoord.WGS84, gcoord.GCJ02)) + }else { + return projFromLonLat(coordinate) + } +} +export function toLonLat(coordinate) { + if (window.coordinateSystem === 'GCJ02') { + return gcoord.transform(projToLonLat(coordinate), gcoord.GCJ02, gcoord.WGS84) + }else { + return projToLonLat(coordinate) + } +} diff --git a/web/src/views/common/mediaInfo.vue b/web/src/views/common/mediaInfo.vue new file mode 100644 index 0000000..ec12a0a --- /dev/null +++ b/web/src/views/common/mediaInfo.vue @@ -0,0 +1,97 @@ + + + + diff --git a/web/src/views/common/ptzCruising.vue b/web/src/views/common/ptzCruising.vue new file mode 100644 index 0000000..e48c14a --- /dev/null +++ b/web/src/views/common/ptzCruising.vue @@ -0,0 +1,328 @@ + + + + diff --git a/web/src/views/common/ptzPreset.vue b/web/src/views/common/ptzPreset.vue new file mode 100644 index 0000000..debd76b --- /dev/null +++ b/web/src/views/common/ptzPreset.vue @@ -0,0 +1,152 @@ + + + diff --git a/web/src/views/common/ptzScan.vue b/web/src/views/common/ptzScan.vue new file mode 100644 index 0000000..9484f07 --- /dev/null +++ b/web/src/views/common/ptzScan.vue @@ -0,0 +1,212 @@ + + + + diff --git a/web/src/views/common/ptzSwitch.vue b/web/src/views/common/ptzSwitch.vue new file mode 100644 index 0000000..b4eecd6 --- /dev/null +++ b/web/src/views/common/ptzSwitch.vue @@ -0,0 +1,76 @@ + + + + diff --git a/web/src/views/common/ptzWiper.vue b/web/src/views/common/ptzWiper.vue new file mode 100644 index 0000000..d60fa51 --- /dev/null +++ b/web/src/views/common/ptzWiper.vue @@ -0,0 +1,58 @@ + + + + diff --git a/web/src/views/common/rtcPlayer.vue b/web/src/views/common/rtcPlayer.vue new file mode 100755 index 0000000..a71f596 --- /dev/null +++ b/web/src/views/common/rtcPlayer.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/web/src/views/common/weekTimePicker.vue b/web/src/views/common/weekTimePicker.vue new file mode 100644 index 0000000..7598b62 --- /dev/null +++ b/web/src/views/common/weekTimePicker.vue @@ -0,0 +1,481 @@ + + + + diff --git a/web/src/views/dashboard/console/ConsoleCPU.vue b/web/src/views/dashboard/console/ConsoleCPU.vue new file mode 100755 index 0000000..7e88996 --- /dev/null +++ b/web/src/views/dashboard/console/ConsoleCPU.vue @@ -0,0 +1,111 @@ + + + diff --git a/web/src/views/dashboard/console/ConsoleDisk.vue b/web/src/views/dashboard/console/ConsoleDisk.vue new file mode 100755 index 0000000..f89f5b7 --- /dev/null +++ b/web/src/views/dashboard/console/ConsoleDisk.vue @@ -0,0 +1,83 @@ + + + diff --git a/web/src/views/dashboard/console/ConsoleMEM.vue b/web/src/views/dashboard/console/ConsoleMEM.vue new file mode 100755 index 0000000..47c3f53 --- /dev/null +++ b/web/src/views/dashboard/console/ConsoleMEM.vue @@ -0,0 +1,106 @@ + + + diff --git a/web/src/views/dashboard/console/ConsoleMediaServer.vue b/web/src/views/dashboard/console/ConsoleMediaServer.vue new file mode 100755 index 0000000..fcdfde9 --- /dev/null +++ b/web/src/views/dashboard/console/ConsoleMediaServer.vue @@ -0,0 +1,88 @@ + + + diff --git a/web/src/views/dashboard/console/ConsoleNet.vue b/web/src/views/dashboard/console/ConsoleNet.vue new file mode 100755 index 0000000..4aa766c --- /dev/null +++ b/web/src/views/dashboard/console/ConsoleNet.vue @@ -0,0 +1,135 @@ + + + diff --git a/web/src/views/dashboard/console/ConsoleNodeLoad.vue b/web/src/views/dashboard/console/ConsoleNodeLoad.vue new file mode 100755 index 0000000..598661d --- /dev/null +++ b/web/src/views/dashboard/console/ConsoleNodeLoad.vue @@ -0,0 +1,76 @@ + + + diff --git a/web/src/views/dashboard/console/ConsoleResource.vue b/web/src/views/dashboard/console/ConsoleResource.vue new file mode 100755 index 0000000..81ab3a1 --- /dev/null +++ b/web/src/views/dashboard/console/ConsoleResource.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/web/src/views/dashboard/index.vue b/web/src/views/dashboard/index.vue new file mode 100755 index 0000000..052348f --- /dev/null +++ b/web/src/views/dashboard/index.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/web/src/views/device/channel/edit.vue b/web/src/views/device/channel/edit.vue new file mode 100644 index 0000000..f57e3a7 --- /dev/null +++ b/web/src/views/device/channel/edit.vue @@ -0,0 +1,30 @@ + + + diff --git a/web/src/views/device/channel/index.vue b/web/src/views/device/channel/index.vue new file mode 100755 index 0000000..0fd5229 --- /dev/null +++ b/web/src/views/device/channel/index.vue @@ -0,0 +1,558 @@ + + + diff --git a/web/src/views/device/channel/record.vue b/web/src/views/device/channel/record.vue new file mode 100755 index 0000000..4bde465 --- /dev/null +++ b/web/src/views/device/channel/record.vue @@ -0,0 +1,625 @@ + + + + + diff --git a/web/src/views/device/edit.vue b/web/src/views/device/edit.vue new file mode 100755 index 0000000..3b7f848 --- /dev/null +++ b/web/src/views/device/edit.vue @@ -0,0 +1,134 @@ + + + diff --git a/web/src/views/device/index.vue b/web/src/views/device/index.vue new file mode 100755 index 0000000..8e5250c --- /dev/null +++ b/web/src/views/device/index.vue @@ -0,0 +1,32 @@ + + + diff --git a/web/src/views/device/list.vue b/web/src/views/device/list.vue new file mode 100755 index 0000000..d78d865 --- /dev/null +++ b/web/src/views/device/list.vue @@ -0,0 +1,427 @@ + + + diff --git a/web/src/views/dialog/GbChannelSelect.vue b/web/src/views/dialog/GbChannelSelect.vue new file mode 100644 index 0000000..54183b2 --- /dev/null +++ b/web/src/views/dialog/GbChannelSelect.vue @@ -0,0 +1,199 @@ + + + diff --git a/web/src/views/dialog/GbDeviceSelect.vue b/web/src/views/dialog/GbDeviceSelect.vue new file mode 100644 index 0000000..68606b2 --- /dev/null +++ b/web/src/views/dialog/GbDeviceSelect.vue @@ -0,0 +1,163 @@ + + + + diff --git a/web/src/views/dialog/MediaServerEdit.vue b/web/src/views/dialog/MediaServerEdit.vue new file mode 100755 index 0000000..7239453 --- /dev/null +++ b/web/src/views/dialog/MediaServerEdit.vue @@ -0,0 +1,317 @@ + + + diff --git a/web/src/views/dialog/SyncChannelProgress.vue b/web/src/views/dialog/SyncChannelProgress.vue new file mode 100755 index 0000000..be76b57 --- /dev/null +++ b/web/src/views/dialog/SyncChannelProgress.vue @@ -0,0 +1,113 @@ + + + diff --git a/web/src/views/dialog/UnusualGroupChannelSelect.vue b/web/src/views/dialog/UnusualGroupChannelSelect.vue new file mode 100644 index 0000000..b358657 --- /dev/null +++ b/web/src/views/dialog/UnusualGroupChannelSelect.vue @@ -0,0 +1,255 @@ + + + diff --git a/web/src/views/dialog/UnusualRegionChannelSelect.vue b/web/src/views/dialog/UnusualRegionChannelSelect.vue new file mode 100644 index 0000000..ff236b5 --- /dev/null +++ b/web/src/views/dialog/UnusualRegionChannelSelect.vue @@ -0,0 +1,267 @@ + + + diff --git a/web/src/views/dialog/addUser.vue b/web/src/views/dialog/addUser.vue new file mode 100644 index 0000000..06d23d5 --- /dev/null +++ b/web/src/views/dialog/addUser.vue @@ -0,0 +1,143 @@ + + + diff --git a/web/src/views/dialog/addUserApiKey.vue b/web/src/views/dialog/addUserApiKey.vue new file mode 100644 index 0000000..aae6953 --- /dev/null +++ b/web/src/views/dialog/addUserApiKey.vue @@ -0,0 +1,139 @@ + + + diff --git a/web/src/views/dialog/catalogEdit.vue b/web/src/views/dialog/catalogEdit.vue new file mode 100755 index 0000000..181e84d --- /dev/null +++ b/web/src/views/dialog/catalogEdit.vue @@ -0,0 +1,161 @@ + + + diff --git a/web/src/views/dialog/changePasswordForAdmin.vue b/web/src/views/dialog/changePasswordForAdmin.vue new file mode 100755 index 0000000..cf5b875 --- /dev/null +++ b/web/src/views/dialog/changePasswordForAdmin.vue @@ -0,0 +1,119 @@ + + + diff --git a/web/src/views/dialog/changePushKey.vue b/web/src/views/dialog/changePushKey.vue new file mode 100755 index 0000000..0ae69ac --- /dev/null +++ b/web/src/views/dialog/changePushKey.vue @@ -0,0 +1,100 @@ + + + diff --git a/web/src/views/dialog/channelCode.vue b/web/src/views/dialog/channelCode.vue new file mode 100644 index 0000000..700d507 --- /dev/null +++ b/web/src/views/dialog/channelCode.vue @@ -0,0 +1,330 @@ + + + + + diff --git a/web/src/views/dialog/channelMapInfobox.vue b/web/src/views/dialog/channelMapInfobox.vue new file mode 100755 index 0000000..7061b03 --- /dev/null +++ b/web/src/views/dialog/channelMapInfobox.vue @@ -0,0 +1,65 @@ + + + diff --git a/web/src/views/dialog/chooseCivilCode.vue b/web/src/views/dialog/chooseCivilCode.vue new file mode 100644 index 0000000..99f3549 --- /dev/null +++ b/web/src/views/dialog/chooseCivilCode.vue @@ -0,0 +1,79 @@ + + + diff --git a/web/src/views/dialog/chooseGroup.vue b/web/src/views/dialog/chooseGroup.vue new file mode 100644 index 0000000..5801f3a --- /dev/null +++ b/web/src/views/dialog/chooseGroup.vue @@ -0,0 +1,88 @@ + + + diff --git a/web/src/views/dialog/chooseTimeRange.vue b/web/src/views/dialog/chooseTimeRange.vue new file mode 100644 index 0000000..be628eb --- /dev/null +++ b/web/src/views/dialog/chooseTimeRange.vue @@ -0,0 +1,74 @@ + + + + diff --git a/web/src/views/dialog/commonChannelEditDialog.vue b/web/src/views/dialog/commonChannelEditDialog.vue new file mode 100755 index 0000000..8c03bf3 --- /dev/null +++ b/web/src/views/dialog/commonChannelEditDialog.vue @@ -0,0 +1,78 @@ + + + diff --git a/web/src/views/dialog/configInfo.vue b/web/src/views/dialog/configInfo.vue new file mode 100755 index 0000000..5296d1b --- /dev/null +++ b/web/src/views/dialog/configInfo.vue @@ -0,0 +1,66 @@ + + + diff --git a/web/src/views/dialog/devicePlayer.vue b/web/src/views/dialog/devicePlayer.vue new file mode 100755 index 0000000..48bbaa0 --- /dev/null +++ b/web/src/views/dialog/devicePlayer.vue @@ -0,0 +1,878 @@ + + + + + diff --git a/web/src/views/dialog/editRecordPlan.vue b/web/src/views/dialog/editRecordPlan.vue new file mode 100644 index 0000000..4670df9 --- /dev/null +++ b/web/src/views/dialog/editRecordPlan.vue @@ -0,0 +1,222 @@ + + + + diff --git a/web/src/views/dialog/groupEdit.vue b/web/src/views/dialog/groupEdit.vue new file mode 100755 index 0000000..ee3e70d --- /dev/null +++ b/web/src/views/dialog/groupEdit.vue @@ -0,0 +1,138 @@ + + + diff --git a/web/src/views/dialog/hasStreamChannel.vue b/web/src/views/dialog/hasStreamChannel.vue new file mode 100644 index 0000000..a492ea4 --- /dev/null +++ b/web/src/views/dialog/hasStreamChannel.vue @@ -0,0 +1,147 @@ + + + diff --git a/web/src/views/dialog/importChannel.vue b/web/src/views/dialog/importChannel.vue new file mode 100755 index 0000000..7d79fe9 --- /dev/null +++ b/web/src/views/dialog/importChannel.vue @@ -0,0 +1,107 @@ + + + + diff --git a/web/src/views/dialog/importChannelShowErrorData.vue b/web/src/views/dialog/importChannelShowErrorData.vue new file mode 100755 index 0000000..f569704 --- /dev/null +++ b/web/src/views/dialog/importChannelShowErrorData.vue @@ -0,0 +1,68 @@ + + + + diff --git a/web/src/views/dialog/linkChannelRecord.vue b/web/src/views/dialog/linkChannelRecord.vue new file mode 100755 index 0000000..8a60219 --- /dev/null +++ b/web/src/views/dialog/linkChannelRecord.vue @@ -0,0 +1,304 @@ + + + diff --git a/web/src/views/dialog/pushStreamEdit.vue b/web/src/views/dialog/pushStreamEdit.vue new file mode 100755 index 0000000..9394759 --- /dev/null +++ b/web/src/views/dialog/pushStreamEdit.vue @@ -0,0 +1,133 @@ + + + diff --git a/web/src/views/dialog/queryTrace.vue b/web/src/views/dialog/queryTrace.vue new file mode 100755 index 0000000..5632d96 --- /dev/null +++ b/web/src/views/dialog/queryTrace.vue @@ -0,0 +1,109 @@ + + + diff --git a/web/src/views/dialog/recordDownload.vue b/web/src/views/dialog/recordDownload.vue new file mode 100755 index 0000000..37686d3 --- /dev/null +++ b/web/src/views/dialog/recordDownload.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/web/src/views/dialog/regionCode.vue b/web/src/views/dialog/regionCode.vue new file mode 100644 index 0000000..b665b58 --- /dev/null +++ b/web/src/views/dialog/regionCode.vue @@ -0,0 +1,266 @@ + + + + + diff --git a/web/src/views/dialog/regionEdit.vue b/web/src/views/dialog/regionEdit.vue new file mode 100644 index 0000000..7687d88 --- /dev/null +++ b/web/src/views/dialog/regionEdit.vue @@ -0,0 +1,316 @@ + + + + + diff --git a/web/src/views/dialog/remarkUserApiKey.vue b/web/src/views/dialog/remarkUserApiKey.vue new file mode 100644 index 0000000..be19869 --- /dev/null +++ b/web/src/views/dialog/remarkUserApiKey.vue @@ -0,0 +1,94 @@ + + + diff --git a/web/src/views/dialog/resetChannel.vue b/web/src/views/dialog/resetChannel.vue new file mode 100644 index 0000000..f101eec --- /dev/null +++ b/web/src/views/dialog/resetChannel.vue @@ -0,0 +1,245 @@ + + + diff --git a/web/src/views/dialog/shareChannel.vue b/web/src/views/dialog/shareChannel.vue new file mode 100755 index 0000000..21ab2d3 --- /dev/null +++ b/web/src/views/dialog/shareChannel.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/web/src/views/dialog/shareChannelAdd.vue b/web/src/views/dialog/shareChannelAdd.vue new file mode 100755 index 0000000..0193ebe --- /dev/null +++ b/web/src/views/dialog/shareChannelAdd.vue @@ -0,0 +1,417 @@ + + + diff --git a/web/src/views/form/index.vue b/web/src/views/form/index.vue new file mode 100644 index 0000000..f4d66d3 --- /dev/null +++ b/web/src/views/form/index.vue @@ -0,0 +1,85 @@ + + + + + + diff --git a/web/src/views/jtDevice/channel/edit.vue b/web/src/views/jtDevice/channel/edit.vue new file mode 100644 index 0000000..a33db8d --- /dev/null +++ b/web/src/views/jtDevice/channel/edit.vue @@ -0,0 +1,89 @@ + + + diff --git a/web/src/views/jtDevice/channel/index.vue b/web/src/views/jtDevice/channel/index.vue new file mode 100755 index 0000000..2af145d --- /dev/null +++ b/web/src/views/jtDevice/channel/index.vue @@ -0,0 +1,368 @@ + + + diff --git a/web/src/views/jtDevice/channel/record.vue b/web/src/views/jtDevice/channel/record.vue new file mode 100755 index 0000000..968aeec --- /dev/null +++ b/web/src/views/jtDevice/channel/record.vue @@ -0,0 +1,611 @@ + + + + + diff --git a/web/src/views/jtDevice/deviceParam/alarm.vue b/web/src/views/jtDevice/deviceParam/alarm.vue new file mode 100755 index 0000000..af13c61 --- /dev/null +++ b/web/src/views/jtDevice/deviceParam/alarm.vue @@ -0,0 +1,124 @@ + + + diff --git a/web/src/views/jtDevice/deviceParam/alarmSign.vue b/web/src/views/jtDevice/deviceParam/alarmSign.vue new file mode 100755 index 0000000..7fcb1a6 --- /dev/null +++ b/web/src/views/jtDevice/deviceParam/alarmSign.vue @@ -0,0 +1,134 @@ + + + diff --git a/web/src/views/jtDevice/deviceParam/awakenParam.vue b/web/src/views/jtDevice/deviceParam/awakenParam.vue new file mode 100755 index 0000000..0a6b86b --- /dev/null +++ b/web/src/views/jtDevice/deviceParam/awakenParam.vue @@ -0,0 +1,121 @@ + + + diff --git a/web/src/views/jtDevice/deviceParam/cameraTimer.vue b/web/src/views/jtDevice/deviceParam/cameraTimer.vue new file mode 100755 index 0000000..f4d710f --- /dev/null +++ b/web/src/views/jtDevice/deviceParam/cameraTimer.vue @@ -0,0 +1,86 @@ + + + diff --git a/web/src/views/jtDevice/deviceParam/canCollectionParam.vue b/web/src/views/jtDevice/deviceParam/canCollectionParam.vue new file mode 100755 index 0000000..53f768c --- /dev/null +++ b/web/src/views/jtDevice/deviceParam/canCollectionParam.vue @@ -0,0 +1,86 @@ + + + diff --git a/web/src/views/jtDevice/deviceParam/carInfo.vue b/web/src/views/jtDevice/deviceParam/carInfo.vue new file mode 100755 index 0000000..f980c49 --- /dev/null +++ b/web/src/views/jtDevice/deviceParam/carInfo.vue @@ -0,0 +1,89 @@ + + + diff --git a/web/src/views/jtDevice/deviceParam/communication.vue b/web/src/views/jtDevice/deviceParam/communication.vue new file mode 100755 index 0000000..1901082 --- /dev/null +++ b/web/src/views/jtDevice/deviceParam/communication.vue @@ -0,0 +1,80 @@ + + + diff --git a/web/src/views/jtDevice/deviceParam/driving.vue b/web/src/views/jtDevice/deviceParam/driving.vue new file mode 100755 index 0000000..cbfca7c --- /dev/null +++ b/web/src/views/jtDevice/deviceParam/driving.vue @@ -0,0 +1,95 @@ + + + diff --git a/web/src/views/jtDevice/deviceParam/gnssParam.vue b/web/src/views/jtDevice/deviceParam/gnssParam.vue new file mode 100755 index 0000000..b2f056a --- /dev/null +++ b/web/src/views/jtDevice/deviceParam/gnssParam.vue @@ -0,0 +1,120 @@ + + + diff --git a/web/src/views/jtDevice/deviceParam/imageConfig.vue b/web/src/views/jtDevice/deviceParam/imageConfig.vue new file mode 100755 index 0000000..9f9b072 --- /dev/null +++ b/web/src/views/jtDevice/deviceParam/imageConfig.vue @@ -0,0 +1,95 @@ + + + + diff --git a/web/src/views/jtDevice/deviceParam/index.vue b/web/src/views/jtDevice/deviceParam/index.vue new file mode 100755 index 0000000..a344195 --- /dev/null +++ b/web/src/views/jtDevice/deviceParam/index.vue @@ -0,0 +1,123 @@ + + + diff --git a/web/src/views/jtDevice/deviceParam/phoneNumber.vue b/web/src/views/jtDevice/deviceParam/phoneNumber.vue new file mode 100755 index 0000000..144b61f --- /dev/null +++ b/web/src/views/jtDevice/deviceParam/phoneNumber.vue @@ -0,0 +1,89 @@ + + + diff --git a/web/src/views/jtDevice/deviceParam/position.vue b/web/src/views/jtDevice/deviceParam/position.vue new file mode 100755 index 0000000..865b24f --- /dev/null +++ b/web/src/views/jtDevice/deviceParam/position.vue @@ -0,0 +1,116 @@ + + + diff --git a/web/src/views/jtDevice/deviceParam/server.vue b/web/src/views/jtDevice/deviceParam/server.vue new file mode 100755 index 0000000..8a06fac --- /dev/null +++ b/web/src/views/jtDevice/deviceParam/server.vue @@ -0,0 +1,116 @@ + + + diff --git a/web/src/views/jtDevice/deviceParam/videoAlarmSign.vue b/web/src/views/jtDevice/deviceParam/videoAlarmSign.vue new file mode 100755 index 0000000..7218c61 --- /dev/null +++ b/web/src/views/jtDevice/deviceParam/videoAlarmSign.vue @@ -0,0 +1,62 @@ + + + diff --git a/web/src/views/jtDevice/deviceParam/videoParam.vue b/web/src/views/jtDevice/deviceParam/videoParam.vue new file mode 100755 index 0000000..6a1d35b --- /dev/null +++ b/web/src/views/jtDevice/deviceParam/videoParam.vue @@ -0,0 +1,181 @@ + + + + diff --git a/web/src/views/jtDevice/dialog/attribute.vue b/web/src/views/jtDevice/dialog/attribute.vue new file mode 100755 index 0000000..6023acf --- /dev/null +++ b/web/src/views/jtDevice/dialog/attribute.vue @@ -0,0 +1,77 @@ + + + diff --git a/web/src/views/jtDevice/dialog/connectionServer.vue b/web/src/views/jtDevice/dialog/connectionServer.vue new file mode 100755 index 0000000..8476b00 --- /dev/null +++ b/web/src/views/jtDevice/dialog/connectionServer.vue @@ -0,0 +1,124 @@ + + + diff --git a/web/src/views/jtDevice/dialog/controlDoor.vue b/web/src/views/jtDevice/dialog/controlDoor.vue new file mode 100755 index 0000000..2c98998 --- /dev/null +++ b/web/src/views/jtDevice/dialog/controlDoor.vue @@ -0,0 +1,62 @@ + + + diff --git a/web/src/views/jtDevice/dialog/driverInfo.vue b/web/src/views/jtDevice/dialog/driverInfo.vue new file mode 100755 index 0000000..2ac1885 --- /dev/null +++ b/web/src/views/jtDevice/dialog/driverInfo.vue @@ -0,0 +1,83 @@ + + + diff --git a/web/src/views/jtDevice/dialog/jtDevicePlayer.vue b/web/src/views/jtDevice/dialog/jtDevicePlayer.vue new file mode 100755 index 0000000..68fc296 --- /dev/null +++ b/web/src/views/jtDevice/dialog/jtDevicePlayer.vue @@ -0,0 +1,894 @@ + + + + + diff --git a/web/src/views/jtDevice/dialog/mediaAttribute.vue b/web/src/views/jtDevice/dialog/mediaAttribute.vue new file mode 100755 index 0000000..31290a3 --- /dev/null +++ b/web/src/views/jtDevice/dialog/mediaAttribute.vue @@ -0,0 +1,75 @@ + + + diff --git a/web/src/views/jtDevice/dialog/phoneBook.vue b/web/src/views/jtDevice/dialog/phoneBook.vue new file mode 100755 index 0000000..f659f14 --- /dev/null +++ b/web/src/views/jtDevice/dialog/phoneBook.vue @@ -0,0 +1,219 @@ + + + + + diff --git a/web/src/views/jtDevice/dialog/position.vue b/web/src/views/jtDevice/dialog/position.vue new file mode 100755 index 0000000..90f92c6 --- /dev/null +++ b/web/src/views/jtDevice/dialog/position.vue @@ -0,0 +1,135 @@ + + + diff --git a/web/src/views/jtDevice/dialog/queryMediaList.vue b/web/src/views/jtDevice/dialog/queryMediaList.vue new file mode 100755 index 0000000..8d1b3bf --- /dev/null +++ b/web/src/views/jtDevice/dialog/queryMediaList.vue @@ -0,0 +1,288 @@ + + + + + diff --git a/web/src/views/jtDevice/dialog/queryMediaListDialog.vue b/web/src/views/jtDevice/dialog/queryMediaListDialog.vue new file mode 100755 index 0000000..9847da1 --- /dev/null +++ b/web/src/views/jtDevice/dialog/queryMediaListDialog.vue @@ -0,0 +1,57 @@ + + + diff --git a/web/src/views/jtDevice/dialog/shootingNow.vue b/web/src/views/jtDevice/dialog/shootingNow.vue new file mode 100755 index 0000000..3a43696 --- /dev/null +++ b/web/src/views/jtDevice/dialog/shootingNow.vue @@ -0,0 +1,232 @@ + + + + + diff --git a/web/src/views/jtDevice/dialog/telephoneCallback.vue b/web/src/views/jtDevice/dialog/telephoneCallback.vue new file mode 100755 index 0000000..c2528f5 --- /dev/null +++ b/web/src/views/jtDevice/dialog/telephoneCallback.vue @@ -0,0 +1,87 @@ + + + diff --git a/web/src/views/jtDevice/dialog/textMsg.vue b/web/src/views/jtDevice/dialog/textMsg.vue new file mode 100755 index 0000000..4e6f0ce --- /dev/null +++ b/web/src/views/jtDevice/dialog/textMsg.vue @@ -0,0 +1,118 @@ + + + diff --git a/web/src/views/jtDevice/edit.vue b/web/src/views/jtDevice/edit.vue new file mode 100755 index 0000000..52f3aff --- /dev/null +++ b/web/src/views/jtDevice/edit.vue @@ -0,0 +1,87 @@ + + + diff --git a/web/src/views/jtDevice/index.vue b/web/src/views/jtDevice/index.vue new file mode 100755 index 0000000..1932915 --- /dev/null +++ b/web/src/views/jtDevice/index.vue @@ -0,0 +1,44 @@ + + + diff --git a/web/src/views/jtDevice/list.vue b/web/src/views/jtDevice/list.vue new file mode 100755 index 0000000..2ef70f1 --- /dev/null +++ b/web/src/views/jtDevice/list.vue @@ -0,0 +1,441 @@ + + + diff --git a/web/src/views/live/index.vue b/web/src/views/live/index.vue new file mode 100755 index 0000000..6095648 --- /dev/null +++ b/web/src/views/live/index.vue @@ -0,0 +1,406 @@ + + + diff --git a/web/src/views/login/index.vue b/web/src/views/login/index.vue new file mode 100644 index 0000000..dd6324d --- /dev/null +++ b/web/src/views/login/index.vue @@ -0,0 +1,250 @@ + + + + + + + diff --git a/web/src/views/map/dialog/drawThinProgress.vue b/web/src/views/map/dialog/drawThinProgress.vue new file mode 100755 index 0000000..8590dbf --- /dev/null +++ b/web/src/views/map/dialog/drawThinProgress.vue @@ -0,0 +1,91 @@ + + + diff --git a/web/src/views/map/index.vue b/web/src/views/map/index.vue new file mode 100755 index 0000000..3dddd87 --- /dev/null +++ b/web/src/views/map/index.vue @@ -0,0 +1,731 @@ + + + + + diff --git a/web/src/views/map/index2.vue b/web/src/views/map/index2.vue new file mode 100755 index 0000000..af57d4f --- /dev/null +++ b/web/src/views/map/index2.vue @@ -0,0 +1,989 @@ + + + + + diff --git a/web/src/views/map/queryTrace.vue b/web/src/views/map/queryTrace.vue new file mode 100755 index 0000000..d712eb8 --- /dev/null +++ b/web/src/views/map/queryTrace.vue @@ -0,0 +1,105 @@ + + + diff --git a/web/src/views/mediaServer/index.vue b/web/src/views/mediaServer/index.vue new file mode 100755 index 0000000..c0da700 --- /dev/null +++ b/web/src/views/mediaServer/index.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/web/src/views/operations/historyLog.vue b/web/src/views/operations/historyLog.vue new file mode 100755 index 0000000..2d1eb5f --- /dev/null +++ b/web/src/views/operations/historyLog.vue @@ -0,0 +1,258 @@ + + + + + diff --git a/web/src/views/operations/realLog.vue b/web/src/views/operations/realLog.vue new file mode 100755 index 0000000..3066e31 --- /dev/null +++ b/web/src/views/operations/realLog.vue @@ -0,0 +1,40 @@ + + + diff --git a/web/src/views/operations/showLog.vue b/web/src/views/operations/showLog.vue new file mode 100755 index 0000000..b983b40 --- /dev/null +++ b/web/src/views/operations/showLog.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/web/src/views/operations/systemInfo.vue b/web/src/views/operations/systemInfo.vue new file mode 100755 index 0000000..038f904 --- /dev/null +++ b/web/src/views/operations/systemInfo.vue @@ -0,0 +1,45 @@ + + + diff --git a/web/src/views/platform/edit.vue b/web/src/views/platform/edit.vue new file mode 100644 index 0000000..acfa027 --- /dev/null +++ b/web/src/views/platform/edit.vue @@ -0,0 +1,281 @@ + + + diff --git a/web/src/views/platform/index.vue b/web/src/views/platform/index.vue new file mode 100755 index 0000000..3c88d65 --- /dev/null +++ b/web/src/views/platform/index.vue @@ -0,0 +1,325 @@ + + + + diff --git a/web/src/views/recordPlan/index.vue b/web/src/views/recordPlan/index.vue new file mode 100755 index 0000000..c30631b --- /dev/null +++ b/web/src/views/recordPlan/index.vue @@ -0,0 +1,169 @@ + + + diff --git a/web/src/views/streamProxy/edit.vue b/web/src/views/streamProxy/edit.vue new file mode 100644 index 0000000..c40289c --- /dev/null +++ b/web/src/views/streamProxy/edit.vue @@ -0,0 +1,208 @@ + + + diff --git a/web/src/views/streamProxy/index.vue b/web/src/views/streamProxy/index.vue new file mode 100755 index 0000000..6825e41 --- /dev/null +++ b/web/src/views/streamProxy/index.vue @@ -0,0 +1,284 @@ + + + diff --git a/web/src/views/streamPush/buildPushStreamUrl.vue b/web/src/views/streamPush/buildPushStreamUrl.vue new file mode 100644 index 0000000..60c5ffb --- /dev/null +++ b/web/src/views/streamPush/buildPushStreamUrl.vue @@ -0,0 +1,188 @@ + + + diff --git a/web/src/views/streamPush/edit.vue b/web/src/views/streamPush/edit.vue new file mode 100644 index 0000000..e7de658 --- /dev/null +++ b/web/src/views/streamPush/edit.vue @@ -0,0 +1,101 @@ + + + + diff --git a/web/src/views/streamPush/index.vue b/web/src/views/streamPush/index.vue new file mode 100755 index 0000000..afbd602 --- /dev/null +++ b/web/src/views/streamPush/index.vue @@ -0,0 +1,313 @@ + + + + diff --git a/web/src/views/user/apiKeyManager.vue b/web/src/views/user/apiKeyManager.vue new file mode 100644 index 0000000..a2af35f --- /dev/null +++ b/web/src/views/user/apiKeyManager.vue @@ -0,0 +1,324 @@ + + + + diff --git a/web/src/views/user/index.vue b/web/src/views/user/index.vue new file mode 100755 index 0000000..616fefa --- /dev/null +++ b/web/src/views/user/index.vue @@ -0,0 +1,183 @@ + + + diff --git a/web/tests/unit/.eslintrc.js b/web/tests/unit/.eslintrc.js new file mode 100644 index 0000000..958d51b --- /dev/null +++ b/web/tests/unit/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + env: { + jest: true + } +} diff --git a/web/tests/unit/components/Breadcrumb.spec.js b/web/tests/unit/components/Breadcrumb.spec.js new file mode 100644 index 0000000..1d94c8f --- /dev/null +++ b/web/tests/unit/components/Breadcrumb.spec.js @@ -0,0 +1,98 @@ +import { mount, createLocalVue } from '@vue/test-utils' +import VueRouter from 'vue-router' +import ElementUI from 'element-ui' +import Breadcrumb from '@/components/Breadcrumb/index.vue' + +const localVue = createLocalVue() +localVue.use(VueRouter) +localVue.use(ElementUI) + +const routes = [ + { + path: '/', + name: 'home', + children: [{ + path: 'dashboard', + name: 'dashboard' + }] + }, + { + path: '/menu', + name: 'menu', + children: [{ + path: 'menu1', + name: 'menu1', + meta: { title: 'menu1' }, + children: [{ + path: 'menu1-1', + name: 'menu1-1', + meta: { title: 'menu1-1' } + }, + { + path: 'menu1-2', + name: 'menu1-2', + redirect: 'noredirect', + meta: { title: 'menu1-2' }, + children: [{ + path: 'menu1-2-1', + name: 'menu1-2-1', + meta: { title: 'menu1-2-1' } + }, + { + path: 'menu1-2-2', + name: 'menu1-2-2' + }] + }] + }] + }] + +const router = new VueRouter({ + routes +}) + +describe('Breadcrumb.vue', () => { + const wrapper = mount(Breadcrumb, { + localVue, + router + }) + it('dashboard', () => { + router.push('/dashboard') + const len = wrapper.findAll('.el-breadcrumb__inner').length + expect(len).toBe(1) + }) + it('normal route', () => { + router.push('/menu/menu1') + const len = wrapper.findAll('.el-breadcrumb__inner').length + expect(len).toBe(2) + }) + it('nested route', () => { + router.push('/menu/menu1/menu1-2/menu1-2-1') + const len = wrapper.findAll('.el-breadcrumb__inner').length + expect(len).toBe(4) + }) + it('no meta.title', () => { + router.push('/menu/menu1/menu1-2/menu1-2-2') + const len = wrapper.findAll('.el-breadcrumb__inner').length + expect(len).toBe(3) + }) + // it('click link', () => { + // router.push('/menu/menu1/menu1-2/menu1-2-2') + // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') + // const second = breadcrumbArray.at(1) + // console.log(breadcrumbArray) + // const href = second.find('a').attributes().href + // expect(href).toBe('#/menu/menu1') + // }) + // it('noRedirect', () => { + // router.push('/menu/menu1/menu1-2/menu1-2-1') + // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') + // const redirectBreadcrumb = breadcrumbArray.at(2) + // expect(redirectBreadcrumb.contains('a')).toBe(false) + // }) + it('last breadcrumb', () => { + router.push('/menu/menu1/menu1-2/menu1-2-1') + const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') + const redirectBreadcrumb = breadcrumbArray.at(3) + expect(redirectBreadcrumb.contains('a')).toBe(false) + }) +}) diff --git a/web/tests/unit/components/Hamburger.spec.js b/web/tests/unit/components/Hamburger.spec.js new file mode 100644 index 0000000..01ea303 --- /dev/null +++ b/web/tests/unit/components/Hamburger.spec.js @@ -0,0 +1,18 @@ +import { shallowMount } from '@vue/test-utils' +import Hamburger from '@/components/Hamburger/index.vue' +describe('Hamburger.vue', () => { + it('toggle click', () => { + const wrapper = shallowMount(Hamburger) + const mockFn = jest.fn() + wrapper.vm.$on('toggleClick', mockFn) + wrapper.find('.hamburger').trigger('click') + expect(mockFn).toBeCalled() + }) + it('prop isActive', () => { + const wrapper = shallowMount(Hamburger) + wrapper.setProps({ isActive: true }) + expect(wrapper.contains('.is-active')).toBe(true) + wrapper.setProps({ isActive: false }) + expect(wrapper.contains('.is-active')).toBe(false) + }) +}) diff --git a/web/tests/unit/components/SvgIcon.spec.js b/web/tests/unit/components/SvgIcon.spec.js new file mode 100644 index 0000000..31467a9 --- /dev/null +++ b/web/tests/unit/components/SvgIcon.spec.js @@ -0,0 +1,22 @@ +import { shallowMount } from '@vue/test-utils' +import SvgIcon from '@/components/SvgIcon/index.vue' +describe('SvgIcon.vue', () => { + it('iconClass', () => { + const wrapper = shallowMount(SvgIcon, { + propsData: { + iconClass: 'test' + } + }) + expect(wrapper.find('use').attributes().href).toBe('#icon-test') + }) + it('className', () => { + const wrapper = shallowMount(SvgIcon, { + propsData: { + iconClass: 'test' + } + }) + expect(wrapper.classes().length).toBe(1) + wrapper.setProps({ className: 'test' }) + expect(wrapper.classes().includes('test')).toBe(true) + }) +}) diff --git a/web/tests/unit/utils/formatTime.spec.js b/web/tests/unit/utils/formatTime.spec.js new file mode 100644 index 0000000..24e165b --- /dev/null +++ b/web/tests/unit/utils/formatTime.spec.js @@ -0,0 +1,30 @@ +import { formatTime } from '@/utils/index.js' + +describe('Utils:formatTime', () => { + const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" + const retrofit = 5 * 1000 + + it('ten digits timestamp', () => { + expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') + }) + it('test now', () => { + expect(formatTime(+new Date() - 1)).toBe('刚刚') + }) + it('less two minute', () => { + expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') + }) + it('less two hour', () => { + expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') + }) + it('less one day', () => { + expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') + }) + it('more than one day', () => { + expect(formatTime(d)).toBe('7月13日17时54分') + }) + it('format', () => { + expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') + expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') + expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') + }) +}) diff --git a/web/tests/unit/utils/param2Obj.spec.js b/web/tests/unit/utils/param2Obj.spec.js new file mode 100644 index 0000000..e106ed8 --- /dev/null +++ b/web/tests/unit/utils/param2Obj.spec.js @@ -0,0 +1,14 @@ +import { param2Obj } from '@/utils/index.js' +describe('Utils:param2Obj', () => { + const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95' + + it('param2Obj test', () => { + expect(param2Obj(url)).toEqual({ + name: 'bill', + age: '29', + sex: '1', + field: window.btoa('test'), + key: '测试' + }) + }) +}) diff --git a/web/tests/unit/utils/parseTime.spec.js b/web/tests/unit/utils/parseTime.spec.js new file mode 100644 index 0000000..56045af --- /dev/null +++ b/web/tests/unit/utils/parseTime.spec.js @@ -0,0 +1,35 @@ +import { parseTime } from '@/utils/index.js' + +describe('Utils:parseTime', () => { + const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" + it('timestamp', () => { + expect(parseTime(d)).toBe('2018-07-13 17:54:01') + }) + it('timestamp string', () => { + expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01') + }) + it('ten digits timestamp', () => { + expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') + }) + it('new Date', () => { + expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') + }) + it('format', () => { + expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') + expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') + expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') + }) + it('get the day of the week', () => { + expect(parseTime(d, '{a}')).toBe('五') // 星期五 + }) + it('get the day of the week', () => { + expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 + }) + it('empty argument', () => { + expect(parseTime()).toBeNull() + }) + + it('null', () => { + expect(parseTime(null)).toBeNull() + }) +}) diff --git a/web/tests/unit/utils/validate.spec.js b/web/tests/unit/utils/validate.spec.js new file mode 100644 index 0000000..f774905 --- /dev/null +++ b/web/tests/unit/utils/validate.spec.js @@ -0,0 +1,17 @@ +import { validUsername, isExternal } from '@/utils/validate.js' + +describe('Utils:validate', () => { + it('validUsername', () => { + expect(validUsername('admin')).toBe(true) + expect(validUsername('editor')).toBe(true) + expect(validUsername('xxxx')).toBe(false) + }) + it('isExternal', () => { + expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) + expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) + expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false) + expect(isExternal('/dashboard')).toBe(false) + expect(isExternal('./dashboard')).toBe(false) + expect(isExternal('dashboard')).toBe(false) + }) +}) diff --git a/web/vue.config.js b/web/vue.config.js new file mode 100644 index 0000000..ad8876c --- /dev/null +++ b/web/vue.config.js @@ -0,0 +1,136 @@ +const path = require("path") +const defaultSettings = require("./src/settings.js") + +function resolve(dir) { + return path.join(__dirname, dir) +} + +const name = defaultSettings.title || "WVP视频平台" // page title + +// If your port is set to 80, +// use administrator privileges to execute the command line. +// For example, Mac: sudo npm run +// You can change the port by the following methods: +// port = 9528 npm run dev OR npm run dev --port = 9528 +const port = process.env.port || process.env.npm_config_port || 9528 // dev port + +// All configuration item explanations can be find in https://cli.vuejs.org/config/ +module.exports = { + /** + * You will need to set publicPath if you plan to deploy your site under a sub path, + * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/, + * then publicPath should be set to "/bar/". + * In most cases please use '/' !!! + * Detail: https://cli.vuejs.org/config/#publicpath + */ + publicPath: "/", + outputDir: "../src/main/resources/static", + assetsDir: "static", + lintOnSave: false, // Disable ESLint to avoid warnings + productionSourceMap: false, + transpileDependencies: ["ol"], + devServer: { + public: "localhost:" + port, + host: "localhost", + port: port, + open: true, + overlay: { + warnings: false, + errors: false, // Changed to false to hide ESLint errors + }, + // before: require('./mock/mock-server.js'), + proxy: { + "/dev-api": { + target: "http://127.0.0.1:18080", + changeOrigin: true, + pathRewrite: { + "^/dev-api": "/", + }, + }, + "/static/snap": { + target: "http://127.0.0.1:18080", + changeOrigin: true, + // pathRewrite: { + // '^/static/snap': '/static/snap' + // } + }, + }, + }, + configureWebpack: { + // provide the app's title in webpack's name field, so that + // it can be accessed in index.html to inject the correct title. + name: name, + resolve: { + alias: { + "@": resolve("src"), + }, + }, + }, + chainWebpack(config) { + // it can improve the speed of the first screen, it is recommended to turn on preload + config.plugin("preload").tap(() => [ + { + rel: "preload", + // to ignore runtime.js + // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171 + fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/], + include: "initial", + }, + ]) + + // when there are many pages, it will cause too many meaningless requests + config.plugins.delete("prefetch") + + // set svg-sprite-loader + config.module.rule("svg").exclude.add(resolve("src/icons")).end() + config.module + .rule("icons") + .test(/\.svg$/) + .include.add(resolve("src/icons")) + .end() + .use("svg-sprite-loader") + .loader("svg-sprite-loader") + .options({ + symbolId: "icon-[name]", + }) + .end() + + config.when(process.env.NODE_ENV !== "development", (config) => { + config + .plugin("ScriptExtHtmlWebpackPlugin") + .after("html") + .use("script-ext-html-webpack-plugin", [ + { + // `runtime` must same as runtimeChunk name. default is `runtime` + inline: /runtime\..*\.js$/, + }, + ]) + .end() + config.optimization.splitChunks({ + chunks: "all", + cacheGroups: { + libs: { + name: "chunk-libs", + test: /[\\/]node_modules[\\/]/, + priority: 10, + chunks: "initial", // only package third parties that are initially dependent + }, + elementUI: { + name: "chunk-elementUI", // split elementUI into a single package + priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app + test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm + }, + commons: { + name: "chunk-commons", + test: resolve("src/components"), // can customize your rules + minChunks: 3, // minimum common number + priority: 5, + reuseExistingChunk: true, + }, + }, + }) + // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk + config.optimization.runtimeChunk("single") + }) + }, +} diff --git a/zlm.md b/zlm.md new file mode 100644 index 0000000..bbaf329 --- /dev/null +++ b/zlm.md @@ -0,0 +1,15 @@ +1. 增加接口:用来返回hook调用耗时。 + 参数: sn, + 返回值: hook耗时 + 功能: zlm收到调用后立即调用心跳hook,hook中携带sn信息, zlm收到hook返回后计算此次hook的耗时,然后返回耗时 + 用途: 1. 可以用来检测hook配置是否正常 + 2. 检测wvp与zlm连接的健康程度 +2. 增加接口:查询loadMP4File的录像播放位置 + 参数: app, steam, + 返回: 当前播放的hook位置 + 功能: zlm收到请求后查询流的seek位置并返回 + 用途: 1. 获取当前播放位置,可以在页面同步显示时间 + 2. 用来时快进五秒 快退五秒类似的操作 +3. loadMP4File扩展, 增加默认seek值和默认倍速,开始时直接从这个位置开始,并使用指定的倍速 +4. 支持下载指定取件的mp4文件,这个区间可能包括多个文件和文件的一部分。 +5. 希望seek接口和倍速接口可以去除schema的必选,作为可选项,不传则默认全部 diff --git a/打包/config/config.ini b/打包/config/config.ini new file mode 100644 index 0000000..18b5fac --- /dev/null +++ b/打包/config/config.ini @@ -0,0 +1,199 @@ +; auto-generated by mINI class { + +[api] +apiDebug=1 +defaultSnap=./www/logo.png +downloadRoot=./www +secret=034523TF8yT83wh5Wvz73f7 +snapRoot=./www/snap/ + +[cluster] +origin_url= +retry_count=3 +timeout_sec=15 + +[ffmpeg] +bin=/usr/bin/ffmpeg +cmd=%s -re -i %s -c:a aac -strict -2 -ar 44100 -ab 48k -c:v libx264 -f flv %s +cmd2=%s -rtsp_transport tcp -i %s -c:a aac -strict -2 -ar 44100 -ab 48k -c:v libx264 -f rtsp %s +log=./ffmpeg/ffmpeg.log +restart_sec=0 +snap=%s -rtsp_transport tcp -i %s -y -f mjpeg -frames:v 1 %s + +[general] +check_nvidia_dev=1 +enableVhost=0 +enable_ffmpeg_log=0 +flowThreshold=1024 +maxStreamWaitMS=15000 +mediaServerId=XwFtVZrtZbHJq4UV +mergeWriteMS=0 +resetWhenRePlay=1 +streamNoneReaderDelayMS=20000 +unready_frame_cache=100 +wait_add_track_ms=3000 +wait_track_ready_ms=10000 + +[hls] +broadcastRecordTs=0 +deleteDelaySec=10 +fastRegister=0 +fileBufSize=65536 +segDelay=0 +segDur=2 +segKeep=0 +segNum=3 +segRetain=5 + +[hook] +alive_interval=10.0 +enable=1 +on_flow_report= +on_http_access= +on_play=http://192.168.1.3:18082/index/hook/on_play +on_publish=http://192.168.1.3:18082/index/hook/on_publish +on_record_mp4=http://192.168.1.3:18082/index/hook/on_record_mp4 +on_record_ts= +on_rtp_server_timeout=http://192.168.1.3:18082/index/hook/on_rtp_server_timeout +on_rtsp_auth= +on_rtsp_realm= +on_send_rtp_stopped=http://192.168.1.3:18082/index/hook/on_send_rtp_stopped +on_server_exited= +on_server_keepalive=http://192.168.1.3:18082/index/hook/on_server_keepalive +on_server_started=http://192.168.1.3:18082/index/hook/on_server_started +on_shell_login= +on_stream_changed=http://192.168.1.3:18082/index/hook/on_stream_changed +on_stream_none_reader=http://192.168.1.3:18082/index/hook/on_stream_none_reader +on_stream_not_found=http://192.168.1.3:18082/index/hook/on_stream_not_found +retry=1 +retry_delay=3.000000 +stream_changed_schemas=rtsp/rtmp/fmp4/ts/hls/hls.fmp4 +timeoutSec=20 + +[http] +allow_cross_domains=1 +allow_ip_range= +charSet=utf-8 +dirMenu=1 +forbidCacheSuffix= +forwarded_ip_header= +keepAliveSecond=15 +maxReqSize=40960 +notFound=404 Not Found

    您访问的资源不存在!


    ZLMediaKit(git hash:f69f3b3/2023-09-09T10:59:27+08:00,branch:master,build time:2023-09-11T15:03:57)
    +port=7082 +rootPath=./www +sendBufSize=65536 +sslport=11443 +virtualPath= + +[multicast] +addrMax=239.255.255.255 +addrMin=239.0.0.0 +udpTTL=64 + +[protocol] +add_mute_audio=1 +auto_close=0 +continue_push_ms=3000 +enable_audio=1 +enable_fmp4=1 +enable_hls=1 +enable_hls_fmp4=0 +enable_mp4=0 +enable_rtmp=1 +enable_rtsp=1 +enable_ts=1 +fmp4_demand=0 +hls_demand=0 +hls_save_path=./www +modify_stamp=1 +mp4_as_player=0 +mp4_max_second=300 +mp4_save_path=./www +paced_sender_ms=0 +rtmp_demand=0 +rtsp_demand=0 +ts_demand=0 + +[record] +appName=record +enableFmp4=0 +fastStart=0 +fileBufSize=65536 +fileRepeat=0 +sampleMS=500 + +[rtc] +datachannel_echo=1 +externIP=192.168.1.3 +max_bitrate=0 +min_bitrate=0 +port=11340 +preferredCodecA=PCMA,opus,mpeg4-generic +preferredCodecV=H264,H265,AV1,VP9,VP8 +rembBitRate=0 +start_bitrate=0 +tcpPort=11340 +timeoutSec=15 + +[rtmp] +directProxy=1 +enhanced=0 +handshakeSecond=15 +keepAliveSecond=15 +port=11935 +sslport=18350 + +[rtp] +audioMtuSize=600 +h264_stap_a=1 +lowLatency=0 +rtpMaxSize=10 +videoMtuSize=1400 + +[rtp_proxy] +aac_pt=101 +dumpDir=./dump +gop_cache=1 +h264_pt=98 +h265_pt=99 +opus_pt=100 +port=11000 +port_range=30000-40000 +ps_pt=96 +rtp_g711_dur_ms=100 +timeoutSec=15 +udp_recv_socket_buffer=4194304 + +[rtsp] +authBasic=0 +directProxy=1 +handshakeSecond=15 +keepAliveSecond=15 +lowLatency=0 +port=22554 +rtpTransportType=-1 +sslport=11332 + +[shell] +maxReqSize=1024 +port=9900 + +[srt] +latencyMul=4 +pktBufSize=8192 +port=9900 +timeoutSec=5 + +[transcode] +acodec=mpeg4-generic +decoder_h264=h264_qsv,h264_videotoolbox,h264_bm,libopenh264 +decoder_h265=hevc_qsv,hevc_videotoolbox,hevc_bm +enable_ffmpeg_log=0 +encoder_h264=h264_qsv,h264_videotoolbox,h264_bm,libx264,libopenh264 +encoder_h265=hevc_qsv,hevc_videotoolbox,hevc_bm,libx265 +filter= +suffix=transport +vcodec=H264 + +; } --- diff --git a/数据库/2.6.9/初始化-mysql-2.6.9.sql b/数据库/2.6.9/初始化-mysql-2.6.9.sql new file mode 100644 index 0000000..8eb8d71 --- /dev/null +++ b/数据库/2.6.9/初始化-mysql-2.6.9.sql @@ -0,0 +1,323 @@ +/*建表*/ +create table wvp_device ( + id serial primary key , + device_id character varying(50) not null , + name character varying(255), + manufacturer character varying(255), + model character varying(255), + firmware character varying(255), + transport character varying(50), + stream_mode character varying(50), + on_line bool default false, + register_time character varying(50), + keepalive_time character varying(50), + ip character varying(50), + create_time character varying(50), + update_time character varying(50), + port integer, + expires integer, + subscribe_cycle_for_catalog integer DEFAULT 0, + subscribe_cycle_for_mobile_position integer DEFAULT 0, + mobile_position_submission_interval integer DEFAULT 5, + subscribe_cycle_for_alarm integer DEFAULT 0, + host_address character varying(50), + charset character varying(50), + ssrc_check bool default false, + geo_coord_sys character varying(50), + media_server_id character varying(50), + custom_name character varying(255), + sdp_ip character varying(50), + local_ip character varying(50), + password character varying(255), + as_message_channel bool default false, + keepalive_interval_time integer, + switch_primary_sub_stream bool default false, + broadcast_push_after_ack bool default false, + constraint uk_device_device unique (device_id) +); + +create table wvp_device_alarm ( + id serial primary key , + device_id character varying(50) not null, + channel_id character varying(50) not null, + alarm_priority character varying(50), + alarm_method character varying(50), + alarm_time character varying(50), + alarm_description character varying(255), + longitude double precision, + latitude double precision, + alarm_type character varying(50), + create_time character varying(50) not null +); + +create table wvp_device_channel ( + id serial primary key , + channel_id character varying(50) not null, + name character varying(255), + custom_name character varying(255), + manufacture character varying(50), + model character varying(50), + owner character varying(50), + civil_code character varying(50), + block character varying(50), + address character varying(50), + parent_id character varying(50), + safety_way integer, + register_way integer, + cert_num character varying(50), + certifiable integer, + err_code integer, + end_time character varying(50), + secrecy character varying(50), + ip_address character varying(50), + port integer, + password character varying(255), + ptz_type integer, + custom_ptz_type integer, + status bool default false, + longitude double precision, + custom_longitude double precision, + latitude double precision, + custom_latitude double precision, + stream_id character varying(255), + device_id character varying(50) not null, + parental character varying(50), + has_audio bool default false, + create_time character varying(50) not null, + update_time character varying(50) not null, + sub_count integer, + longitude_gcj02 double precision, + latitude_gcj02 double precision, + longitude_wgs84 double precision, + latitude_wgs84 double precision, + business_group_id character varying(50), + gps_time character varying(50), + constraint uk_wvp_device_channel_unique_device_channel unique (device_id, channel_id) +); + +create table wvp_device_mobile_position ( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + device_name character varying(255), + time character varying(50), + longitude double precision, + latitude double precision, + altitude double precision, + speed double precision, + direction double precision, + report_source character varying(50), + longitude_gcj02 double precision, + latitude_gcj02 double precision, + longitude_wgs84 double precision, + latitude_wgs84 double precision, + create_time character varying(50) +); + +create table wvp_gb_stream ( + gb_stream_id serial primary key, + app character varying(255) not null, + stream character varying(255) not null, + gb_id character varying(50) not null, + name character varying(255), + longitude double precision, + latitude double precision, + stream_type character varying(50), + media_server_id character varying(50), + create_time character varying(50), + constraint uk_gb_stream_unique_gb_id unique (gb_id), + constraint uk_gb_stream_unique_app_stream unique (app, stream) +); + +create table wvp_log ( + id serial primary key , + name character varying(50), + type character varying(50), + uri character varying(200), + address character varying(50), + result character varying(50), + timing bigint, + username character varying(50), + create_time character varying(50) +); + +create table wvp_media_server ( + id character varying(255) primary key , + ip character varying(50), + hook_ip character varying(50), + sdp_ip character varying(50), + stream_ip character varying(50), + http_port integer, + http_ssl_port integer, + rtmp_port integer, + rtmp_ssl_port integer, + rtp_proxy_port integer, + rtsp_port integer, + rtsp_ssl_port integer, + auto_config bool default false, + secret character varying(50), + rtp_enable bool default false, + rtp_port_range character varying(50), + send_rtp_port_range character varying(50), + record_assist_port integer, + default_server bool default false, + create_time character varying(50), + update_time character varying(50), + hook_alive_interval integer, + record_path character varying(255), + record_day integer default 7, + constraint uk_media_server_unique_ip_http_port unique (ip, http_port) +); + +create table wvp_platform ( + id serial primary key , + enable bool default false, + name character varying(255), + server_gb_id character varying(50), + server_gb_domain character varying(50), + server_ip character varying(50), + server_port integer, + device_gb_id character varying(50), + device_ip character varying(50), + device_port character varying(50), + username character varying(255), + password character varying(50), + expires character varying(50), + keep_timeout character varying(50), + transport character varying(50), + character_set character varying(50), + catalog_id character varying(50), + ptz bool default false, + rtcp bool default false, + status bool default false, + start_offline_push bool default false, + administrative_division character varying(50), + catalog_group integer, + create_time character varying(50), + update_time character varying(50), + as_message_channel bool default false, + auto_push_channel bool default false, + constraint uk_platform_unique_server_gb_id unique (server_gb_id) +); + +create table wvp_platform_catalog ( + id character varying(50), + platform_id character varying(50), + name character varying(255), + parent_id character varying(50), + civil_code character varying(50), + business_group_id character varying(50), + constraint uk_platform_catalog_id_platform_id unique (id, platform_id) +); + +create table wvp_platform_gb_channel ( + id serial primary key , + platform_id character varying(50), + catalog_id character varying(50), + device_channel_id integer, + constraint uk_platform_gb_channel_platform_id_catalog_id_device_channel_id unique (platform_id, catalog_id, device_channel_id) +); + +create table wvp_platform_gb_stream ( + id serial primary key, + platform_id character varying(50), + catalog_id character varying(50), + gb_stream_id integer, + constraint uk_platform_gb_stream_platform_id_catalog_id_gb_stream_id unique (platform_id, catalog_id, gb_stream_id) +); + +create table wvp_stream_proxy ( + id serial primary key, + type character varying(50), + app character varying(255), + stream character varying(255), + url character varying(255), + src_url character varying(255), + dst_url character varying(255), + timeout_ms integer, + ffmpeg_cmd_key character varying(255), + rtp_type character varying(50), + media_server_id character varying(50), + enable_audio bool default false, + enable_mp4 bool default false, + enable bool default false, + status boolean, + enable_remove_none_reader bool default false, + create_time character varying(50), + name character varying(255), + update_time character varying(50), + stream_key character varying(255), + enable_disable_none_reader bool default false, + constraint uk_stream_proxy_app_stream unique (app, stream) +); + +create table wvp_stream_push ( + id serial primary key, + app character varying(255), + stream character varying(255), + total_reader_count character varying(50), + origin_type integer, + origin_type_str character varying(50), + create_time character varying(50), + alive_second integer, + media_server_id character varying(50), + server_id character varying(50), + push_time character varying(50), + status bool default false, + update_time character varying(50), + push_ing bool default false, + self bool default false, + constraint uk_stream_push_app_stream unique (app, stream) +); +create table wvp_cloud_record ( + id serial primary key, + app character varying(255), + stream character varying(255), + call_id character varying(255), + start_time bigint, + end_time bigint, + media_server_id character varying(50), + file_name character varying(255), + folder character varying(255), + file_path character varying(255), + collect bool default false, + file_size bigint, + time_len bigint, + constraint uk_stream_push_app_stream_path unique (app, stream, file_path) +); + +create table wvp_user ( + id serial primary key, + username character varying(255), + password character varying(255), + role_id integer, + create_time character varying(50), + update_time character varying(50), + push_key character varying(50), + constraint uk_user_username unique (username) +); + +create table wvp_user_role ( + id serial primary key, + name character varying(50), + authority character varying(50), + create_time character varying(50), + update_time character varying(50) +); +create table wvp_resources_tree ( + id serial primary key , + is_catalog bool default true, + device_channel_id integer , + gb_stream_id integer, + name character varying(255), + parentId integer, + path character varying(255) +); + + +/*初始数据*/ +INSERT INTO wvp_user VALUES (1, 'admin','21232f297a57a5a743894a0e4a801fc3',1,'2021-04-13 14:14:57','2021-04-13 14:14:57','3e80d1762a324d5b0ff636e0bd16f1e3'); +INSERT INTO wvp_user_role VALUES (1, 'admin','0','2021-04-13 14:14:57','2021-04-13 14:14:57'); + + + diff --git a/数据库/2.6.9/初始化-postgresql-kingbase-2.6.9.sql b/数据库/2.6.9/初始化-postgresql-kingbase-2.6.9.sql new file mode 100644 index 0000000..b48f646 --- /dev/null +++ b/数据库/2.6.9/初始化-postgresql-kingbase-2.6.9.sql @@ -0,0 +1,323 @@ +/*建表*/ +create table wvp_device ( + id serial primary key , + device_id character varying(50) not null , + name character varying(255), + manufacturer character varying(255), + model character varying(255), + firmware character varying(255), + transport character varying(50), + stream_mode character varying(50), + on_line bool default false, + register_time character varying(50), + keepalive_time character varying(50), + ip character varying(50), + create_time character varying(50), + update_time character varying(50), + port integer, + expires integer, + subscribe_cycle_for_catalog integer DEFAULT 0, + subscribe_cycle_for_mobile_position integer DEFAULT 0, + mobile_position_submission_interval integer DEFAULT 5, + subscribe_cycle_for_alarm integer DEFAULT 0, + host_address character varying(50), + charset character varying(50), + ssrc_check bool default false, + geo_coord_sys character varying(50), + media_server_id character varying(50), + custom_name character varying(255), + sdp_ip character varying(50), + local_ip character varying(50), + password character varying(255), + as_message_channel bool default false, + keepalive_interval_time integer, + switch_primary_sub_stream bool default false, + broadcast_push_after_ack bool default false, + constraint uk_device_device unique (device_id) +); + +create table wvp_device_alarm ( + id serial primary key , + device_id character varying(50) not null, + channel_id character varying(50) not null, + alarm_priority character varying(50), + alarm_method character varying(50), + alarm_time character varying(50), + alarm_description character varying(255), + longitude double precision, + latitude double precision, + alarm_type character varying(50), + create_time character varying(50) not null +); + +create table wvp_device_channel ( + id serial primary key , + channel_id character varying(50) not null, + name character varying(255), + custom_name character varying(255), + manufacture character varying(50), + model character varying(50), + owner character varying(50), + civil_code character varying(50), + block character varying(50), + address character varying(50), + parent_id character varying(50), + safety_way integer, + register_way integer, + cert_num character varying(50), + certifiable integer, + err_code integer, + end_time character varying(50), + secrecy character varying(50), + ip_address character varying(50), + port integer, + password character varying(255), + ptz_type integer, + custom_ptz_type integer, + status bool default false, + longitude double precision, + custom_longitude double precision, + latitude double precision, + custom_latitude double precision, + stream_id character varying(255), + device_id character varying(50) not null, + parental character varying(50), + has_audio bool default false, + create_time character varying(50) not null, + update_time character varying(50) not null, + sub_count integer, + longitude_gcj02 double precision, + latitude_gcj02 double precision, + longitude_wgs84 double precision, + latitude_wgs84 double precision, + business_group_id character varying(50), + gps_time character varying(50), + constraint uk_wvp_device_channel_unique_device_channel unique (device_id, channel_id) +); + +create table wvp_device_mobile_position ( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + device_name character varying(255), + time character varying(50), + longitude double precision, + latitude double precision, + altitude double precision, + speed double precision, + direction double precision, + report_source character varying(50), + longitude_gcj02 double precision, + latitude_gcj02 double precision, + longitude_wgs84 double precision, + latitude_wgs84 double precision, + create_time character varying(50) +); + +create table wvp_gb_stream ( + gb_stream_id serial primary key, + app character varying(255) not null, + stream character varying(255) not null, + gb_id character varying(50) not null, + name character varying(255), + longitude double precision, + latitude double precision, + stream_type character varying(50), + media_server_id character varying(50), + create_time character varying(50), + constraint uk_gb_stream_unique_gb_id unique (gb_id), + constraint uk_gb_stream_unique_app_stream unique (app, stream) +); + +create table wvp_log ( + id serial primary key , + name character varying(50), + type character varying(50), + uri character varying(200), + address character varying(50), + result character varying(50), + timing bigint, + username character varying(50), + create_time character varying(50) +); + +create table wvp_media_server ( + id character varying(255) primary key , + ip character varying(50), + hook_ip character varying(50), + sdp_ip character varying(50), + stream_ip character varying(50), + http_port integer, + http_ssl_port integer, + rtmp_port integer, + rtmp_ssl_port integer, + rtp_proxy_port integer, + rtsp_port integer, + rtsp_ssl_port integer, + auto_config bool default false, + secret character varying(50), + rtp_enable bool default false, + rtp_port_range character varying(50), + send_rtp_port_range character varying(50), + record_assist_port integer, + default_server bool default false, + create_time character varying(50), + update_time character varying(50), + hook_alive_interval integer, + record_path character varying(255), + record_day integer default 7, + constraint uk_media_server_unique_ip_http_port unique (ip, http_port) +); + +create table wvp_platform ( + id serial primary key , + enable bool default false, + name character varying(255), + server_gb_id character varying(50), + server_gb_domain character varying(50), + server_ip character varying(50), + server_port integer, + device_gb_id character varying(50), + device_ip character varying(50), + device_port character varying(50), + username character varying(255), + password character varying(50), + expires character varying(50), + keep_timeout character varying(50), + transport character varying(50), + character_set character varying(50), + catalog_id character varying(50), + ptz bool default false, + rtcp bool default false, + status bool default false, + start_offline_push bool default false, + administrative_division character varying(50), + catalog_group integer, + create_time character varying(50), + update_time character varying(50), + as_message_channel bool default false, + auto_push_channel bool default false, + constraint uk_platform_unique_server_gb_id unique (server_gb_id) +); + +create table wvp_platform_catalog ( + id character varying(50), + platform_id character varying(50), + name character varying(255), + parent_id character varying(50), + civil_code character varying(50), + business_group_id character varying(50), + constraint uk_platform_catalog_id_platform_id unique (id, platform_id) +); + +create table wvp_platform_gb_channel ( + id serial primary key , + platform_id character varying(50), + catalog_id character varying(50), + device_channel_id integer, + constraint uk_platform_gb_channel_platform_id_catalog_id_device_channel_id unique (platform_id, catalog_id, device_channel_id) +); + +create table wvp_platform_gb_stream ( + id serial primary key, + platform_id character varying(50), + catalog_id character varying(50), + gb_stream_id integer, + constraint uk_platform_gb_stream_platform_id_catalog_id_gb_stream_id unique (platform_id, catalog_id, gb_stream_id) +); + +create table wvp_stream_proxy ( + id serial primary key, + type character varying(50), + app character varying(255), + stream character varying(255), + url character varying(255), + src_url character varying(255), + dst_url character varying(255), + timeout_ms integer, + ffmpeg_cmd_key character varying(255), + rtp_type character varying(50), + media_server_id character varying(50), + enable_audio bool default false, + enable_mp4 bool default false, + enable bool default false, + status boolean, + enable_remove_none_reader bool default false, + create_time character varying(50), + name character varying(255), + update_time character varying(50), + stream_key character varying(255), + enable_disable_none_reader bool default false, + constraint uk_stream_proxy_app_stream unique (app, stream) +); + +create table wvp_stream_push ( + id serial primary key, + app character varying(255), + stream character varying(255), + total_reader_count character varying(50), + origin_type integer, + origin_type_str character varying(50), + create_time character varying(50), + alive_second integer, + media_server_id character varying(50), + server_id character varying(50), + push_time character varying(50), + status bool default false, + update_time character varying(50), + push_ing bool default false, + self bool default false, + constraint uk_stream_push_app_stream unique (app, stream) +); +create table wvp_cloud_record ( + id serial primary key, + app character varying(255), + stream character varying(255), + call_id character varying(255), + start_time int8, + end_time int8, + media_server_id character varying(50), + file_name character varying(255), + folder character varying(255), + file_path character varying(255), + collect bool default false, + file_size int8, + time_len int8, + constraint uk_stream_push_app_stream_path unique (app, stream, file_path) +); + +create table wvp_user ( + id serial primary key, + username character varying(255), + password character varying(255), + role_id integer, + create_time character varying(50), + update_time character varying(50), + push_key character varying(50), + constraint uk_user_username unique (username) +); + +create table wvp_user_role ( + id serial primary key, + name character varying(50), + authority character varying(50), + create_time character varying(50), + update_time character varying(50) +); +create table wvp_resources_tree ( + id serial primary key , + is_catalog bool default true, + device_channel_id integer , + gb_stream_id integer, + name character varying(255), + parentId integer, + path character varying(255) +); + + +/*初始数据*/ +INSERT INTO wvp_user VALUES (1, 'admin','21232f297a57a5a743894a0e4a801fc3',1,'2021-04-13 14:14:57','2021-04-13 14:14:57','3e80d1762a324d5b0ff636e0bd16f1e3'); +INSERT INTO wvp_user_role VALUES (1, 'admin','0','2021-04-13 14:14:57','2021-04-13 14:14:57'); + + + diff --git a/数据库/2.6.9/更新-mysql-2.6.9.sql b/数据库/2.6.9/更新-mysql-2.6.9.sql new file mode 100644 index 0000000..735e76d --- /dev/null +++ b/数据库/2.6.9/更新-mysql-2.6.9.sql @@ -0,0 +1,503 @@ + +alter table device + change deviceId device_id varchar(50) not null; + +alter table device + change streamMode stream_mode varchar(50) null; + +alter table device + change registerTime register_time varchar(50) null; + +alter table device + change keepaliveTime keepalive_time varchar(50) null; + +alter table device + change createTime create_time varchar(50) not null; + +alter table device + change updateTime update_time varchar(50) not null; + +alter table device + change subscribeCycleForCatalog subscribe_cycle_for_catalog bool default false; + +alter table device + change subscribeCycleForMobilePosition subscribe_cycle_for_mobile_position bool default false; + +alter table device + change mobilePositionSubmissionInterval mobile_position_submission_interval int default 5 not null; + +alter table device + change subscribeCycleForAlarm subscribe_cycle_for_alarm bool default false; + +alter table device + change hostAddress host_address varchar(50) null; + +alter table device + change ssrcCheck ssrc_check bool default false; + +alter table device + change geoCoordSys geo_coord_sys varchar(50) not null; + +alter table device +drop column treeType; + +alter table device + change mediaServerId media_server_id varchar(50) default 'auto' null; + +alter table device + change sdpIp sdp_ip varchar(50) null; + +alter table device + change localIp local_ip varchar(50) null; + +alter table device + change asMessageChannel as_message_channel bool default false; + +alter table device + change keepaliveIntervalTime keepalive_interval_time int null; + +alter table device + change online on_line varchar(50) null; + +alter table device + add COLUMN switch_primary_sub_stream bool default false comment '开启主子码流切换的开关(0-不开启,1-开启)现在已知支持设备为 大华、TP——LINK全系设备'; + +alter table device_alarm + change deviceId device_id varchar(50) not null; + +alter table device_alarm + change channelId channel_id varchar(50) not null; + +alter table device_alarm + change alarmPriority alarm_priority varchar(50) not null; + +alter table device_alarm + change alarmMethod alarm_method varchar(50) null; + +alter table device_alarm + change alarmTime alarm_time varchar(50) not null; + +alter table device_alarm + change alarmDescription alarm_description varchar(255) null; + +alter table device_alarm + change alarmType alarm_type varchar(50) null; + +alter table device_alarm + change createTime create_time varchar(50) null; + +alter table device_channel + change channelId channel_id varchar(50) not null; + +alter table device_channel + change civilCode civil_code varchar(50) null; + +alter table device_channel + change parentId parent_id varchar(50) null; + +alter table device_channel + change safetyWay safety_way int null; + +alter table device_channel + change registerWay register_way int null; + +alter table device_channel + change certNum cert_num varchar(50) null; + +alter table device_channel + change errCode err_code int null; + +alter table device_channel + change endTime end_time varchar(50) null; + +alter table device_channel + change ipAddress ip_address varchar(50) null; + +alter table device_channel + change PTZType ptz_type int null; + +alter table device_channel + change status status bool default false; + +alter table device_channel + change streamId stream_id varchar(255) null; + +alter table device_channel + change deviceId device_id varchar(50) not null; + + +alter table device_channel + change hasAudio has_audio bool default false; + +alter table device_channel + change createTime create_time varchar(50) not null; + +alter table device_channel + change updateTime update_time varchar(50) not null; + +alter table device_channel + change subCount sub_count int default 0 null; + +alter table device_channel + change longitudeGcj02 longitude_gcj02 double null; + +alter table device_channel + change latitudeGcj02 latitude_gcj02 double null; + +alter table device_channel + change longitudeWgs84 longitude_wgs84 double null; + +alter table device_channel + change latitudeWgs84 latitude_wgs84 double null; + +alter table device_channel + change businessGroupId business_group_id varchar(50) null; + +alter table device_channel + change gpsTime gps_time varchar(50) null; + +alter table device_mobile_position + change deviceId device_id varchar(50) not null; + +alter table device_mobile_position + change channelId channel_id varchar(50) not null; + +alter table device_mobile_position + change deviceName device_name varchar(255) null; + +alter table device_mobile_position + change reportSource report_source varchar(50) null; + +alter table device_mobile_position + change longitudeGcj02 longitude_gcj02 double null; + +alter table device_mobile_position + change latitudeGcj02 latitude_gcj02 double null; + +alter table device_mobile_position + change longitudeWgs84 longitude_wgs84 double null; + +alter table device_mobile_position + change latitudeWgs84 latitude_wgs84 double null; + +alter table device_mobile_position + change createTime create_time varchar(50) null; + +alter table gb_stream + change gbStreamId gb_stream_id int auto_increment; + +alter table gb_stream + change gbId gb_id varchar(50) not null; + +alter table gb_stream + change streamType stream_type varchar(50) null; + +alter table gb_stream + change mediaServerId media_server_id varchar(50) null; + +alter table gb_stream + change createTime create_time varchar(50) null; + +alter table log + change createTime create_time varchar(50) not null; + +alter table media_server + change hookIp hook_ip varchar(50) not null; + +alter table media_server + add column send_rtp_port_range varchar(50) default null; + +alter table media_server + change sdpIp sdp_ip varchar(50) not null; + +alter table media_server + change streamIp stream_ip varchar(50) not null; + +alter table media_server + change httpPort http_port int not null; + +alter table media_server + change httpSSlPort http_ssl_port int not null; + +alter table media_server + change rtmpPort rtmp_port int not null; + +alter table media_server + change rtmpSSlPort rtmp_ssl_port int not null; + +alter table media_server + change rtpProxyPort rtp_proxy_port int not null; + +alter table media_server + change rtspPort rtsp_port int not null; + +alter table media_server + change rtspSSLPort rtsp_ssl_port int not null; + +alter table media_server + change autoConfig auto_config bool default true; + +alter table media_server + change rtpEnable rtp_enable bool default false; + +alter table media_server + change rtpPortRange rtp_port_range varchar(50) not null; + +alter table media_server + change recordAssistPort record_assist_port int not null; + +alter table media_server + change defaultServer default_server bool default false; + +alter table media_server + change createTime create_time varchar(50) not null; + +alter table media_server + change updateTime update_time varchar(50) not null; + +alter table media_server + change hookAliveInterval hook_alive_interval int not null; + +alter table parent_platform + change serverGBId server_gb_id varchar(50) not null; + +alter table parent_platform + change serverGBDomain server_gb_domain varchar(50) null; + +alter table parent_platform + change serverIP server_ip varchar(50) null; + +alter table parent_platform + change serverPort server_port int null; + +alter table parent_platform + change deviceGBId device_gb_id varchar(50) not null; + +alter table parent_platform + change deviceIp device_ip varchar(50) null; + +alter table parent_platform + change devicePort device_port varchar(50) null; + +alter table parent_platform + change keepTimeout keep_timeout varchar(50) null; + +alter table parent_platform + change characterSet character_set varchar(50) null; + +alter table parent_platform + change catalogId catalog_id varchar(50) not null; + +alter table parent_platform + change startOfflinePush start_offline_push bool default false; + +alter table parent_platform + change administrativeDivision administrative_division varchar(50) not null; + +alter table parent_platform + change catalogGroup catalog_group int default 1 null; + +alter table parent_platform + change createTime create_time varchar(50) null; + +alter table parent_platform + change updateTime update_time varchar(50) null; + +alter table parent_platform +drop column treeType; + +alter table parent_platform + change asMessageChannel as_message_channel bool default false; + +alter table parent_platform + change enable enable bool default false; + +alter table parent_platform + change ptz ptz bool default false; + +alter table parent_platform + change rtcp rtcp bool default false; + +alter table parent_platform + change status status bool default false; + +alter table parent_platform + change status status bool default false; + +alter table platform_catalog + change platformId platform_id varchar(50) not null; + +alter table platform_catalog + change parentId parent_id varchar(50) null; + +alter table platform_catalog + change civilCode civil_code varchar(50) null; + +alter table platform_catalog + change businessGroupId business_group_id varchar(50) null; + +alter table platform_gb_channel + change platformId platform_id varchar(50) not null; + +alter table platform_gb_channel + change catalogId catalog_id varchar(50) not null; + +alter table platform_gb_channel + change deviceChannelId device_channel_id int not null; + +alter table platform_gb_stream + change platformId platform_id varchar(50) not null; + +alter table platform_gb_stream + change catalogId catalog_id varchar(50) not null; + +alter table platform_gb_stream + change gbStreamId gb_stream_id int not null; + +alter table stream_proxy + change mediaServerId media_server_id varchar(50) null; + +alter table stream_proxy + change createTime create_time varchar(50) not null; + +alter table stream_proxy + change updateTime update_time varchar(50) null; + +alter table stream_proxy + change enable_remove_none_reader enable_remove_none_reader bool default false; + +alter table stream_proxy + change enable_disable_none_reader enable_disable_none_reader bool default false; + +alter table stream_proxy + change enable_audio enable_audio bool default false; + +alter table stream_proxy + change enable_mp4 enable_mp4 bool default false; + +alter table stream_proxy + change enable enable bool default false; + +alter table stream_push + change totalReaderCount total_reader_count varchar(50) null; + +alter table stream_push + change originType origin_type int null; + +alter table stream_push + change originTypeStr origin_type_str varchar(50) null; + +alter table stream_push + change createTime create_time varchar(50) null; + +alter table stream_push + change aliveSecond alive_second int null; + +alter table stream_push + change mediaServerId media_server_id varchar(50) null; + +alter table stream_push + change status status bool default false; + +alter table stream_push + change pushTime push_time varchar(50) null; + +alter table stream_push + change updateTime update_time varchar(50) null; + +alter table stream_push + change pushIng push_ing bool default false; + +alter table stream_push + change status status bool default false; + +alter table stream_push + change self self bool default false; + +alter table stream_push +drop column serverId; + + +alter table user + change roleId role_id int not null; + +alter table user + change createTime create_time varchar(50) not null; + +alter table user + change updateTime update_time varchar(50) not null; + +alter table user + change pushKey push_key varchar(50) null; + +alter table user_role + change createTime create_time varchar(50) not null; + +alter table user_role + change updateTime update_time varchar(50) not null; + +rename table device to wvp_device; +rename table device_alarm to wvp_device_alarm; +rename table device_channel to wvp_device_channel; +rename table device_mobile_position to wvp_device_mobile_position; +rename table gb_stream to wvp_gb_stream; +rename table log to wvp_log; +rename table media_server to wvp_media_server; +rename table parent_platform to wvp_platform; +rename table platform_catalog to wvp_platform_catalog; +rename table platform_gb_channel to wvp_platform_gb_channel; +rename table platform_gb_stream to wvp_platform_gb_stream; +rename table stream_proxy to wvp_stream_proxy; +rename table stream_push to wvp_stream_push; +rename table user to wvp_user; +rename table user_role to wvp_user_role; + +alter table wvp_device add column broadcast_push_after_ack bool default false; +alter table wvp_device_channel add column custom_name varchar(255) null ; +alter table wvp_device_channel add column custom_longitude double null ; +alter table wvp_device_channel add column custom_latitude double null ; +alter table wvp_device_channel add column custom_ptz_type int null ; + +create table wvp_resources_tree ( + id serial primary key , + is_catalog bool default true, + device_channel_id integer , + gb_stream_id integer, + name character varying(255), + parentId integer, + path character varying(255) +); + +alter table wvp_platform + add auto_push_channel bool default false; + +alter table wvp_stream_proxy + add stream_key character varying(255); + +create table wvp_cloud_record ( + id serial primary key, + app character varying(255), + stream character varying(255), + call_id character varying(255), + start_time bigint, + end_time bigint, + media_server_id character varying(50), + file_name character varying(255), + folder character varying(255), + file_path character varying(255), + collect bool default false, + file_size bigint, + time_len bigint, + constraint uk_stream_push_app_stream_path unique (app, stream, file_path) +); + +alter table wvp_media_server + add record_path character varying(255); + +alter table wvp_media_server + add record_day integer default 7; + +alter table wvp_stream_push + add server_id character varying(50); + + diff --git a/数据库/2.6.9/更新-postgresql-kingbase-2.6.9.sql b/数据库/2.6.9/更新-postgresql-kingbase-2.6.9.sql new file mode 100644 index 0000000..bbee04e --- /dev/null +++ b/数据库/2.6.9/更新-postgresql-kingbase-2.6.9.sql @@ -0,0 +1,505 @@ + +alter table device + change deviceId device_id varchar(50) not null; + +alter table device + change streamMode stream_mode varchar(50) null; + +alter table device + change registerTime register_time varchar(50) null; + +alter table device + change keepaliveTime keepalive_time varchar(50) null; + +alter table device + change createTime create_time varchar(50) not null; + +alter table device + change updateTime update_time varchar(50) not null; + +alter table device + change subscribeCycleForCatalog subscribe_cycle_for_catalog bool default false; + +alter table device + change subscribeCycleForMobilePosition subscribe_cycle_for_mobile_position bool default false; + +alter table device + change mobilePositionSubmissionInterval mobile_position_submission_interval int default 5 not null; + +alter table device + change subscribeCycleForAlarm subscribe_cycle_for_alarm bool default false; + +alter table device + change hostAddress host_address varchar(50) null; + +alter table device + change ssrcCheck ssrc_check bool default false; + +alter table device + change geoCoordSys geo_coord_sys varchar(50) not null; + +alter table device +drop column treeType; + +alter table device + change mediaServerId media_server_id varchar(50) default 'auto' null; + +alter table device + change sdpIp sdp_ip varchar(50) null; + +alter table device + change localIp local_ip varchar(50) null; + +alter table device + change asMessageChannel as_message_channel bool default false; + +alter table device + change keepaliveIntervalTime keepalive_interval_time int null; + +alter table device + change online on_line varchar(50) null; + +alter table device + add COLUMN switch_primary_sub_stream bool default false comment '开启主子码流切换的开关(0-不开启,1-开启)现在已知支持设备为 大华、TP——LINK全系设备' + + +alter table device_alarm + change deviceId device_id varchar(50) not null; + +alter table device_alarm + change channelId channel_id varchar(50) not null; + +alter table device_alarm + change alarmPriority alarm_priority varchar(50) not null; + +alter table device_alarm + change alarmMethod alarm_method varchar(50) null; + +alter table device_alarm + change alarmTime alarm_time varchar(50) not null; + +alter table device_alarm + change alarmDescription alarm_description varchar(255) null; + +alter table device_alarm + change alarmType alarm_type varchar(50) null; + +alter table device_alarm + change createTime create_time varchar(50) null; + +alter table device_channel + change channelId channel_id varchar(50) not null; + +alter table device_channel + change civilCode civil_code varchar(50) null; + +alter table device_channel + change parentId parent_id varchar(50) null; + +alter table device_channel + change safetyWay safety_way int null; + +alter table device_channel + change registerWay register_way int null; + +alter table device_channel + change certNum cert_num varchar(50) null; + +alter table device_channel + change errCode err_code int null; + +alter table device_channel + change endTime end_time varchar(50) null; + +alter table device_channel + change ipAddress ip_address varchar(50) null; + +alter table device_channel + change PTZType ptz_type int null; + +alter table device_channel + change status status bool default false; + +alter table device_channel + change streamId stream_id varchar(255) null; + +alter table device_channel + change deviceId device_id varchar(50) not null; + + +alter table device_channel + change hasAudio has_audio bool default false; + +alter table device_channel + change createTime create_time varchar(50) not null; + +alter table device_channel + change updateTime update_time varchar(50) not null; + +alter table device_channel + change subCount sub_count int default 0 null; + +alter table device_channel + change longitudeGcj02 longitude_gcj02 double null; + +alter table device_channel + change latitudeGcj02 latitude_gcj02 double null; + +alter table device_channel + change longitudeWgs84 longitude_wgs84 double null; + +alter table device_channel + change latitudeWgs84 latitude_wgs84 double null; + +alter table device_channel + change businessGroupId business_group_id varchar(50) null; + +alter table device_channel + change gpsTime gps_time varchar(50) null; + +alter table device_mobile_position + change deviceId device_id varchar(50) not null; + +alter table device_mobile_position + change channelId channel_id varchar(50) not null; + +alter table device_mobile_position + change deviceName device_name varchar(255) null; + +alter table device_mobile_position + change reportSource report_source varchar(50) null; + +alter table device_mobile_position + change longitudeGcj02 longitude_gcj02 double null; + +alter table device_mobile_position + change latitudeGcj02 latitude_gcj02 double null; + +alter table device_mobile_position + change longitudeWgs84 longitude_wgs84 double null; + +alter table device_mobile_position + change latitudeWgs84 latitude_wgs84 double null; + +alter table device_mobile_position + change createTime create_time varchar(50) null; + +alter table gb_stream + change gbStreamId gb_stream_id int auto_increment; + +alter table gb_stream + change gbId gb_id varchar(50) not null; + +alter table gb_stream + change streamType stream_type varchar(50) null; + +alter table gb_stream + change mediaServerId media_server_id varchar(50) null; + +alter table gb_stream + change createTime create_time varchar(50) null; + +alter table log + change createTime create_time varchar(50) not null; + +alter table media_server + change hookIp hook_ip varchar(50) not null; + +alter table media_server + add column send_rtp_port_range varchar(50) default null; + +alter table media_server + change sdpIp sdp_ip varchar(50) not null; + +alter table media_server + change streamIp stream_ip varchar(50) not null; + +alter table media_server + change httpPort http_port int not null; + +alter table media_server + change httpSSlPort http_ssl_port int not null; + +alter table media_server + change rtmpPort rtmp_port int not null; + +alter table media_server + change rtmpSSlPort rtmp_ssl_port int not null; + +alter table media_server + change rtpProxyPort rtp_proxy_port int not null; + +alter table media_server + change rtspPort rtsp_port int not null; + +alter table media_server + change rtspSSLPort rtsp_ssl_port int not null; + +alter table media_server + change autoConfig auto_config bool default true; + +alter table media_server + change rtpEnable rtp_enable bool default false; + +alter table media_server + change rtpPortRange rtp_port_range varchar(50) not null; + +alter table media_server + change recordAssistPort record_assist_port int not null; + +alter table media_server + change defaultServer default_server bool default false; + +alter table media_server + change createTime create_time varchar(50) not null; + +alter table media_server + change updateTime update_time varchar(50) not null; + +alter table media_server + change hookAliveInterval hook_alive_interval int not null; + +alter table parent_platform + change serverGBId server_gb_id varchar(50) not null; + +alter table parent_platform + change serverGBDomain server_gb_domain varchar(50) null; + +alter table parent_platform + change serverIP server_ip varchar(50) null; + +alter table parent_platform + change serverPort server_port int null; + +alter table parent_platform + change deviceGBId device_gb_id varchar(50) not null; + +alter table parent_platform + change deviceIp device_ip varchar(50) null; + +alter table parent_platform + change devicePort device_port varchar(50) null; + +alter table parent_platform + change keepTimeout keep_timeout varchar(50) null; + +alter table parent_platform + change characterSet character_set varchar(50) null; + +alter table parent_platform + change catalogId catalog_id varchar(50) not null; + +alter table parent_platform + change startOfflinePush start_offline_push bool default false; + +alter table parent_platform + change administrativeDivision administrative_division varchar(50) not null; + +alter table parent_platform + change catalogGroup catalog_group int default 1 null; + +alter table parent_platform + change createTime create_time varchar(50) null; + +alter table parent_platform + change updateTime update_time varchar(50) null; + +alter table parent_platform +drop column treeType; + +alter table parent_platform + change asMessageChannel as_message_channel bool default false; + +alter table parent_platform + change enable enable bool default false; + +alter table parent_platform + change ptz ptz bool default false; + +alter table parent_platform + change rtcp rtcp bool default false; + +alter table parent_platform + change status status bool default false; + +alter table parent_platform + change status status bool default false; + +alter table platform_catalog + change platformId platform_id varchar(50) not null; + +alter table platform_catalog + change parentId parent_id varchar(50) null; + +alter table platform_catalog + change civilCode civil_code varchar(50) null; + +alter table platform_catalog + change businessGroupId business_group_id varchar(50) null; + +alter table platform_gb_channel + change platformId platform_id varchar(50) not null; + +alter table platform_gb_channel + change catalogId catalog_id varchar(50) not null; + +alter table platform_gb_channel + change deviceChannelId device_channel_id int not null; + +alter table platform_gb_stream + change platformId platform_id varchar(50) not null; + +alter table platform_gb_stream + change catalogId catalog_id varchar(50) not null; + +alter table platform_gb_stream + change gbStreamId gb_stream_id int not null; + +alter table stream_proxy + change mediaServerId media_server_id varchar(50) null; + +alter table stream_proxy + change createTime create_time varchar(50) not null; + +alter table stream_proxy + change updateTime update_time varchar(50) null; + +alter table stream_proxy + change enable_remove_none_reader enable_remove_none_reader bool default false; + +alter table stream_proxy + change enable_disable_none_reader enable_disable_none_reader bool default false; + +alter table stream_proxy + change enable_audio enable_audio bool default false; + +alter table stream_proxy + change enable_mp4 enable_mp4 bool default false; + +alter table stream_proxy + change enable enable bool default false; + +alter table stream_push + change totalReaderCount total_reader_count varchar(50) null; + +alter table stream_push + change originType origin_type int null; + +alter table stream_push + change originTypeStr origin_type_str varchar(50) null; + +alter table stream_push + change createTime create_time varchar(50) null; + +alter table stream_push + change aliveSecond alive_second int null; + +alter table stream_push + change mediaServerId media_server_id varchar(50) null; + +alter table stream_push + change status status bool default false; + +alter table stream_push + change pushTime push_time varchar(50) null; + +alter table stream_push + change updateTime update_time varchar(50) null; + +alter table stream_push + change pushIng push_ing bool default false; + +alter table stream_push + change status status bool default false; + +alter table stream_push + change self self bool default false; + +alter table stream_push +drop column serverId; + + +alter table user + change roleId role_id int not null; + +alter table user + change createTime create_time varchar(50) not null; + +alter table user + change updateTime update_time varchar(50) not null; + +alter table user + change pushKey push_key varchar(50) null; + +alter table user_role + change createTime create_time varchar(50) not null; + +alter table user_role + change updateTime update_time varchar(50) not null; + +rename table device to wvp_device; +rename table device_alarm to wvp_device_alarm; +rename table device_channel to wvp_device_channel; +rename table device_mobile_position to wvp_device_mobile_position; +rename table gb_stream to wvp_gb_stream; +rename table log to wvp_log; +rename table media_server to wvp_media_server; +rename table parent_platform to wvp_platform; +rename table platform_catalog to wvp_platform_catalog; +rename table platform_gb_channel to wvp_platform_gb_channel; +rename table platform_gb_stream to wvp_platform_gb_stream; +rename table stream_proxy to wvp_stream_proxy; +rename table stream_push to wvp_stream_push; +rename table user to wvp_user; +rename table user_role to wvp_user_role; + +alter table wvp_device add column broadcast_push_after_ack bool default false; +alter table wvp_device_channel add column custom_name varchar(255) null ; +alter table wvp_device_channel add column custom_longitude double null ; +alter table wvp_device_channel add column custom_latitude double null ; +alter table wvp_device_channel add column custom_ptz_type int null ; + +create table wvp_resources_tree ( + id serial primary key , + is_catalog bool default true, + device_channel_id integer , + gb_stream_id integer, + name character varying(255), + parentId integer, + path character varying(255) +); + +alter table wvp_platform + add auto_push_channel bool default false; + +alter table wvp_stream_proxy + add stream_key character varying(255); + +create table wvp_cloud_record ( + id serial primary key, + app character varying(255), + stream character varying(255), + call_id character varying(255), + start_time int8, + end_time int8, + media_server_id character varying(50), + file_name character varying(255), + folder character varying(255), + file_path character varying(255), + collect bool default false, + file_size int8, + time_len int8, + constraint uk_stream_push_app_stream_path unique (app, stream, file_path) +); + +alter table wvp_media_server + add record_path character varying(255); + +alter table wvp_media_server + add record_day integer default 7; + +alter table wvp_stream_push + add server_id character varying(50); + + + diff --git a/数据库/2.7.0/初始化-mysql-2.7.0.sql b/数据库/2.7.0/初始化-mysql-2.7.0.sql new file mode 100644 index 0000000..147ed63 --- /dev/null +++ b/数据库/2.7.0/初始化-mysql-2.7.0.sql @@ -0,0 +1,335 @@ +/*建表*/ +create table wvp_device ( + id serial primary key , + device_id character varying(50) not null , + name character varying(255), + manufacturer character varying(255), + model character varying(255), + firmware character varying(255), + transport character varying(50), + stream_mode character varying(50), + on_line bool default false, + register_time character varying(50), + keepalive_time character varying(50), + ip character varying(50), + create_time character varying(50), + update_time character varying(50), + port integer, + expires integer, + subscribe_cycle_for_catalog integer DEFAULT 0, + subscribe_cycle_for_mobile_position integer DEFAULT 0, + mobile_position_submission_interval integer DEFAULT 5, + subscribe_cycle_for_alarm integer DEFAULT 0, + host_address character varying(50), + charset character varying(50), + ssrc_check bool default false, + geo_coord_sys character varying(50), + media_server_id character varying(50), + custom_name character varying(255), + sdp_ip character varying(50), + local_ip character varying(50), + password character varying(255), + as_message_channel bool default false, + keepalive_interval_time integer, + broadcast_push_after_ack bool default false, + constraint uk_device_device unique (device_id) +); + +create table wvp_device_alarm ( + id serial primary key , + device_id character varying(50) not null, + channel_id character varying(50) not null, + alarm_priority character varying(50), + alarm_method character varying(50), + alarm_time character varying(50), + alarm_description character varying(255), + longitude double precision, + latitude double precision, + alarm_type character varying(50), + create_time character varying(50) not null +); + +create table wvp_device_channel ( + id serial primary key , + channel_id character varying(50) not null, + name character varying(255), + custom_name character varying(255), + manufacture character varying(50), + model character varying(50), + owner character varying(50), + civil_code character varying(50), + block character varying(50), + address character varying(50), + parent_id character varying(50), + safety_way integer, + register_way integer, + cert_num character varying(50), + certifiable integer, + err_code integer, + end_time character varying(50), + secrecy character varying(50), + ip_address character varying(50), + port integer, + password character varying(255), + ptz_type integer, + custom_ptz_type integer, + status bool default false, + longitude double precision, + custom_longitude double precision, + latitude double precision, + custom_latitude double precision, + stream_id character varying(255), + device_id character varying(50) not null, + parental character varying(50), + has_audio bool default false, + create_time character varying(50) not null, + update_time character varying(50) not null, + sub_count integer, + longitude_gcj02 double precision, + latitude_gcj02 double precision, + longitude_wgs84 double precision, + latitude_wgs84 double precision, + business_group_id character varying(50), + gps_time character varying(50), + stream_identification character varying(50), + constraint uk_wvp_device_channel_unique_device_channel unique (device_id, channel_id) +); + +create table wvp_device_mobile_position ( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + device_name character varying(255), + time character varying(50), + longitude double precision, + latitude double precision, + altitude double precision, + speed double precision, + direction double precision, + report_source character varying(50), + longitude_gcj02 double precision, + latitude_gcj02 double precision, + longitude_wgs84 double precision, + latitude_wgs84 double precision, + create_time character varying(50) +); + +create table wvp_gb_stream ( + gb_stream_id serial primary key, + app character varying(255) not null, + stream character varying(255) not null, + gb_id character varying(50) not null, + name character varying(255), + longitude double precision, + latitude double precision, + stream_type character varying(50), + media_server_id character varying(50), + create_time character varying(50), + constraint uk_gb_stream_unique_gb_id unique (gb_id), + constraint uk_gb_stream_unique_app_stream unique (app, stream) +); + +create table wvp_log ( + id serial primary key , + name character varying(50), + type character varying(50), + uri character varying(200), + address character varying(50), + result character varying(50), + timing bigint, + username character varying(50), + create_time character varying(50) +); + +create table wvp_media_server ( + id character varying(255) primary key , + ip character varying(50), + hook_ip character varying(50), + sdp_ip character varying(50), + stream_ip character varying(50), + http_port integer, + http_ssl_port integer, + rtmp_port integer, + rtmp_ssl_port integer, + rtp_proxy_port integer, + rtsp_port integer, + rtsp_ssl_port integer, + auto_config bool default false, + secret character varying(50), + rtp_enable bool default false, + rtp_port_range character varying(50), + send_rtp_port_range character varying(50), + record_assist_port integer, + default_server bool default false, + create_time character varying(50), + update_time character varying(50), + hook_alive_interval integer, + record_path character varying(255), + record_day integer default 7, + constraint uk_media_server_unique_ip_http_port unique (ip, http_port) +); + +create table wvp_platform ( + id serial primary key , + enable bool default false, + name character varying(255), + server_gb_id character varying(50), + server_gb_domain character varying(50), + server_ip character varying(50), + server_port integer, + device_gb_id character varying(50), + device_ip character varying(50), + device_port character varying(50), + username character varying(255), + password character varying(50), + expires character varying(50), + keep_timeout character varying(50), + transport character varying(50), + character_set character varying(50), + catalog_id character varying(50), + ptz bool default false, + rtcp bool default false, + status bool default false, + start_offline_push bool default false, + administrative_division character varying(50), + catalog_group integer, + create_time character varying(50), + update_time character varying(50), + as_message_channel bool default false, + auto_push_channel bool default false, + send_stream_ip character varying(50), + constraint uk_platform_unique_server_gb_id unique (server_gb_id) +); + +create table wvp_platform_catalog ( + id character varying(50), + platform_id character varying(50), + name character varying(255), + parent_id character varying(50), + civil_code character varying(50), + business_group_id character varying(50), + constraint uk_platform_catalog_id_platform_id unique (id, platform_id) +); + +create table wvp_platform_gb_channel ( + id serial primary key , + platform_id character varying(50), + catalog_id character varying(50), + device_channel_id integer, + constraint uk_platform_gb_channel_platform_id_catalog_id_device_channel_id unique (platform_id, catalog_id, device_channel_id) +); + +create table wvp_platform_gb_stream ( + id serial primary key, + platform_id character varying(50), + catalog_id character varying(50), + gb_stream_id integer, + constraint uk_platform_gb_stream_platform_id_catalog_id_gb_stream_id unique (platform_id, catalog_id, gb_stream_id) +); + +create table wvp_stream_proxy ( + id serial primary key, + type character varying(50), + app character varying(255), + stream character varying(255), + url character varying(255), + src_url character varying(255), + dst_url character varying(255), + timeout_ms integer, + ffmpeg_cmd_key character varying(255), + rtp_type character varying(50), + media_server_id character varying(50), + enable_audio bool default false, + enable_mp4 bool default false, + enable bool default false, + status boolean, + enable_remove_none_reader bool default false, + create_time character varying(50), + name character varying(255), + update_time character varying(50), + stream_key character varying(255), + enable_disable_none_reader bool default false, + constraint uk_stream_proxy_app_stream unique (app, stream) +); + +create table wvp_stream_push ( + id serial primary key, + app character varying(255), + stream character varying(255), + total_reader_count character varying(50), + origin_type integer, + origin_type_str character varying(50), + create_time character varying(50), + alive_second integer, + media_server_id character varying(50), + server_id character varying(50), + push_time character varying(50), + status bool default false, + update_time character varying(50), + push_ing bool default false, + self bool default false, + constraint uk_stream_push_app_stream unique (app, stream) +); +create table wvp_cloud_record ( + id serial primary key, + app character varying(255), + stream character varying(255), + call_id character varying(255), + start_time bigint, + end_time bigint, + media_server_id character varying(50), + file_name character varying(255), + folder character varying(255), + file_path character varying(255), + collect bool default false, + file_size bigint, + time_len bigint, + constraint uk_stream_push_app_stream_path unique (app, stream, file_path) +); + +create table wvp_user ( + id serial primary key, + username character varying(255), + password character varying(255), + role_id integer, + create_time character varying(50), + update_time character varying(50), + push_key character varying(50), + constraint uk_user_username unique (username) +); + +create table wvp_user_role ( + id serial primary key, + name character varying(50), + authority character varying(50), + create_time character varying(50), + update_time character varying(50) +); +create table wvp_resources_tree ( + id serial primary key , + is_catalog bool default true, + device_channel_id integer , + gb_stream_id integer, + name character varying(255), + parentId integer, + path character varying(255) +); +create table wvp_user_api_key ( + id serial primary key , + user_id bigint, + app character varying(255) , + api_key text, + expired_at bigint, + remark character varying(255), + enable bool default true, + create_time character varying(50), + update_time character varying(50) +); + + +/*初始数据*/ +INSERT INTO wvp_user VALUES (1, 'admin','21232f297a57a5a743894a0e4a801fc3',1,'2021-04-13 14:14:57','2021-04-13 14:14:57','3e80d1762a324d5b0ff636e0bd16f1e3'); +INSERT INTO wvp_user_role VALUES (1, 'admin','0','2021-04-13 14:14:57','2021-04-13 14:14:57'); + + + diff --git a/数据库/2.7.0/初始化-postgresql-kingbase-2.7.0.sql b/数据库/2.7.0/初始化-postgresql-kingbase-2.7.0.sql new file mode 100644 index 0000000..790316d --- /dev/null +++ b/数据库/2.7.0/初始化-postgresql-kingbase-2.7.0.sql @@ -0,0 +1,334 @@ +/*建表*/ +create table wvp_device ( + id serial primary key , + device_id character varying(50) not null , + name character varying(255), + manufacturer character varying(255), + model character varying(255), + firmware character varying(255), + transport character varying(50), + stream_mode character varying(50), + on_line bool default false, + register_time character varying(50), + keepalive_time character varying(50), + ip character varying(50), + create_time character varying(50), + update_time character varying(50), + port integer, + expires integer, + subscribe_cycle_for_catalog integer DEFAULT 0, + subscribe_cycle_for_mobile_position integer DEFAULT 0, + mobile_position_submission_interval integer DEFAULT 5, + subscribe_cycle_for_alarm integer DEFAULT 0, + host_address character varying(50), + charset character varying(50), + ssrc_check bool default false, + geo_coord_sys character varying(50), + media_server_id character varying(50), + custom_name character varying(255), + sdp_ip character varying(50), + local_ip character varying(50), + password character varying(255), + as_message_channel bool default false, + keepalive_interval_time integer, + broadcast_push_after_ack bool default false, + constraint uk_device_device unique (device_id) +); + +create table wvp_device_alarm ( + id serial primary key , + device_id character varying(50) not null, + channel_id character varying(50) not null, + alarm_priority character varying(50), + alarm_method character varying(50), + alarm_time character varying(50), + alarm_description character varying(255), + longitude double precision, + latitude double precision, + alarm_type character varying(50), + create_time character varying(50) not null +); + +create table wvp_device_channel ( + id serial primary key , + channel_id character varying(50) not null, + name character varying(255), + custom_name character varying(255), + manufacture character varying(50), + model character varying(50), + owner character varying(50), + civil_code character varying(50), + block character varying(50), + address character varying(50), + parent_id character varying(50), + safety_way integer, + register_way integer, + cert_num character varying(50), + certifiable integer, + err_code integer, + end_time character varying(50), + secrecy character varying(50), + ip_address character varying(50), + port integer, + password character varying(255), + ptz_type integer, + custom_ptz_type integer, + status bool default false, + longitude double precision, + custom_longitude double precision, + latitude double precision, + custom_latitude double precision, + stream_id character varying(255), + device_id character varying(50) not null, + parental character varying(50), + has_audio bool default false, + create_time character varying(50) not null, + update_time character varying(50) not null, + sub_count integer, + longitude_gcj02 double precision, + latitude_gcj02 double precision, + longitude_wgs84 double precision, + latitude_wgs84 double precision, + business_group_id character varying(50), + gps_time character varying(50), + stream_identification character varying(50), + constraint uk_wvp_device_channel_unique_device_channel unique (device_id, channel_id) +); + +create table wvp_device_mobile_position ( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + device_name character varying(255), + time character varying(50), + longitude double precision, + latitude double precision, + altitude double precision, + speed double precision, + direction double precision, + report_source character varying(50), + longitude_gcj02 double precision, + latitude_gcj02 double precision, + longitude_wgs84 double precision, + latitude_wgs84 double precision, + create_time character varying(50) +); + +create table wvp_gb_stream ( + gb_stream_id serial primary key, + app character varying(255) not null, + stream character varying(255) not null, + gb_id character varying(50) not null, + name character varying(255), + longitude double precision, + latitude double precision, + stream_type character varying(50), + media_server_id character varying(50), + create_time character varying(50), + constraint uk_gb_stream_unique_gb_id unique (gb_id), + constraint uk_gb_stream_unique_app_stream unique (app, stream) +); + +create table wvp_log ( + id serial primary key , + name character varying(50), + type character varying(50), + uri character varying(200), + address character varying(50), + result character varying(50), + timing bigint, + username character varying(50), + create_time character varying(50) +); + +create table wvp_media_server ( + id character varying(255) primary key , + ip character varying(50), + hook_ip character varying(50), + sdp_ip character varying(50), + stream_ip character varying(50), + http_port integer, + http_ssl_port integer, + rtmp_port integer, + rtmp_ssl_port integer, + rtp_proxy_port integer, + rtsp_port integer, + rtsp_ssl_port integer, + auto_config bool default false, + secret character varying(50), + rtp_enable bool default false, + rtp_port_range character varying(50), + send_rtp_port_range character varying(50), + record_assist_port integer, + default_server bool default false, + create_time character varying(50), + update_time character varying(50), + hook_alive_interval integer, + record_path character varying(255), + record_day integer default 7, + constraint uk_media_server_unique_ip_http_port unique (ip, http_port) +); + +create table wvp_platform ( + id serial primary key , + enable bool default false, + name character varying(255), + server_gb_id character varying(50), + server_gb_domain character varying(50), + server_ip character varying(50), + server_port integer, + device_gb_id character varying(50), + device_ip character varying(50), + device_port character varying(50), + username character varying(255), + password character varying(50), + expires character varying(50), + keep_timeout character varying(50), + transport character varying(50), + character_set character varying(50), + catalog_id character varying(50), + ptz bool default false, + rtcp bool default false, + status bool default false, + start_offline_push bool default false, + administrative_division character varying(50), + catalog_group integer, + create_time character varying(50), + update_time character varying(50), + as_message_channel bool default false, + auto_push_channel bool default false, + send_stream_ip character varying(50), + constraint uk_platform_unique_server_gb_id unique (server_gb_id) +); + +create table wvp_platform_catalog ( + id character varying(50), + platform_id character varying(50), + name character varying(255), + parent_id character varying(50), + civil_code character varying(50), + business_group_id character varying(50), + constraint uk_platform_catalog_id_platform_id unique (id, platform_id) +); + +create table wvp_platform_gb_channel ( + id serial primary key , + platform_id character varying(50), + catalog_id character varying(50), + device_channel_id integer, + constraint uk_platform_gb_channel_platform_id_catalog_id_device_channel_id unique (platform_id, catalog_id, device_channel_id) +); + +create table wvp_platform_gb_stream ( + id serial primary key, + platform_id character varying(50), + catalog_id character varying(50), + gb_stream_id integer, + constraint uk_platform_gb_stream_platform_id_catalog_id_gb_stream_id unique (platform_id, catalog_id, gb_stream_id) +); + +create table wvp_stream_proxy ( + id serial primary key, + type character varying(50), + app character varying(255), + stream character varying(255), + url character varying(255), + src_url character varying(255), + dst_url character varying(255), + timeout_ms integer, + ffmpeg_cmd_key character varying(255), + rtp_type character varying(50), + media_server_id character varying(50), + enable_audio bool default false, + enable_mp4 bool default false, + enable bool default false, + status boolean, + enable_remove_none_reader bool default false, + create_time character varying(50), + name character varying(255), + update_time character varying(50), + stream_key character varying(255), + enable_disable_none_reader bool default false, + constraint uk_stream_proxy_app_stream unique (app, stream) +); + +create table wvp_stream_push ( + id serial primary key, + app character varying(255), + stream character varying(255), + total_reader_count character varying(50), + origin_type integer, + origin_type_str character varying(50), + create_time character varying(50), + alive_second integer, + media_server_id character varying(50), + server_id character varying(50), + push_time character varying(50), + status bool default false, + update_time character varying(50), + push_ing bool default false, + self bool default false, + constraint uk_stream_push_app_stream unique (app, stream) +); +create table wvp_cloud_record ( + id serial primary key, + app character varying(255), + stream character varying(255), + call_id character varying(255), + start_time int8, + end_time int8, + media_server_id character varying(50), + file_name character varying(255), + folder character varying(255), + file_path character varying(255), + collect bool default false, + file_size int8, + time_len int8, + constraint uk_stream_push_app_stream_path unique (app, stream, file_path) +); + +create table wvp_user ( + id serial primary key, + username character varying(255), + password character varying(255), + role_id integer, + create_time character varying(50), + update_time character varying(50), + push_key character varying(50), + constraint uk_user_username unique (username) +); + +create table wvp_user_role ( + id serial primary key, + name character varying(50), + authority character varying(50), + create_time character varying(50), + update_time character varying(50) +); +create table wvp_resources_tree ( + id serial primary key , + is_catalog bool default true, + device_channel_id integer , + gb_stream_id integer, + name character varying(255), + parentId integer, + path character varying(255) +); +create table wvp_user_api_key ( + id serial primary key , + user_id bigint, + app character varying(255) , + api_key text, + expired_at bigint, + remark character varying(255), + enable bool default true, + create_time character varying(50), + update_time character varying(50) +); + +/*初始数据*/ +INSERT INTO wvp_user VALUES (1, 'admin','21232f297a57a5a743894a0e4a801fc3',1,'2021-04-13 14:14:57','2021-04-13 14:14:57','3e80d1762a324d5b0ff636e0bd16f1e3'); +INSERT INTO wvp_user_role VALUES (1, 'admin','0','2021-04-13 14:14:57','2021-04-13 14:14:57'); + + + diff --git a/数据库/2.7.0/更新-mysql-2.7.0.sql b/数据库/2.7.0/更新-mysql-2.7.0.sql new file mode 100644 index 0000000..b14a5c8 --- /dev/null +++ b/数据库/2.7.0/更新-mysql-2.7.0.sql @@ -0,0 +1,18 @@ +alter table wvp_device_channel + add stream_identification character varying(50); + +alter table wvp_device + drop switch_primary_sub_stream; + +# 第一个补丁包 +alter table wvp_platform + add send_stream_ip character varying(50); + +alter table wvp_device + change on_line on_line bool default false; + +alter table wvp_device + change id id serial primary key; + +alter table wvp_device + change ssrc_check ssrc_check bool default false; \ No newline at end of file diff --git a/数据库/2.7.0/更新-postgresql-kingbase-2.7.0.sql b/数据库/2.7.0/更新-postgresql-kingbase-2.7.0.sql new file mode 100644 index 0000000..b14a5c8 --- /dev/null +++ b/数据库/2.7.0/更新-postgresql-kingbase-2.7.0.sql @@ -0,0 +1,18 @@ +alter table wvp_device_channel + add stream_identification character varying(50); + +alter table wvp_device + drop switch_primary_sub_stream; + +# 第一个补丁包 +alter table wvp_platform + add send_stream_ip character varying(50); + +alter table wvp_device + change on_line on_line bool default false; + +alter table wvp_device + change id id serial primary key; + +alter table wvp_device + change ssrc_check ssrc_check bool default false; \ No newline at end of file diff --git a/数据库/2.7.1/初始化-mysql-2.7.1.sql b/数据库/2.7.1/初始化-mysql-2.7.1.sql new file mode 100644 index 0000000..98d4340 --- /dev/null +++ b/数据库/2.7.1/初始化-mysql-2.7.1.sql @@ -0,0 +1,342 @@ +/*建表*/ +create table wvp_device ( + id serial primary key , + device_id character varying(50) not null , + name character varying(255), + manufacturer character varying(255), + model character varying(255), + firmware character varying(255), + transport character varying(50), + stream_mode character varying(50), + on_line bool default false, + register_time character varying(50), + keepalive_time character varying(50), + ip character varying(50), + create_time character varying(50), + update_time character varying(50), + port integer, + expires integer, + subscribe_cycle_for_catalog integer DEFAULT 0, + subscribe_cycle_for_mobile_position integer DEFAULT 0, + mobile_position_submission_interval integer DEFAULT 5, + subscribe_cycle_for_alarm integer DEFAULT 0, + host_address character varying(50), + charset character varying(50), + ssrc_check bool default false, + geo_coord_sys character varying(50), + media_server_id character varying(50), + custom_name character varying(255), + sdp_ip character varying(50), + local_ip character varying(50), + password character varying(255), + as_message_channel bool default false, + keepalive_interval_time integer, + broadcast_push_after_ack bool default false, + constraint uk_device_device unique (device_id) +); + +create table wvp_device_alarm ( + id serial primary key , + device_id character varying(50) not null, + channel_id character varying(50) not null, + alarm_priority character varying(50), + alarm_method character varying(50), + alarm_time character varying(50), + alarm_description character varying(255), + longitude double precision, + latitude double precision, + alarm_type character varying(50), + create_time character varying(50) not null +); + +create table wvp_device_channel ( + id serial primary key , + channel_id character varying(50) not null, + name character varying(255), + custom_name character varying(255), + manufacture character varying(50), + model character varying(50), + owner character varying(50), + civil_code character varying(50), + block character varying(50), + address character varying(50), + parent_id character varying(50), + safety_way integer, + register_way integer, + cert_num character varying(50), + certifiable integer, + err_code integer, + end_time character varying(50), + secrecy character varying(50), + ip_address character varying(50), + port integer, + password character varying(255), + ptz_type integer, + custom_ptz_type integer, + status bool default false, + longitude double precision, + custom_longitude double precision, + latitude double precision, + custom_latitude double precision, + stream_id character varying(255), + device_id character varying(50) not null, + parental character varying(50), + has_audio bool default false, + create_time character varying(50) not null, + update_time character varying(50) not null, + sub_count integer, + longitude_gcj02 double precision, + latitude_gcj02 double precision, + longitude_wgs84 double precision, + latitude_wgs84 double precision, + business_group_id character varying(50), + gps_time character varying(50), + stream_identification character varying(50), + constraint uk_wvp_device_channel_unique_device_channel unique (device_id, channel_id) +); + +create table wvp_device_mobile_position ( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + device_name character varying(255), + time character varying(50), + longitude double precision, + latitude double precision, + altitude double precision, + speed double precision, + direction double precision, + report_source character varying(50), + longitude_gcj02 double precision, + latitude_gcj02 double precision, + longitude_wgs84 double precision, + latitude_wgs84 double precision, + create_time character varying(50) +); + +create table wvp_gb_stream ( + gb_stream_id serial primary key, + app character varying(255) not null, + stream character varying(255) not null, + gb_id character varying(50) not null, + name character varying(255), + longitude double precision, + latitude double precision, + stream_type character varying(50), + media_server_id character varying(50), + create_time character varying(50), + constraint uk_gb_stream_unique_gb_id unique (gb_id), + constraint uk_gb_stream_unique_app_stream unique (app, stream) +); + +create table wvp_log ( + id serial primary key , + name character varying(50), + type character varying(50), + uri character varying(200), + address character varying(50), + result character varying(50), + timing bigint, + username character varying(50), + create_time character varying(50) +); + +create table wvp_media_server ( + id character varying(255) primary key , + ip character varying(50), + hook_ip character varying(50), + sdp_ip character varying(50), + stream_ip character varying(50), + http_port integer, + http_ssl_port integer, + rtmp_port integer, + rtmp_ssl_port integer, + rtp_proxy_port integer, + rtsp_port integer, + rtsp_ssl_port integer, + flv_port integer, + flv_ssl_port integer, + ws_flv_port integer, + ws_flv_ssl_port integer, + auto_config bool default false, + secret character varying(50), + type character varying(50) default 'zlm', + rtp_enable bool default false, + rtp_port_range character varying(50), + send_rtp_port_range character varying(50), + record_assist_port integer, + default_server bool default false, + create_time character varying(50), + update_time character varying(50), + hook_alive_interval integer, + record_path character varying(255), + record_day integer default 7, + transcode_suffix character varying(255), + constraint uk_media_server_unique_ip_http_port unique (ip, http_port) +); + +create table wvp_platform ( + id serial primary key , + enable bool default false, + name character varying(255), + server_gb_id character varying(50), + server_gb_domain character varying(50), + server_ip character varying(50), + server_port integer, + device_gb_id character varying(50), + device_ip character varying(50), + device_port character varying(50), + username character varying(255), + password character varying(50), + expires character varying(50), + keep_timeout character varying(50), + transport character varying(50), + character_set character varying(50), + catalog_id character varying(50), + ptz bool default false, + rtcp bool default false, + status bool default false, + start_offline_push bool default false, + administrative_division character varying(50), + catalog_group integer, + create_time character varying(50), + update_time character varying(50), + as_message_channel bool default false, + auto_push_channel bool default false, + send_stream_ip character varying(50), + constraint uk_platform_unique_server_gb_id unique (server_gb_id) +); + +create table wvp_platform_catalog ( + id character varying(50), + platform_id character varying(50), + name character varying(255), + parent_id character varying(50), + civil_code character varying(50), + business_group_id character varying(50), + constraint uk_platform_catalog_id_platform_id unique (id, platform_id) +); + +create table wvp_platform_gb_channel ( + id serial primary key , + platform_id character varying(50), + catalog_id character varying(50), + device_channel_id integer, + constraint uk_platform_gb_channel_platform_id_catalog_id_device_channel_id unique (platform_id, catalog_id, device_channel_id) +); + +create table wvp_platform_gb_stream ( + id serial primary key, + platform_id character varying(50), + catalog_id character varying(50), + gb_stream_id integer, + constraint uk_platform_gb_stream_platform_id_catalog_id_gb_stream_id unique (platform_id, catalog_id, gb_stream_id) +); + +create table wvp_stream_proxy ( + id serial primary key, + type character varying(50), + app character varying(255), + stream character varying(255), + url character varying(255), + src_url character varying(255), + dst_url character varying(255), + timeout_ms integer, + ffmpeg_cmd_key character varying(255), + rtp_type character varying(50), + media_server_id character varying(50), + enable_audio bool default false, + enable_mp4 bool default false, + enable bool default false, + status boolean, + enable_remove_none_reader bool default false, + create_time character varying(50), + name character varying(255), + update_time character varying(50), + stream_key character varying(255), + enable_disable_none_reader bool default false, + constraint uk_stream_proxy_app_stream unique (app, stream) +); + +create table wvp_stream_push ( + id serial primary key, + app character varying(255), + stream character varying(255), + total_reader_count character varying(50), + origin_type integer, + origin_type_str character varying(50), + create_time character varying(50), + alive_second integer, + media_server_id character varying(50), + server_id character varying(50), + push_time character varying(50), + status bool default false, + update_time character varying(50), + push_ing bool default false, + self bool default false, + constraint uk_stream_push_app_stream unique (app, stream) +); +create table wvp_cloud_record ( + id serial primary key, + app character varying(255), + stream character varying(255), + call_id character varying(255), + start_time bigint, + end_time bigint, + media_server_id character varying(50), + file_name character varying(255), + folder character varying(255), + file_path character varying(255), + collect bool default false, + file_size bigint, + time_len bigint, + constraint uk_stream_push_app_stream_path unique (app, stream, file_path) +); + +create table wvp_user ( + id serial primary key, + username character varying(255), + password character varying(255), + role_id integer, + create_time character varying(50), + update_time character varying(50), + push_key character varying(50), + constraint uk_user_username unique (username) +); + +create table wvp_user_role ( + id serial primary key, + name character varying(50), + authority character varying(50), + create_time character varying(50), + update_time character varying(50) +); +create table wvp_resources_tree ( + id serial primary key , + is_catalog bool default true, + device_channel_id integer , + gb_stream_id integer, + name character varying(255), + parentId integer, + path character varying(255) +); + +create table wvp_user_api_key ( + id serial primary key , + user_id bigint, + app character varying(255) , + api_key text, + expired_at bigint, + remark character varying(255), + enable bool default true, + create_time character varying(50), + update_time character varying(50) +); + + +/*初始数据*/ +INSERT INTO wvp_user VALUES (1, 'admin','21232f297a57a5a743894a0e4a801fc3',1,'2021-04-13 14:14:57','2021-04-13 14:14:57','3e80d1762a324d5b0ff636e0bd16f1e3'); +INSERT INTO wvp_user_role VALUES (1, 'admin','0','2021-04-13 14:14:57','2021-04-13 14:14:57'); + + + diff --git a/数据库/2.7.1/初始化-postgresql-kingbase-2.7.1.sql b/数据库/2.7.1/初始化-postgresql-kingbase-2.7.1.sql new file mode 100644 index 0000000..eb2bc83 --- /dev/null +++ b/数据库/2.7.1/初始化-postgresql-kingbase-2.7.1.sql @@ -0,0 +1,342 @@ +/*建表*/ +create table wvp_device ( + id serial primary key , + device_id character varying(50) not null , + name character varying(255), + manufacturer character varying(255), + model character varying(255), + firmware character varying(255), + transport character varying(50), + stream_mode character varying(50), + on_line bool default false, + register_time character varying(50), + keepalive_time character varying(50), + ip character varying(50), + create_time character varying(50), + update_time character varying(50), + port integer, + expires integer, + subscribe_cycle_for_catalog integer DEFAULT 0, + subscribe_cycle_for_mobile_position integer DEFAULT 0, + mobile_position_submission_interval integer DEFAULT 5, + subscribe_cycle_for_alarm integer DEFAULT 0, + host_address character varying(50), + charset character varying(50), + ssrc_check bool default false, + geo_coord_sys character varying(50), + media_server_id character varying(50), + custom_name character varying(255), + sdp_ip character varying(50), + local_ip character varying(50), + password character varying(255), + as_message_channel bool default false, + keepalive_interval_time integer, + broadcast_push_after_ack bool default false, + constraint uk_device_device unique (device_id) +); + +create table wvp_device_alarm ( + id serial primary key , + device_id character varying(50) not null, + channel_id character varying(50) not null, + alarm_priority character varying(50), + alarm_method character varying(50), + alarm_time character varying(50), + alarm_description character varying(255), + longitude double precision, + latitude double precision, + alarm_type character varying(50), + create_time character varying(50) not null +); + +create table wvp_device_channel ( + id serial primary key , + channel_id character varying(50) not null, + name character varying(255), + custom_name character varying(255), + manufacture character varying(50), + model character varying(50), + owner character varying(50), + civil_code character varying(50), + block character varying(50), + address character varying(50), + parent_id character varying(50), + safety_way integer, + register_way integer, + cert_num character varying(50), + certifiable integer, + err_code integer, + end_time character varying(50), + secrecy character varying(50), + ip_address character varying(50), + port integer, + password character varying(255), + ptz_type integer, + custom_ptz_type integer, + status bool default false, + longitude double precision, + custom_longitude double precision, + latitude double precision, + custom_latitude double precision, + stream_id character varying(255), + device_id character varying(50) not null, + parental character varying(50), + has_audio bool default false, + create_time character varying(50) not null, + update_time character varying(50) not null, + sub_count integer, + longitude_gcj02 double precision, + latitude_gcj02 double precision, + longitude_wgs84 double precision, + latitude_wgs84 double precision, + business_group_id character varying(50), + gps_time character varying(50), + stream_identification character varying(50), + constraint uk_wvp_device_channel_unique_device_channel unique (device_id, channel_id) +); + +create table wvp_device_mobile_position ( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + device_name character varying(255), + time character varying(50), + longitude double precision, + latitude double precision, + altitude double precision, + speed double precision, + direction double precision, + report_source character varying(50), + longitude_gcj02 double precision, + latitude_gcj02 double precision, + longitude_wgs84 double precision, + latitude_wgs84 double precision, + create_time character varying(50) +); + +create table wvp_gb_stream ( + gb_stream_id serial primary key, + app character varying(255) not null, + stream character varying(255) not null, + gb_id character varying(50) not null, + name character varying(255), + longitude double precision, + latitude double precision, + stream_type character varying(50), + media_server_id character varying(50), + create_time character varying(50), + constraint uk_gb_stream_unique_gb_id unique (gb_id), + constraint uk_gb_stream_unique_app_stream unique (app, stream) +); + +create table wvp_log ( + id serial primary key , + name character varying(50), + type character varying(50), + uri character varying(200), + address character varying(50), + result character varying(50), + timing bigint, + username character varying(50), + create_time character varying(50) +); + +create table wvp_media_server ( + id character varying(255) primary key , + ip character varying(50), + hook_ip character varying(50), + sdp_ip character varying(50), + stream_ip character varying(50), + http_port integer, + http_ssl_port integer, + rtmp_port integer, + rtmp_ssl_port integer, + rtp_proxy_port integer, + rtsp_port integer, + rtsp_ssl_port integer, + flv_port integer, + flv_ssl_port integer, + ws_flv_port integer, + ws_flv_ssl_port integer, + auto_config bool default false, + secret character varying(50), + type character varying(50) default 'zlm', + rtp_enable bool default false, + rtp_port_range character varying(50), + send_rtp_port_range character varying(50), + record_assist_port integer, + default_server bool default false, + create_time character varying(50), + update_time character varying(50), + hook_alive_interval integer, + record_path character varying(255), + record_day integer default 7, + transcode_suffix character varying(255), + constraint uk_media_server_unique_ip_http_port unique (ip, http_port) +); + +create table wvp_platform ( + id serial primary key , + enable bool default false, + name character varying(255), + server_gb_id character varying(50), + server_gb_domain character varying(50), + server_ip character varying(50), + server_port integer, + device_gb_id character varying(50), + device_ip character varying(50), + device_port character varying(50), + username character varying(255), + password character varying(50), + expires character varying(50), + keep_timeout character varying(50), + transport character varying(50), + character_set character varying(50), + catalog_id character varying(50), + ptz bool default false, + rtcp bool default false, + status bool default false, + start_offline_push bool default false, + administrative_division character varying(50), + catalog_group integer, + create_time character varying(50), + update_time character varying(50), + as_message_channel bool default false, + auto_push_channel bool default false, + send_stream_ip character varying(50), + constraint uk_platform_unique_server_gb_id unique (server_gb_id) +); + +create table wvp_platform_catalog ( + id character varying(50), + platform_id character varying(50), + name character varying(255), + parent_id character varying(50), + civil_code character varying(50), + business_group_id character varying(50), + constraint uk_platform_catalog_id_platform_id unique (id, platform_id) +); + +create table wvp_platform_gb_channel ( + id serial primary key , + platform_id character varying(50), + catalog_id character varying(50), + device_channel_id integer, + constraint uk_platform_gb_channel_platform_id_catalog_id_device_channel_id unique (platform_id, catalog_id, device_channel_id) +); + +create table wvp_platform_gb_stream ( + id serial primary key, + platform_id character varying(50), + catalog_id character varying(50), + gb_stream_id integer, + constraint uk_platform_gb_stream_platform_id_catalog_id_gb_stream_id unique (platform_id, catalog_id, gb_stream_id) +); + +create table wvp_stream_proxy ( + id serial primary key, + type character varying(50), + app character varying(255), + stream character varying(255), + url character varying(255), + src_url character varying(255), + dst_url character varying(255), + timeout_ms integer, + ffmpeg_cmd_key character varying(255), + rtp_type character varying(50), + media_server_id character varying(50), + enable_audio bool default false, + enable_mp4 bool default false, + enable bool default false, + status boolean, + enable_remove_none_reader bool default false, + create_time character varying(50), + name character varying(255), + update_time character varying(50), + stream_key character varying(255), + enable_disable_none_reader bool default false, + constraint uk_stream_proxy_app_stream unique (app, stream) +); + +create table wvp_stream_push ( + id serial primary key, + app character varying(255), + stream character varying(255), + total_reader_count character varying(50), + origin_type integer, + origin_type_str character varying(50), + create_time character varying(50), + alive_second integer, + media_server_id character varying(50), + server_id character varying(50), + push_time character varying(50), + status bool default false, + update_time character varying(50), + push_ing bool default false, + self bool default false, + constraint uk_stream_push_app_stream unique (app, stream) +); +create table wvp_cloud_record ( + id serial primary key, + app character varying(255), + stream character varying(255), + call_id character varying(255), + start_time int8, + end_time int8, + media_server_id character varying(50), + file_name character varying(255), + folder character varying(255), + file_path character varying(255), + collect bool default false, + file_size int8, + time_len int8, + constraint uk_stream_push_app_stream_path unique (app, stream, file_path) +); + +create table wvp_user ( + id serial primary key, + username character varying(255), + password character varying(255), + role_id integer, + create_time character varying(50), + update_time character varying(50), + push_key character varying(50), + constraint uk_user_username unique (username) +); + +create table wvp_user_role ( + id serial primary key, + name character varying(50), + authority character varying(50), + create_time character varying(50), + update_time character varying(50) +); +create table wvp_resources_tree ( + id serial primary key , + is_catalog bool default true, + device_channel_id integer , + gb_stream_id integer, + name character varying(255), + parentId integer, + path character varying(255) +); + +create table wvp_user_api_key ( + id serial primary key , + user_id bigint, + app character varying(255) , + api_key text, + expired_at bigint, + remark character varying(255), + enable bool default true, + create_time character varying(50), + update_time character varying(50) +); + + +/*初始数据*/ +INSERT INTO wvp_user VALUES (1, 'admin','21232f297a57a5a743894a0e4a801fc3',1,'2021-04-13 14:14:57','2021-04-13 14:14:57','3e80d1762a324d5b0ff636e0bd16f1e3'); +INSERT INTO wvp_user_role VALUES (1, 'admin','0','2021-04-13 14:14:57','2021-04-13 14:14:57'); + + + diff --git a/数据库/2.7.1/更新-mysql-2.7.1.sql b/数据库/2.7.1/更新-mysql-2.7.1.sql new file mode 100644 index 0000000..dff8570 --- /dev/null +++ b/数据库/2.7.1/更新-mysql-2.7.1.sql @@ -0,0 +1,26 @@ +alter table wvp_media_server + add transcode_suffix character varying(255); + +alter table wvp_media_server + add type character varying(50) default 'zlm'; + +alter table wvp_media_server + add flv_port integer; +alter table wvp_media_server + add flv_ssl_port integer; +alter table wvp_media_server + add ws_flv_port integer; +alter table wvp_media_server + add ws_flv_ssl_port integer; + +create table wvp_user_api_key ( + id serial primary key , + user_id bigint, + app character varying(255) , + api_key text, + expired_at bigint, + remark character varying(255), + enable bool default true, + create_time character varying(50), + update_time character varying(50) +); \ No newline at end of file diff --git a/数据库/2.7.1/更新-postgresql-kingbase-2.7.1.sql b/数据库/2.7.1/更新-postgresql-kingbase-2.7.1.sql new file mode 100644 index 0000000..dff8570 --- /dev/null +++ b/数据库/2.7.1/更新-postgresql-kingbase-2.7.1.sql @@ -0,0 +1,26 @@ +alter table wvp_media_server + add transcode_suffix character varying(255); + +alter table wvp_media_server + add type character varying(50) default 'zlm'; + +alter table wvp_media_server + add flv_port integer; +alter table wvp_media_server + add flv_ssl_port integer; +alter table wvp_media_server + add ws_flv_port integer; +alter table wvp_media_server + add ws_flv_ssl_port integer; + +create table wvp_user_api_key ( + id serial primary key , + user_id bigint, + app character varying(255) , + api_key text, + expired_at bigint, + remark character varying(255), + enable bool default true, + create_time character varying(50), + update_time character varying(50) +); \ No newline at end of file diff --git a/数据库/2.7.3/初始化-mysql-2.7.3.sql b/数据库/2.7.3/初始化-mysql-2.7.3.sql new file mode 100644 index 0000000..3d25e59 --- /dev/null +++ b/数据库/2.7.3/初始化-mysql-2.7.3.sql @@ -0,0 +1,466 @@ +/*建表*/ +drop table IF EXISTS wvp_device; +create table IF NOT EXISTS wvp_device +( + id serial primary key, + device_id character varying(50) not null, + name character varying(255), + manufacturer character varying(255), + model character varying(255), + firmware character varying(255), + transport character varying(50), + stream_mode character varying(50), + on_line bool default false, + register_time character varying(50), + keepalive_time character varying(50), + ip character varying(50), + create_time character varying(50), + update_time character varying(50), + port integer, + expires integer, + subscribe_cycle_for_catalog integer DEFAULT 0, + subscribe_cycle_for_mobile_position integer DEFAULT 0, + mobile_position_submission_interval integer DEFAULT 5, + subscribe_cycle_for_alarm integer DEFAULT 0, + host_address character varying(50), + charset character varying(50), + ssrc_check bool default false, + geo_coord_sys character varying(50), + media_server_id character varying(50) default 'auto', + custom_name character varying(255), + sdp_ip character varying(50), + local_ip character varying(50), + password character varying(255), + as_message_channel bool default false, + heart_beat_interval integer, + heart_beat_count integer, + position_capability integer, + broadcast_push_after_ack bool default false, + server_id character varying(50), + constraint uk_device_device unique (device_id) +); + +drop table IF EXISTS wvp_device_alarm; +create table IF NOT EXISTS wvp_device_alarm +( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + alarm_priority character varying(50), + alarm_method character varying(50), + alarm_time character varying(50), + alarm_description character varying(255), + longitude double precision, + latitude double precision, + alarm_type character varying(50), + create_time character varying(50) not null +); + +drop table IF EXISTS wvp_device_mobile_position; +create table IF NOT EXISTS wvp_device_mobile_position +( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + device_name character varying(255), + time character varying(50), + longitude double precision, + latitude double precision, + altitude double precision, + speed double precision, + direction double precision, + report_source character varying(50), + create_time character varying(50) +); + +drop table IF EXISTS wvp_device_channel; +create table IF NOT EXISTS wvp_device_channel +( + id serial primary key, + device_id character varying(50), + name character varying(255), + manufacturer character varying(50), + model character varying(50), + owner character varying(50), + civil_code character varying(50), + block character varying(50), + address character varying(50), + parental integer, + parent_id character varying(50), + safety_way integer, + register_way integer, + cert_num character varying(50), + certifiable integer, + err_code integer, + end_time character varying(50), + secrecy integer, + ip_address character varying(50), + port integer, + password character varying(255), + status character varying(50), + longitude double precision, + latitude double precision, + ptz_type integer, + position_type integer, + room_type integer, + use_type integer, + supply_light_type integer, + direction_type integer, + resolution character varying(255), + business_group_id character varying(255), + download_speed character varying(255), + svc_space_support_mod integer, + svc_time_support_mode integer, + create_time character varying(50) not null, + update_time character varying(50) not null, + sub_count integer, + stream_id character varying(255), + has_audio bool default false, + gps_time character varying(50), + stream_identification character varying(50), + channel_type int default 0 not null, + gb_device_id character varying(50), + gb_name character varying(255), + gb_manufacturer character varying(255), + gb_model character varying(255), + gb_owner character varying(255), + gb_civil_code character varying(255), + gb_block character varying(255), + gb_address character varying(255), + gb_parental integer, + gb_parent_id character varying(255), + gb_safety_way integer, + gb_register_way integer, + gb_cert_num character varying(50), + gb_certifiable integer, + gb_err_code integer, + gb_end_time character varying(50), + gb_secrecy integer, + gb_ip_address character varying(50), + gb_port integer, + gb_password character varying(50), + gb_status character varying(50), + gb_longitude double, + gb_latitude double, + gb_business_group_id character varying(50), + gb_ptz_type integer, + gb_position_type integer, + gb_room_type integer, + gb_use_type integer, + gb_supply_light_type integer, + gb_direction_type integer, + gb_resolution character varying(255), + gb_download_speed character varying(255), + gb_svc_space_support_mod integer, + gb_svc_time_support_mode integer, + record_plan_id integer, + data_type integer not null, + data_device_id integer not null, + gps_speed double precision, + gps_altitude double precision, + gps_direction double precision, + index (data_type), + index (data_device_id), + constraint uk_wvp_unique_channel unique (gb_device_id) +); + +drop table IF EXISTS wvp_media_server; +create table IF NOT EXISTS wvp_media_server +( + id character varying(255) primary key, + ip character varying(50), + hook_ip character varying(50), + sdp_ip character varying(50), + stream_ip character varying(50), + http_port integer, + http_ssl_port integer, + rtmp_port integer, + rtmp_ssl_port integer, + rtp_proxy_port integer, + rtsp_port integer, + rtsp_ssl_port integer, + flv_port integer, + flv_ssl_port integer, + ws_flv_port integer, + ws_flv_ssl_port integer, + auto_config bool default false, + secret character varying(50), + type character varying(50) default 'zlm', + rtp_enable bool default false, + rtp_port_range character varying(50), + send_rtp_port_range character varying(50), + record_assist_port integer, + default_server bool default false, + create_time character varying(50), + update_time character varying(50), + hook_alive_interval integer, + record_path character varying(255), + record_day integer default 7, + transcode_suffix character varying(255), + server_id character varying(50), + constraint uk_media_server_unique_ip_http_port unique (ip, http_port, server_id) +); + +drop table IF EXISTS wvp_platform; +create table IF NOT EXISTS wvp_platform +( + id serial primary key, + enable bool default false, + name character varying(255), + server_gb_id character varying(50), + server_gb_domain character varying(50), + server_ip character varying(50), + server_port integer, + device_gb_id character varying(50), + device_ip character varying(50), + device_port character varying(50), + username character varying(255), + password character varying(50), + expires character varying(50), + keep_timeout character varying(50), + transport character varying(50), + civil_code character varying(50), + manufacturer character varying(255), + model character varying(255), + address character varying(255), + character_set character varying(50), + ptz bool default false, + rtcp bool default false, + status bool default false, + catalog_group integer, + register_way integer, + secrecy integer, + create_time character varying(50), + update_time character varying(50), + as_message_channel bool default false, + catalog_with_platform integer default 1, + catalog_with_group integer default 1, + catalog_with_region integer default 1, + auto_push_channel bool default true, + send_stream_ip character varying(50), + server_id character varying(50), + constraint uk_platform_unique_server_gb_id unique (server_gb_id) +); + +drop table IF EXISTS wvp_platform_channel; +create table IF NOT EXISTS wvp_platform_channel +( + id serial primary key, + platform_id integer, + device_channel_id integer, + custom_device_id character varying(50), + custom_name character varying(255), + custom_manufacturer character varying(50), + custom_model character varying(50), + custom_owner character varying(50), + custom_civil_code character varying(50), + custom_block character varying(50), + custom_address character varying(50), + custom_parental integer, + custom_parent_id character varying(50), + custom_safety_way integer, + custom_register_way integer, + custom_cert_num character varying(50), + custom_certifiable integer, + custom_err_code integer, + custom_end_time character varying(50), + custom_secrecy integer, + custom_ip_address character varying(50), + custom_port integer, + custom_password character varying(255), + custom_status character varying(50), + custom_longitude double precision, + custom_latitude double precision, + custom_ptz_type integer, + custom_position_type integer, + custom_room_type integer, + custom_use_type integer, + custom_supply_light_type integer, + custom_direction_type integer, + custom_resolution character varying(255), + custom_business_group_id character varying(255), + custom_download_speed character varying(255), + custom_svc_space_support_mod integer, + custom_svc_time_support_mode integer, + constraint uk_platform_gb_channel_platform_id_catalog_id_device_channel_id unique (platform_id, device_channel_id), + constraint uk_platform_gb_channel_device_id unique (custom_device_id) +); + +drop table IF EXISTS wvp_platform_group; +create table IF NOT EXISTS wvp_platform_group +( + id serial primary key, + platform_id integer, + group_id integer, + constraint uk_wvp_platform_group_platform_id_group_id unique (platform_id, group_id) +); + +drop table IF EXISTS wvp_platform_region; +create table IF NOT EXISTS wvp_platform_region +( + id serial primary key, + platform_id integer, + region_id integer, + constraint uk_wvp_platform_region_platform_id_group_id unique (platform_id, region_id) +); + +drop table IF EXISTS wvp_stream_proxy; +create table IF NOT EXISTS wvp_stream_proxy +( + id serial primary key, + type character varying(50), + app character varying(255), + stream character varying(255), + src_url character varying(255), + timeout integer, + ffmpeg_cmd_key character varying(255), + rtsp_type character varying(50), + media_server_id character varying(50), + enable_audio bool default false, + enable_mp4 bool default false, + pulling bool default false, + enable bool default false, + enable_remove_none_reader bool default false, + create_time character varying(50), + name character varying(255), + update_time character varying(50), + stream_key character varying(255), + server_id character varying(50), + enable_disable_none_reader bool default false, + relates_media_server_id character varying(50), + constraint uk_stream_proxy_app_stream unique (app, stream) +); + +drop table IF EXISTS wvp_stream_push; +create table IF NOT EXISTS wvp_stream_push +( + id serial primary key, + app character varying(255), + stream character varying(255), + create_time character varying(50), + media_server_id character varying(50), + server_id character varying(50), + push_time character varying(50), + status bool default false, + update_time character varying(50), + pushing bool default false, + self bool default false, + start_offline_push bool default true, + constraint uk_stream_push_app_stream unique (app, stream) +); + +drop table IF EXISTS wvp_cloud_record; +create table IF NOT EXISTS wvp_cloud_record +( + id serial primary key, + app character varying(255), + stream character varying(255), + call_id character varying(255), + start_time bigint, + end_time bigint, + media_server_id character varying(50), + server_id character varying(50), + file_name character varying(255), + folder character varying(500), + file_path character varying(500), + collect bool default false, + file_size bigint, + time_len double precision +); + +drop table IF EXISTS wvp_user; +create table IF NOT EXISTS wvp_user +( + id serial primary key, + username character varying(255), + password character varying(255), + role_id integer, + create_time character varying(50), + update_time character varying(50), + push_key character varying(50), + constraint uk_user_username unique (username) +); + +drop table IF EXISTS wvp_user_role; +create table IF NOT EXISTS wvp_user_role +( + id serial primary key, + name character varying(50), + authority character varying(50), + create_time character varying(50), + update_time character varying(50) +); + + +drop table IF EXISTS wvp_user_api_key; +create table IF NOT EXISTS wvp_user_api_key +( + id serial primary key, + user_id bigint, + app character varying(255), + api_key text, + expired_at bigint, + remark character varying(255), + enable bool default true, + create_time character varying(50), + update_time character varying(50) +); + + +/*初始数据*/ +INSERT INTO wvp_user +VALUES (1, 'admin', '21232f297a57a5a743894a0e4a801fc3', 1, '2021-04-13 14:14:57', '2021-04-13 14:14:57', + '3e80d1762a324d5b0ff636e0bd16f1e3'); +INSERT INTO wvp_user_role +VALUES (1, 'admin', '0', '2021-04-13 14:14:57', '2021-04-13 14:14:57'); + +drop table IF EXISTS wvp_common_group; +create table IF NOT EXISTS wvp_common_group +( + id serial primary key, + device_id varchar(50) NOT NULL, + name varchar(255) NOT NULL, + parent_id int, + parent_device_id varchar(50) DEFAULT NULL, + business_group varchar(50) NOT NULL, + create_time varchar(50) NOT NULL, + update_time varchar(50) NOT NULL, + civil_code varchar(50) default null, + constraint uk_common_group_device_platform unique (device_id) +); + +drop table IF EXISTS wvp_common_region; +create table IF NOT EXISTS wvp_common_region +( + id serial primary key, + device_id varchar(50) NOT NULL, + name varchar(255) NOT NULL, + parent_id int, + parent_device_id varchar(50) DEFAULT NULL, + create_time varchar(50) NOT NULL, + update_time varchar(50) NOT NULL, + constraint uk_common_region_device_id unique (device_id) +); + +drop table IF EXISTS wvp_record_plan; +create table IF NOT EXISTS wvp_record_plan +( + id serial primary key, + snap bool default false, + name varchar(255) NOT NULL, + create_time character varying(50), + update_time character varying(50) +); + +drop table IF EXISTS wvp_record_plan_item; +create table IF NOT EXISTS wvp_record_plan_item +( + id serial primary key, + start int, + stop int, + week_day int, + plan_id int, + create_time character varying(50), + update_time character varying(50) +); + diff --git a/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql b/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql new file mode 100644 index 0000000..524fdbe --- /dev/null +++ b/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql @@ -0,0 +1,467 @@ +/*建表*/ +drop table IF EXISTS wvp_device; +create table IF NOT EXISTS wvp_device +( + id serial primary key, + device_id character varying(50) not null, + name character varying(255), + manufacturer character varying(255), + model character varying(255), + firmware character varying(255), + transport character varying(50), + stream_mode character varying(50), + on_line bool default false, + register_time character varying(50), + keepalive_time character varying(50), + ip character varying(50), + create_time character varying(50), + update_time character varying(50), + port integer, + expires integer, + subscribe_cycle_for_catalog integer DEFAULT 0, + subscribe_cycle_for_mobile_position integer DEFAULT 0, + mobile_position_submission_interval integer DEFAULT 5, + subscribe_cycle_for_alarm integer DEFAULT 0, + host_address character varying(50), + charset character varying(50), + ssrc_check bool default false, + geo_coord_sys character varying(50), + media_server_id character varying(50) default 'auto', + custom_name character varying(255), + sdp_ip character varying(50), + local_ip character varying(50), + password character varying(255), + as_message_channel bool default false, + heart_beat_interval integer, + heart_beat_count integer, + position_capability integer, + broadcast_push_after_ack bool default false, + server_id character varying(50), + constraint uk_device_device unique (device_id) +); + +drop table IF EXISTS wvp_device_alarm; +create table IF NOT EXISTS wvp_device_alarm +( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + alarm_priority character varying(50), + alarm_method character varying(50), + alarm_time character varying(50), + alarm_description character varying(255), + longitude double precision, + latitude double precision, + alarm_type character varying(50), + create_time character varying(50) not null +); + +drop table IF EXISTS wvp_device_mobile_position; +create table IF NOT EXISTS wvp_device_mobile_position +( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + device_name character varying(255), + time character varying(50), + longitude double precision, + latitude double precision, + altitude double precision, + speed double precision, + direction double precision, + report_source character varying(50), + create_time character varying(50) +); + +drop table IF EXISTS wvp_device_channel; +create table IF NOT EXISTS wvp_device_channel +( + id serial primary key, + device_id character varying(50), + name character varying(255), + manufacturer character varying(50), + model character varying(50), + owner character varying(50), + civil_code character varying(50), + block character varying(50), + address character varying(50), + parental integer, + parent_id character varying(50), + safety_way integer, + register_way integer, + cert_num character varying(50), + certifiable integer, + err_code integer, + end_time character varying(50), + secrecy integer, + ip_address character varying(50), + port integer, + password character varying(255), + status character varying(50), + longitude double precision, + latitude double precision, + ptz_type integer, + position_type integer, + room_type integer, + use_type integer, + supply_light_type integer, + direction_type integer, + resolution character varying(255), + business_group_id character varying(255), + download_speed character varying(255), + svc_space_support_mod integer, + svc_time_support_mode integer, + create_time character varying(50) not null, + update_time character varying(50) not null, + sub_count integer, + stream_id character varying(255), + has_audio bool default false, + gps_time character varying(50), + stream_identification character varying(50), + channel_type int default 0 not null, + gb_device_id character varying(50), + gb_name character varying(255), + gb_manufacturer character varying(255), + gb_model character varying(255), + gb_owner character varying(255), + gb_civil_code character varying(255), + gb_block character varying(255), + gb_address character varying(255), + gb_parental integer, + gb_parent_id character varying(255), + gb_safety_way integer, + gb_register_way integer, + gb_cert_num character varying(50), + gb_certifiable integer, + gb_err_code integer, + gb_end_time character varying(50), + gb_secrecy integer, + gb_ip_address character varying(50), + gb_port integer, + gb_password character varying(50), + gb_status character varying(50), + gb_longitude double precision, + gb_latitude double precision, + gb_business_group_id character varying(50), + gb_ptz_type integer, + gb_position_type integer, + gb_room_type integer, + gb_use_type integer, + gb_supply_light_type integer, + gb_direction_type integer, + gb_resolution character varying(255), + gb_download_speed character varying(255), + gb_svc_space_support_mod integer, + gb_svc_time_support_mode integer, + record_plan_id integer, + data_type integer not null, + data_device_id integer not null, + gps_speed double precision, + gps_altitude double precision, + gps_direction double precision, + constraint uk_wvp_unique_channel unique (gb_device_id) +); + +CREATE INDEX idx_data_type ON wvp_device_channel (data_type); +CREATE INDEX idx_data_device_id ON wvp_device_channel (data_device_id); + +drop table IF EXISTS wvp_media_server; +create table IF NOT EXISTS wvp_media_server +( + id character varying(255) primary key, + ip character varying(50), + hook_ip character varying(50), + sdp_ip character varying(50), + stream_ip character varying(50), + http_port integer, + http_ssl_port integer, + rtmp_port integer, + rtmp_ssl_port integer, + rtp_proxy_port integer, + rtsp_port integer, + rtsp_ssl_port integer, + flv_port integer, + flv_ssl_port integer, + ws_flv_port integer, + ws_flv_ssl_port integer, + auto_config bool default false, + secret character varying(50), + type character varying(50) default 'zlm', + rtp_enable bool default false, + rtp_port_range character varying(50), + send_rtp_port_range character varying(50), + record_assist_port integer, + default_server bool default false, + create_time character varying(50), + update_time character varying(50), + hook_alive_interval integer, + record_path character varying(255), + record_day integer default 7, + transcode_suffix character varying(255), + server_id character varying(50), + constraint uk_media_server_unique_ip_http_port unique (ip, http_port, server_id) +); + +drop table IF EXISTS wvp_platform; +create table IF NOT EXISTS wvp_platform +( + id serial primary key, + enable bool default false, + name character varying(255), + server_gb_id character varying(50), + server_gb_domain character varying(50), + server_ip character varying(50), + server_port integer, + device_gb_id character varying(50), + device_ip character varying(50), + device_port character varying(50), + username character varying(255), + password character varying(50), + expires character varying(50), + keep_timeout character varying(50), + transport character varying(50), + civil_code character varying(50), + manufacturer character varying(255), + model character varying(255), + address character varying(255), + character_set character varying(50), + ptz bool default false, + rtcp bool default false, + status bool default false, + catalog_group integer, + register_way integer, + secrecy integer, + create_time character varying(50), + update_time character varying(50), + as_message_channel bool default false, + catalog_with_platform integer default 1, + catalog_with_group integer default 1, + catalog_with_region integer default 1, + auto_push_channel bool default true, + send_stream_ip character varying(50), + server_id character varying(50), + constraint uk_platform_unique_server_gb_id unique (server_gb_id) +); + +drop table IF EXISTS wvp_platform_channel; +create table IF NOT EXISTS wvp_platform_channel +( + id serial primary key, + platform_id integer, + device_channel_id integer, + custom_device_id character varying(50), + custom_name character varying(255), + custom_manufacturer character varying(50), + custom_model character varying(50), + custom_owner character varying(50), + custom_civil_code character varying(50), + custom_block character varying(50), + custom_address character varying(50), + custom_parental integer, + custom_parent_id character varying(50), + custom_safety_way integer, + custom_register_way integer, + custom_cert_num character varying(50), + custom_certifiable integer, + custom_err_code integer, + custom_end_time character varying(50), + custom_secrecy integer, + custom_ip_address character varying(50), + custom_port integer, + custom_password character varying(255), + custom_status character varying(50), + custom_longitude double precision, + custom_latitude double precision, + custom_ptz_type integer, + custom_position_type integer, + custom_room_type integer, + custom_use_type integer, + custom_supply_light_type integer, + custom_direction_type integer, + custom_resolution character varying(255), + custom_business_group_id character varying(255), + custom_download_speed character varying(255), + custom_svc_space_support_mod integer, + custom_svc_time_support_mode integer, + constraint uk_platform_gb_channel_platform_id_catalog_id_device_channel_id unique (platform_id, device_channel_id), + constraint uk_platform_gb_channel_device_id unique (custom_device_id) +); + +drop table IF EXISTS wvp_platform_group; +create table IF NOT EXISTS wvp_platform_group +( + id serial primary key, + platform_id integer, + group_id integer, + constraint uk_wvp_platform_group_platform_id_group_id unique (platform_id, group_id) +); + +drop table IF EXISTS wvp_platform_region; +create table IF NOT EXISTS wvp_platform_region +( + id serial primary key, + platform_id integer, + region_id integer, + constraint uk_wvp_platform_region_platform_id_group_id unique (platform_id, region_id) +); + +drop table IF EXISTS wvp_stream_proxy; +create table IF NOT EXISTS wvp_stream_proxy +( + id serial primary key, + type character varying(50), + app character varying(255), + stream character varying(255), + src_url character varying(255), + timeout integer, + ffmpeg_cmd_key character varying(255), + rtsp_type character varying(50), + media_server_id character varying(50), + enable_audio bool default false, + enable_mp4 bool default false, + pulling bool default false, + enable bool default false, + enable_remove_none_reader bool default false, + create_time character varying(50), + name character varying(255), + update_time character varying(50), + stream_key character varying(255), + server_id character varying(50), + enable_disable_none_reader bool default false, + relates_media_server_id character varying(50), + constraint uk_stream_proxy_app_stream unique (app, stream) +); + +drop table IF EXISTS wvp_stream_push; +create table IF NOT EXISTS wvp_stream_push +( + id serial primary key, + app character varying(255), + stream character varying(255), + create_time character varying(50), + media_server_id character varying(50), + server_id character varying(50), + push_time character varying(50), + status bool default false, + update_time character varying(50), + pushing bool default false, + self bool default false, + start_offline_push bool default true, + constraint uk_stream_push_app_stream unique (app, stream) +); + +drop table IF EXISTS wvp_cloud_record; +create table IF NOT EXISTS wvp_cloud_record +( + id serial primary key, + app character varying(255), + stream character varying(255), + call_id character varying(255), + start_time int8, + end_time int8, + media_server_id character varying(50), + server_id character varying(50), + file_name character varying(255), + folder character varying(500), + file_path character varying(500), + collect bool default false, + file_size int8, + time_len double precision +); + +drop table IF EXISTS wvp_user; +create table IF NOT EXISTS wvp_user +( + id serial primary key, + username character varying(255), + password character varying(255), + role_id integer, + create_time character varying(50), + update_time character varying(50), + push_key character varying(50), + constraint uk_user_username unique (username) +); + +drop table IF EXISTS wvp_user_role; +create table IF NOT EXISTS wvp_user_role +( + id serial primary key, + name character varying(50), + authority character varying(50), + create_time character varying(50), + update_time character varying(50) +); + + +drop table IF EXISTS wvp_user_api_key; +create table IF NOT EXISTS wvp_user_api_key +( + id serial primary key, + user_id int8, + app character varying(255), + api_key text, + expired_at int8, + remark character varying(255), + enable bool default true, + create_time character varying(50), + update_time character varying(50) +); + + +/*初始数据*/ +INSERT INTO wvp_user +VALUES (1, 'admin', '21232f297a57a5a743894a0e4a801fc3', 1, '2021-04-13 14:14:57', '2021-04-13 14:14:57', + '3e80d1762a324d5b0ff636e0bd16f1e3'); +INSERT INTO wvp_user_role +VALUES (1, 'admin', '0', '2021-04-13 14:14:57', '2021-04-13 14:14:57'); + +drop table IF EXISTS wvp_common_group; +create table IF NOT EXISTS wvp_common_group +( + id serial primary key, + device_id varchar(50) NOT NULL, + name varchar(255) NOT NULL, + parent_id int, + parent_device_id varchar(50) DEFAULT NULL, + business_group varchar(50) NOT NULL, + create_time varchar(50) NOT NULL, + update_time varchar(50) NOT NULL, + civil_code varchar(50) default null, + constraint uk_common_group_device_platform unique (device_id) +); + +drop table IF EXISTS wvp_common_region; +create table IF NOT EXISTS wvp_common_region +( + id serial primary key, + device_id varchar(50) NOT NULL, + name varchar(255) NOT NULL, + parent_id int, + parent_device_id varchar(50) DEFAULT NULL, + create_time varchar(50) NOT NULL, + update_time varchar(50) NOT NULL, + constraint uk_common_region_device_id unique (device_id) +); + +drop table IF EXISTS wvp_record_plan; +create table IF NOT EXISTS wvp_record_plan +( + id serial primary key, + snap bool default false, + name varchar(255) NOT NULL, + create_time character varying(50), + update_time character varying(50) +); + +drop table IF EXISTS wvp_record_plan_item; +create table IF NOT EXISTS wvp_record_plan_item +( + id serial primary key, + "start" int, + stop int, + week_day int, + plan_id int, + create_time character varying(50), + update_time character varying(50) +); + diff --git a/数据库/2.7.3/更新-mysql-2.7.1升级到2.7.3.sql b/数据库/2.7.3/更新-mysql-2.7.1升级到2.7.3.sql new file mode 100644 index 0000000..410914e --- /dev/null +++ b/数据库/2.7.3/更新-mysql-2.7.1升级到2.7.3.sql @@ -0,0 +1,432 @@ + +drop table if exists wvp_resources_tree; +drop table if exists wvp_platform_catalog; +drop table if exists wvp_platform_gb_stream; +drop table if exists wvp_platform_gb_channel; +drop table if exists wvp_gb_stream; +drop table if exists wvp_log; +drop table IF EXISTS wvp_device; +drop table IF EXISTS wvp_platform; +drop table IF EXISTS wvp_media_server; +drop table IF EXISTS wvp_device_mobile_position; +drop table IF EXISTS wvp_device_channel; +drop table IF EXISTS wvp_stream_proxy; +drop table IF EXISTS wvp_stream_push; + +create table IF NOT EXISTS wvp_device +( + id serial primary key, + device_id character varying(50) not null, + name character varying(255), + manufacturer character varying(255), + model character varying(255), + firmware character varying(255), + transport character varying(50), + stream_mode character varying(50), + on_line bool default false, + register_time character varying(50), + keepalive_time character varying(50), + ip character varying(50), + create_time character varying(50), + update_time character varying(50), + port integer, + expires integer, + subscribe_cycle_for_catalog integer DEFAULT 0, + subscribe_cycle_for_mobile_position integer DEFAULT 0, + mobile_position_submission_interval integer DEFAULT 5, + subscribe_cycle_for_alarm integer DEFAULT 0, + host_address character varying(50), + charset character varying(50), + ssrc_check bool default false, + geo_coord_sys character varying(50), + media_server_id character varying(50) default 'auto', + custom_name character varying(255), + sdp_ip character varying(50), + local_ip character varying(50), + password character varying(255), + as_message_channel bool default false, + heart_beat_interval integer, + heart_beat_count integer, + position_capability integer, + broadcast_push_after_ack bool default false, + server_id character varying(50), + constraint uk_device_device unique (device_id) +); + +create table if not exists wvp_common_group +( + id bigint unsigned auto_increment primary key, + device_id varchar(50) not null, + name varchar(255) not null, + parent_id int null, + parent_device_id varchar(50) null, + business_group varchar(50) not null, + create_time varchar(50) not null, + update_time varchar(50) not null, + civil_code varchar(50) null, + constraint id + unique (id), + constraint uk_common_group_device_platform + unique (device_id) +); + +create table if not exists wvp_common_region +( + id bigint unsigned auto_increment + primary key, + device_id varchar(50) not null, + name varchar(255) not null, + parent_id int null, + parent_device_id varchar(50) null, + create_time varchar(50) not null, + update_time varchar(50) not null, + constraint id + unique (id), + constraint uk_common_region_device_id + unique (device_id) +); + +create table IF NOT EXISTS wvp_device_channel +( + id serial primary key, + device_id character varying(50), + name character varying(255), + manufacturer character varying(50), + model character varying(50), + owner character varying(50), + civil_code character varying(50), + block character varying(50), + address character varying(50), + parental integer, + parent_id character varying(50), + safety_way integer, + register_way integer, + cert_num character varying(50), + certifiable integer, + err_code integer, + end_time character varying(50), + secrecy integer, + ip_address character varying(50), + port integer, + password character varying(255), + status character varying(50), + longitude double precision, + latitude double precision, + ptz_type integer, + position_type integer, + room_type integer, + use_type integer, + supply_light_type integer, + direction_type integer, + resolution character varying(255), + business_group_id character varying(255), + download_speed character varying(255), + svc_space_support_mod integer, + svc_time_support_mode integer, + create_time character varying(50) not null, + update_time character varying(50) not null, + sub_count integer, + stream_id character varying(255), + has_audio bool default false, + gps_time character varying(50), + stream_identification character varying(50), + channel_type int default 0 not null, + gb_device_id character varying(50), + gb_name character varying(255), + gb_manufacturer character varying(255), + gb_model character varying(255), + gb_owner character varying(255), + gb_civil_code character varying(255), + gb_block character varying(255), + gb_address character varying(255), + gb_parental integer, + gb_parent_id character varying(255), + gb_safety_way integer, + gb_register_way integer, + gb_cert_num character varying(50), + gb_certifiable integer, + gb_err_code integer, + gb_end_time character varying(50), + gb_secrecy integer, + gb_ip_address character varying(50), + gb_port integer, + gb_password character varying(50), + gb_status character varying(50), + gb_longitude double, + gb_latitude double, + gb_business_group_id character varying(50), + gb_ptz_type integer, + gb_position_type integer, + gb_room_type integer, + gb_use_type integer, + gb_supply_light_type integer, + gb_direction_type integer, + gb_resolution character varying(255), + gb_download_speed character varying(255), + gb_svc_space_support_mod integer, + gb_svc_time_support_mode integer, + record_plan_id integer, + data_type integer not null, + data_device_id integer not null, + gps_speed double precision, + gps_altitude double precision, + gps_direction double precision, + index (data_type), + index (data_device_id), + constraint uk_wvp_unique_channel unique (gb_device_id) +); + +create table IF NOT EXISTS wvp_device_mobile_position +( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + device_name character varying(255), + time character varying(50), + longitude double precision, + latitude double precision, + altitude double precision, + speed double precision, + direction double precision, + report_source character varying(50), + create_time character varying(50) +); + +create table IF NOT EXISTS wvp_media_server +( + id character varying(255) primary key, + ip character varying(50), + hook_ip character varying(50), + sdp_ip character varying(50), + stream_ip character varying(50), + http_port integer, + http_ssl_port integer, + rtmp_port integer, + rtmp_ssl_port integer, + rtp_proxy_port integer, + rtsp_port integer, + rtsp_ssl_port integer, + flv_port integer, + flv_ssl_port integer, + ws_flv_port integer, + ws_flv_ssl_port integer, + auto_config bool default false, + secret character varying(50), + type character varying(50) default 'zlm', + rtp_enable bool default false, + rtp_port_range character varying(50), + send_rtp_port_range character varying(50), + record_assist_port integer, + default_server bool default false, + create_time character varying(50), + update_time character varying(50), + hook_alive_interval integer, + record_path character varying(255), + record_day integer default 7, + transcode_suffix character varying(255), + server_id character varying(50), + constraint uk_media_server_unique_ip_http_port unique (ip, http_port, server_id) +); + +create table IF NOT EXISTS wvp_platform +( + id serial primary key, + enable bool default false, + name character varying(255), + server_gb_id character varying(50), + server_gb_domain character varying(50), + server_ip character varying(50), + server_port integer, + device_gb_id character varying(50), + device_ip character varying(50), + device_port character varying(50), + username character varying(255), + password character varying(50), + expires character varying(50), + keep_timeout character varying(50), + transport character varying(50), + civil_code character varying(50), + manufacturer character varying(255), + model character varying(255), + address character varying(255), + character_set character varying(50), + ptz bool default false, + rtcp bool default false, + status bool default false, + catalog_group integer, + register_way integer, + secrecy integer, + create_time character varying(50), + update_time character varying(50), + as_message_channel bool default false, + catalog_with_platform integer default 1, + catalog_with_group integer default 1, + catalog_with_region integer default 1, + auto_push_channel bool default true, + send_stream_ip character varying(50), + server_id character varying(50), + constraint uk_platform_unique_server_gb_id unique (server_gb_id) +); + +create table IF NOT EXISTS wvp_stream_proxy +( + id serial primary key, + type character varying(50), + app character varying(255), + stream character varying(255), + src_url character varying(255), + timeout integer, + ffmpeg_cmd_key character varying(255), + rtsp_type character varying(50), + media_server_id character varying(50), + enable_audio bool default false, + enable_mp4 bool default false, + pulling bool default false, + enable bool default false, + enable_remove_none_reader bool default false, + create_time character varying(50), + name character varying(255), + update_time character varying(50), + stream_key character varying(255), + server_id character varying(50), + enable_disable_none_reader bool default false, + relates_media_server_id character varying(50), + constraint uk_stream_proxy_app_stream unique (app, stream) +); + +create table IF NOT EXISTS wvp_stream_push +( + id serial primary key, + app character varying(255), + stream character varying(255), + create_time character varying(50), + media_server_id character varying(50), + server_id character varying(50), + push_time character varying(50), + status bool default false, + update_time character varying(50), + pushing bool default false, + self bool default false, + start_offline_push bool default true, + constraint uk_stream_push_app_stream unique (app, stream) +); + +create table IF NOT EXISTS wvp_platform_channel +( + id serial primary key, + platform_id integer, + device_channel_id integer, + custom_device_id character varying(50), + custom_name character varying(255), + custom_manufacturer character varying(50), + custom_model character varying(50), + custom_owner character varying(50), + custom_civil_code character varying(50), + custom_block character varying(50), + custom_address character varying(50), + custom_parental integer, + custom_parent_id character varying(50), + custom_safety_way integer, + custom_register_way integer, + custom_cert_num character varying(50), + custom_certifiable integer, + custom_err_code integer, + custom_end_time character varying(50), + custom_secrecy integer, + custom_ip_address character varying(50), + custom_port integer, + custom_password character varying(255), + custom_status character varying(50), + custom_longitude double precision, + custom_latitude double precision, + custom_ptz_type integer, + custom_position_type integer, + custom_room_type integer, + custom_use_type integer, + custom_supply_light_type integer, + custom_direction_type integer, + custom_resolution character varying(255), + custom_business_group_id character varying(255), + custom_download_speed character varying(255), + custom_svc_space_support_mod integer, + custom_svc_time_support_mode integer, + constraint uk_platform_gb_channel_platform_id_catalog_id_device_channel_id unique (platform_id, device_channel_id), + constraint uk_platform_gb_channel_device_id unique (custom_device_id) +); + +create table IF NOT EXISTS wvp_platform_group +( + id serial primary key, + platform_id integer, + group_id integer, + constraint uk_wvp_platform_group_platform_id_group_id unique (platform_id, group_id) +); + +create table IF NOT EXISTS wvp_platform_region +( + id serial primary key, + platform_id integer, + region_id integer, + constraint uk_wvp_platform_region_platform_id_group_id unique (platform_id, region_id) +); + +create table IF NOT EXISTS wvp_record_plan +( + id serial primary key, + snap bool default false, + name varchar(255) NOT NULL, + create_time character varying(50), + update_time character varying(50) +); + +create table IF NOT EXISTS wvp_record_plan_item +( + id serial primary key, + start int, + stop int, + week_day int, + plan_id int, + create_time character varying(50), + update_time character varying(50) +); + + +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20250111`() +BEGIN + + DECLARE serverId VARCHAR(32) DEFAULT '你的服务ID'; + + IF EXISTS (SELECT column_name FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_cloud_record' and INDEX_NAME = 'uk_stream_push_app_stream_path') + THEN + alter table wvp_cloud_record drop index uk_stream_push_app_stream_path ; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_cloud_record' and column_name = 'folder') + THEN + alter table wvp_cloud_record modify folder varchar(500) null; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_cloud_record' and column_name = 'file_path') + THEN + alter table wvp_cloud_record modify file_path varchar(500) null; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_cloud_record' and column_name = 'server_id') + THEN + alter table wvp_cloud_record add server_id character varying(50); + update wvp_cloud_record set server_id = serverId; + END IF; +END;// +call wvp_20250111(); +DROP PROCEDURE wvp_20250111; +DELIMITER ; + +/** +* 20250414 +*/ +alter table wvp_cloud_record modify time_len double precision; diff --git a/数据库/2.7.3/更新-mysql-2.7.3.sql b/数据库/2.7.3/更新-mysql-2.7.3.sql new file mode 100644 index 0000000..49c79a6 --- /dev/null +++ b/数据库/2.7.3/更新-mysql-2.7.3.sql @@ -0,0 +1,303 @@ +/* +* 20240528 +*/ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20240528`() +BEGIN + IF NOT EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'transcode_suffix') + THEN + ALTER TABLE wvp_media_server ADD transcode_suffix character varying(255); + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'type') + THEN + alter table wvp_media_server + add type character varying(50) default 'zlm'; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'flv_port') + THEN + alter table wvp_media_server add flv_port integer; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'flv_ssl_port') + THEN + alter table wvp_media_server add flv_ssl_port integer; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'ws_flv_port') + THEN + alter table wvp_media_server add ws_flv_port integer; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'ws_flv_ssl_port') + THEN + alter table wvp_media_server add ws_flv_ssl_port integer; + END IF; +END; // +call wvp_20240528(); +DROP PROCEDURE wvp_20240528; +DELIMITER ; + +create table IF NOT EXISTS wvp_user_api_key ( + id serial primary key , + user_id bigint, + app character varying(255) , + api_key text, + expired_at bigint, + remark character varying(255), + enable bool default true, + create_time character varying(50), + update_time character varying(50) +); + +/* +* 20241222 +*/ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20241222`() +BEGIN + IF EXISTS (SELECT column_name FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and INDEX_NAME = 'uk_wvp_device_channel_unique_device_channel') + THEN + alter table wvp_device_channel drop index uk_wvp_device_channel_unique_device_channel; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and INDEX_NAME = 'uk_wvp_unique_stream_push_id') + THEN + alter table wvp_device_channel drop index uk_wvp_unique_stream_push_id; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and INDEX_NAME = 'uk_wvp_unique_stream_proxy_id') + THEN + alter table wvp_device_channel drop index uk_wvp_unique_stream_proxy_id; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'data_type') + THEN + alter table wvp_device_channel add data_type integer not null; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'data_device_id') + THEN + alter table wvp_device_channel add data_device_id integer not null; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'device_db_id') + THEN + update wvp_device_channel wdc INNER JOIN + (SELECT id, device_db_id from wvp_device_channel where device_db_id is not null ) ct on ct.id = wdc.id + set wdc.data_type = 1, wdc.data_device_id = ct.device_db_id where wdc.device_db_id is not null; + alter table wvp_device_channel drop device_db_id; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'stream_push_id') + THEN + update wvp_device_channel wdc INNER JOIN + (SELECT id, stream_push_id from wvp_device_channel where stream_push_id is not null ) ct on ct.id = wdc.id + set wdc.data_type = 2, wdc.data_device_id = ct.stream_push_id where wdc.stream_push_id is not null; + alter table wvp_device_channel drop stream_push_id; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'stream_proxy_id') + THEN + update wvp_device_channel wdc INNER JOIN + (SELECT id, stream_proxy_id from wvp_device_channel where stream_proxy_id is not null ) ct on ct.id = wdc.id + set wdc.data_type = 3, wdc.data_device_id = ct.stream_proxy_id where wdc.stream_proxy_id is not null; + alter table wvp_device_channel drop stream_proxy_id; + END IF; +END; // +call wvp_20241222(); +DROP PROCEDURE wvp_20241222; +DELIMITER ; +/* +* 20241231 +*/ +DELIMITER // +CREATE PROCEDURE `wvp_20241231`() +BEGIN + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_stream_proxy' and column_name = 'relates_media_server_id') + THEN + alter table wvp_stream_proxy add relates_media_server_id character varying(50); + END IF; +END; // +call wvp_20241231(); +DROP PROCEDURE wvp_20241231; +DELIMITER ; +/* +* 20250111 +*/ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20250111`() +BEGIN + IF EXISTS (SELECT column_name FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_cloud_record' and INDEX_NAME = 'uk_stream_push_app_stream_path') + THEN + alter table wvp_cloud_record drop index uk_stream_push_app_stream_path ; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_cloud_record' and column_name = 'folder') + THEN + alter table wvp_cloud_record modify folder varchar(500) null; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_cloud_record' and column_name = 'file_path') + THEN + alter table wvp_cloud_record modify file_path varchar(500) null; + END IF; +END; // +call wvp_20250111(); +DROP PROCEDURE wvp_20250111; +DELIMITER ; + +/* +* 20250211 +*/ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20250211`() +BEGIN + IF EXISTS (SELECT column_name FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device' and column_name = 'keepalive_interval_time') + THEN + alter table wvp_device change keepalive_interval_time heart_beat_interval integer after as_message_channel; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device' and column_name = 'heart_beat_count') + THEN + alter table wvp_device add heart_beat_count integer; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device' and column_name = 'position_capability') + THEN + alter table wvp_device add position_capability integer; + END IF; +END; // +call wvp_20250211(); +DROP PROCEDURE wvp_20250211; +DELIMITER ; + +/** + * 20250312 + */ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20250312`() +BEGIN + DECLARE serverId VARCHAR(32) DEFAULT '你的服务ID'; + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device' and column_name = 'server_id') + THEN + alter table wvp_device add server_id character varying(50); + update wvp_device set server_id = serverId; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'server_id') + THEN + alter table wvp_media_server add server_id character varying(50); + update wvp_media_server set server_id = serverId; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_stream_proxy' and column_name = 'server_id') + THEN + alter table wvp_stream_proxy add server_id character varying(50); + update wvp_stream_proxy set server_id = serverId; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_cloud_record' and column_name = 'server_id') + THEN + alter table wvp_cloud_record add server_id character varying(50); + update wvp_cloud_record set server_id = serverId; + END IF; + + IF not EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_platform' and column_name = 'server_id') + THEN + alter table wvp_platform add server_id character varying(50); + END IF; +END; // +call wvp_20250312(); +DROP PROCEDURE wvp_20250312; +DELIMITER ; + +/* +* 20250319 +*/ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20250319`() +BEGIN + IF NOT EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'gps_speed') + THEN + alter table wvp_device_channel add gps_speed double precision; + END IF; + + IF NOT EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'gps_altitude') + THEN + alter table wvp_device_channel add gps_altitude double precision; + END IF; + + IF NOT EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'gps_direction') + THEN + alter table wvp_device_channel add gps_direction double precision; + END IF; +END; // +call wvp_20250319(); +DROP PROCEDURE wvp_20250319; +DELIMITER ; + +/* +* 20250319 +*/ +update wvp_record_plan_item set start = start * 30, stop = (stop + 1) * 30 + +/* +* 20250402 +*/ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20250402`() +BEGIN + IF NOT EXISTS (SELECT column_name FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and INDEX_NAME = 'data_type') + THEN + create index data_type on wvp_device_channel (data_type); + END IF; + IF NOT EXISTS (SELECT column_name FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and INDEX_NAME = 'data_device_id') + THEN + create index data_device_id on wvp_device_channel (data_device_id); + END IF; + +END; // +call wvp_20250402(); +DROP PROCEDURE wvp_20250402; +DELIMITER ; + +/** +* 20250414 +*/ +alter table wvp_cloud_record modify time_len double precision; + + + diff --git a/数据库/2.7.3/更新-postgresql-kingbase-2.7.1升级到2.7.3.sql b/数据库/2.7.3/更新-postgresql-kingbase-2.7.1升级到2.7.3.sql new file mode 100644 index 0000000..b0c42c6 --- /dev/null +++ b/数据库/2.7.3/更新-postgresql-kingbase-2.7.1升级到2.7.3.sql @@ -0,0 +1,394 @@ +drop table if exists wvp_resources_tree; +drop table if exists wvp_platform_catalog; +drop table if exists wvp_platform_gb_stream; +drop table if exists wvp_platform_gb_channel; +drop table if exists wvp_gb_stream; +drop table if exists wvp_log; +drop table IF EXISTS wvp_device; +drop table IF EXISTS wvp_platform; +drop table IF EXISTS wvp_media_server; +drop table IF EXISTS wvp_device_mobile_position; +drop table IF EXISTS wvp_device_channel; +drop table IF EXISTS wvp_stream_proxy; +drop table IF EXISTS wvp_stream_push; + +create table IF NOT EXISTS wvp_device +( + id serial primary key, + device_id character varying(50) not null, + name character varying(255), + manufacturer character varying(255), + model character varying(255), + firmware character varying(255), + transport character varying(50), + stream_mode character varying(50), + on_line bool default false, + register_time character varying(50), + keepalive_time character varying(50), + ip character varying(50), + create_time character varying(50), + update_time character varying(50), + port integer, + expires integer, + subscribe_cycle_for_catalog integer DEFAULT 0, + subscribe_cycle_for_mobile_position integer DEFAULT 0, + mobile_position_submission_interval integer DEFAULT 5, + subscribe_cycle_for_alarm integer DEFAULT 0, + host_address character varying(50), + charset character varying(50), + ssrc_check bool default false, + geo_coord_sys character varying(50), + media_server_id character varying(50) default 'auto', + custom_name character varying(255), + sdp_ip character varying(50), + local_ip character varying(50), + password character varying(255), + as_message_channel bool default false, + heart_beat_interval integer, + heart_beat_count integer, + position_capability integer, + broadcast_push_after_ack bool default false, + server_id character varying(50), + constraint uk_device_device unique (device_id) +); + +create table IF NOT EXISTS wvp_device_channel +( + id serial primary key, + device_id character varying(50), + name character varying(255), + manufacturer character varying(50), + model character varying(50), + owner character varying(50), + civil_code character varying(50), + block character varying(50), + address character varying(50), + parental integer, + parent_id character varying(50), + safety_way integer, + register_way integer, + cert_num character varying(50), + certifiable integer, + err_code integer, + end_time character varying(50), + secrecy integer, + ip_address character varying(50), + port integer, + password character varying(255), + status character varying(50), + longitude double precision, + latitude double precision, + ptz_type integer, + position_type integer, + room_type integer, + use_type integer, + supply_light_type integer, + direction_type integer, + resolution character varying(255), + business_group_id character varying(255), + download_speed character varying(255), + svc_space_support_mod integer, + svc_time_support_mode integer, + create_time character varying(50) not null, + update_time character varying(50) not null, + sub_count integer, + stream_id character varying(255), + has_audio bool default false, + gps_time character varying(50), + stream_identification character varying(50), + channel_type int default 0 not null, + gb_device_id character varying(50), + gb_name character varying(255), + gb_manufacturer character varying(255), + gb_model character varying(255), + gb_owner character varying(255), + gb_civil_code character varying(255), + gb_block character varying(255), + gb_address character varying(255), + gb_parental integer, + gb_parent_id character varying(255), + gb_safety_way integer, + gb_register_way integer, + gb_cert_num character varying(50), + gb_certifiable integer, + gb_err_code integer, + gb_end_time character varying(50), + gb_secrecy integer, + gb_ip_address character varying(50), + gb_port integer, + gb_password character varying(50), + gb_status character varying(50), + gb_longitude double precision, + gb_latitude double precision, + gb_business_group_id character varying(50), + gb_ptz_type integer, + gb_position_type integer, + gb_room_type integer, + gb_use_type integer, + gb_supply_light_type integer, + gb_direction_type integer, + gb_resolution character varying(255), + gb_download_speed character varying(255), + gb_svc_space_support_mod integer, + gb_svc_time_support_mode integer, + record_plan_id integer, + data_type integer not null, + data_device_id integer not null, + gps_speed double precision, + gps_altitude double precision, + gps_direction double precision, + constraint uk_wvp_unique_channel unique (gb_device_id) +); + +create index if not exists data_type on wvp_device_channel (data_type); +create index if not exists data_device_id on wvp_device_channel (data_device_id); + +create table IF NOT EXISTS wvp_device_mobile_position +( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + device_name character varying(255), + time character varying(50), + longitude double precision, + latitude double precision, + altitude double precision, + speed double precision, + direction double precision, + report_source character varying(50), + create_time character varying(50) +); + +create table IF NOT EXISTS wvp_media_server +( + id character varying(255) primary key, + ip character varying(50), + hook_ip character varying(50), + sdp_ip character varying(50), + stream_ip character varying(50), + http_port integer, + http_ssl_port integer, + rtmp_port integer, + rtmp_ssl_port integer, + rtp_proxy_port integer, + rtsp_port integer, + rtsp_ssl_port integer, + flv_port integer, + flv_ssl_port integer, + ws_flv_port integer, + ws_flv_ssl_port integer, + auto_config bool default false, + secret character varying(50), + type character varying(50) default 'zlm', + rtp_enable bool default false, + rtp_port_range character varying(50), + send_rtp_port_range character varying(50), + record_assist_port integer, + default_server bool default false, + create_time character varying(50), + update_time character varying(50), + hook_alive_interval integer, + record_path character varying(255), + record_day integer default 7, + transcode_suffix character varying(255), + server_id character varying(50), + constraint uk_media_server_unique_ip_http_port unique (ip, http_port, server_id) +); + +create table IF NOT EXISTS wvp_common_group +( + id serial primary key, + device_id varchar(50) NOT NULL, + name varchar(255) NOT NULL, + parent_id int, + parent_device_id varchar(50) DEFAULT NULL, + business_group varchar(50) NOT NULL, + create_time varchar(50) NOT NULL, + update_time varchar(50) NOT NULL, + civil_code varchar(50) default null, + constraint uk_common_group_device_platform unique (device_id) +); + +create table IF NOT EXISTS wvp_common_region +( + id serial primary key, + device_id varchar(50) NOT NULL, + name varchar(255) NOT NULL, + parent_id int, + parent_device_id varchar(50) DEFAULT NULL, + create_time varchar(50) NOT NULL, + update_time varchar(50) NOT NULL, + constraint uk_common_region_device_id unique (device_id) +); + +create table IF NOT EXISTS wvp_record_plan +( + id serial primary key, + snap bool default false, + name varchar(255) NOT NULL, + create_time character varying(50), + update_time character varying(50) +); + +create table IF NOT EXISTS wvp_record_plan_item +( + id serial primary key, + "start" int, + stop int, + week_day int, + plan_id int, + create_time character varying(50), + update_time character varying(50) +); + +create table IF NOT EXISTS wvp_platform +( + id serial primary key, + enable bool default false, + name character varying(255), + server_gb_id character varying(50), + server_gb_domain character varying(50), + server_ip character varying(50), + server_port integer, + device_gb_id character varying(50), + device_ip character varying(50), + device_port character varying(50), + username character varying(255), + password character varying(50), + expires character varying(50), + keep_timeout character varying(50), + transport character varying(50), + civil_code character varying(50), + manufacturer character varying(255), + model character varying(255), + address character varying(255), + character_set character varying(50), + ptz bool default false, + rtcp bool default false, + status bool default false, + catalog_group integer, + register_way integer, + secrecy integer, + create_time character varying(50), + update_time character varying(50), + as_message_channel bool default false, + catalog_with_platform integer default 1, + catalog_with_group integer default 1, + catalog_with_region integer default 1, + auto_push_channel bool default true, + send_stream_ip character varying(50), + server_id character varying(50), + constraint uk_platform_unique_server_gb_id unique (server_gb_id) +); + +create table IF NOT EXISTS wvp_platform_channel +( + id serial primary key, + platform_id integer, + device_channel_id integer, + custom_device_id character varying(50), + custom_name character varying(255), + custom_manufacturer character varying(50), + custom_model character varying(50), + custom_owner character varying(50), + custom_civil_code character varying(50), + custom_block character varying(50), + custom_address character varying(50), + custom_parental integer, + custom_parent_id character varying(50), + custom_safety_way integer, + custom_register_way integer, + custom_cert_num character varying(50), + custom_certifiable integer, + custom_err_code integer, + custom_end_time character varying(50), + custom_secrecy integer, + custom_ip_address character varying(50), + custom_port integer, + custom_password character varying(255), + custom_status character varying(50), + custom_longitude double precision, + custom_latitude double precision, + custom_ptz_type integer, + custom_position_type integer, + custom_room_type integer, + custom_use_type integer, + custom_supply_light_type integer, + custom_direction_type integer, + custom_resolution character varying(255), + custom_business_group_id character varying(255), + custom_download_speed character varying(255), + custom_svc_space_support_mod integer, + custom_svc_time_support_mode integer, + constraint uk_platform_gb_channel_platform_id_catalog_id_device_channel_id unique (platform_id, device_channel_id), + constraint uk_platform_gb_channel_device_id unique (custom_device_id) +); + +create table IF NOT EXISTS wvp_platform_group +( + id serial primary key, + platform_id integer, + group_id integer, + constraint uk_wvp_platform_group_platform_id_group_id unique (platform_id, group_id) +); + +create table IF NOT EXISTS wvp_platform_region +( + id serial primary key, + platform_id integer, + region_id integer, + constraint uk_wvp_platform_region_platform_id_group_id unique (platform_id, region_id) +); + +create table IF NOT EXISTS wvp_stream_proxy +( + id serial primary key, + type character varying(50), + app character varying(255), + stream character varying(255), + src_url character varying(255), + timeout integer, + ffmpeg_cmd_key character varying(255), + rtsp_type character varying(50), + media_server_id character varying(50), + enable_audio bool default false, + enable_mp4 bool default false, + pulling bool default false, + enable bool default false, + enable_remove_none_reader bool default false, + create_time character varying(50), + name character varying(255), + update_time character varying(50), + stream_key character varying(255), + server_id character varying(50), + enable_disable_none_reader bool default false, + relates_media_server_id character varying(50), + constraint uk_stream_proxy_app_stream unique (app, stream) +); + +create table IF NOT EXISTS wvp_stream_push +( + id serial primary key, + app character varying(255), + stream character varying(255), + create_time character varying(50), + media_server_id character varying(50), + server_id character varying(50), + push_time character varying(50), + status bool default false, + update_time character varying(50), + pushing bool default false, + self bool default false, + start_offline_push bool default true, + constraint uk_stream_push_app_stream unique (app, stream) +); +alter table wvp_cloud_record add column if not exists server_id character varying(50); +ALTER TABLE wvp_cloud_record DROP CONSTRAINT IF EXISTS uk_stream_push_app_stream_path; +alter table wvp_cloud_record alter folder type varchar(500); +alter table wvp_cloud_record alter file_path type varchar(500); +update wvp_cloud_record set server_id = '你的服务ID'; + +/** +* 20250414 +*/ +alter table wvp_cloud_record modify time_len double precision; diff --git a/数据库/2.7.3/更新-postgresql-kingbase-2.7.3.sql b/数据库/2.7.3/更新-postgresql-kingbase-2.7.3.sql new file mode 100644 index 0000000..b665882 --- /dev/null +++ b/数据库/2.7.3/更新-postgresql-kingbase-2.7.3.sql @@ -0,0 +1,119 @@ +/* +* 20240528 +*/ +ALTER TABLE wvp_media_server ADD COLUMN IF NOT EXISTS transcode_suffix character varying(255); +ALTER table wvp_media_server ADD COLUMN IF NOT EXISTS type character varying(50) default 'zlm'; +ALTER table wvp_media_server ADD COLUMN IF NOT EXISTS flv_port integer; +ALTER table wvp_media_server ADD COLUMN IF NOT EXISTS flv_ssl_port integer; +ALTER table wvp_media_server ADD COLUMN IF NOT EXISTS ws_flv_port integer; +ALTER table wvp_media_server ADD COLUMN IF NOT EXISTS ws_flv_ssl_port integer; + + +create table IF NOT EXISTS wvp_user_api_key ( + id serial primary key , + user_id bigint, + app character varying(255) , + api_key text, + expired_at bigint, + remark character varying(255), + enable bool default true, + create_time character varying(50), + update_time character varying(50) +); + +/* +* 20241222 +*/ +ALTER TABLE wvp_device_channel drop CONSTRAINT IF EXISTS uk_wvp_device_channel_unique_device_channel; +ALTER TABLE wvp_device_channel DROP CONSTRAINT IF EXISTS uk_wvp_unique_stream_push_id; +ALTER TABLE wvp_device_channel DROP CONSTRAINT IF EXISTS uk_wvp_unique_stream_proxy_id; + +ALTER TABLE wvp_device_channel ADD COLUMN IF NOT EXISTS data_type integer not null; +ALTER TABLE wvp_device_channel ADD COLUMN IF NOT EXISTS data_device_id integer not null; + +DO $$ + BEGIN + IF EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT current_schema()) and table_name = 'wvp_device_channel' and column_name = 'device_db_id') + THEN + update wvp_device_channel wdc set data_type = 1, data_device_id = + (SELECT device_db_id from wvp_device_channel where device_db_id is not null and id = wdc.id ) + where device_db_id is not null; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT current_schema()) and table_name = 'wvp_device_channel' and column_name = 'stream_push_id') + THEN + update wvp_device_channel wdc set data_type = 2, data_device_id = + (SELECT stream_push_id from wvp_device_channel where stream_push_id is not null and id = wdc.id ) + where stream_push_id is not null; + END IF; + + IF EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT current_schema()) and table_name = 'wvp_device_channel' and column_name = 'stream_proxy_id') + THEN + update wvp_device_channel wdc set data_type = 3, data_device_id = (SELECT stream_proxy_id from wvp_device_channel where stream_proxy_id is not null and id = wdc.id ) + where stream_proxy_id is not null; + END IF; + END $$; + + +alter table wvp_device_channel drop column IF EXISTS device_db_id; +alter table wvp_device_channel drop column IF EXISTS stream_push_id; +alter table wvp_device_channel drop column IF EXISTS stream_proxy_id; + +/* +* 20241231 +*/ +alter table wvp_stream_proxy add column IF NOT EXISTS relates_media_server_id character varying(50); + +/* +* 20250111 +*/ +ALTER TABLE wvp_cloud_record DROP CONSTRAINT IF EXISTS uk_stream_push_app_stream_path; +alter table wvp_cloud_record alter folder type varchar(500); +alter table wvp_cloud_record alter file_path type varchar(500); + +/* +* 20250211 +*/ +alter table wvp_device rename keepalive_interval_time to heart_beat_interval; +alter table wvp_device add column if not exists heart_beat_count integer; +alter table wvp_device add column if not exists position_capability integer; + +/** + * 20250312 + */ +alter table wvp_device add column if not exists server_id character varying(50); +alter table wvp_media_server add column if not exists server_id character varying(50); +alter table wvp_stream_proxy add column if not exists server_id character varying(50); +alter table wvp_cloud_record add column if not exists server_id character varying(50); +alter table wvp_platform add column if not exists server_id character varying(50); + +update wvp_device set server_id = '你的服务ID'; +update wvp_media_server set server_id = '你的服务ID'; +update wvp_stream_proxy set server_id = '你的服务ID'; +update wvp_cloud_record set server_id = '你的服务ID'; + +/* +* 20250319 +*/ +alter table wvp_device_channel add column if not exists gps_speed double precision; +alter table wvp_device_channel add column if not exists gps_altitude double precision; +alter table wvp_device_channel add column if not exists gps_direction double precision; + +/* +* 20250319 +*/ +update wvp_record_plan_item set start = start * 30, stop = (stop + 1) * 30 +/* +* 20250402 +*/ +create index if not exists data_type on wvp_device_channel (data_type); +create index if not exists data_device_id on wvp_device_channel (data_device_id); + +/** +* 20250414 +*/ +alter table wvp_cloud_record modify time_len double precision; + diff --git a/数据库/2.7.4-h2/h2-data.sql b/数据库/2.7.4-h2/h2-data.sql new file mode 100644 index 0000000..17cb00f --- /dev/null +++ b/数据库/2.7.4-h2/h2-data.sql @@ -0,0 +1,7 @@ + +/*初始数据*/ +INSERT INTO wvp_user +VALUES (1, 'admin', '21232f297a57a5a743894a0e4a801fc3', 1, '2021-04-13 14:14:57', '2021-04-13 14:14:57', + '3e80d1762a324d5b0ff636e0bd16f1e3'); +INSERT INTO wvp_user_role +VALUES (1, 'admin', '0', '2021-04-13 14:14:57', '2021-04-13 14:14:57'); diff --git a/数据库/2.7.4-h2/h2-schema.sql b/数据库/2.7.4-h2/h2-schema.sql new file mode 100644 index 0000000..f1bef6d --- /dev/null +++ b/数据库/2.7.4-h2/h2-schema.sql @@ -0,0 +1,435 @@ +/*建表*/ +create table IF NOT EXISTS wvp_device +( + id bigint primary key, + device_id character varying(50) not null, + name character varying(255), + manufacturer character varying(255), + model character varying(255), + firmware character varying(255), + transport character varying(50), + stream_mode character varying(50), + on_line bool default false, + register_time character varying(50), + keepalive_time character varying(50), + ip character varying(50), + create_time character varying(50), + update_time character varying(50), + port integer, + expires integer, + subscribe_cycle_for_catalog integer DEFAULT 0, + subscribe_cycle_for_mobile_position integer DEFAULT 0, + mobile_position_submission_interval integer DEFAULT 5, + subscribe_cycle_for_alarm integer DEFAULT 0, + host_address character varying(50), + charset character varying(50), + ssrc_check bool default false, + geo_coord_sys character varying(50), + media_server_id character varying(50) default 'auto', + custom_name character varying(255), + sdp_ip character varying(50), + local_ip character varying(50), + password character varying(255), + as_message_channel bool default false, + heart_beat_interval integer, + heart_beat_count integer, + position_capability integer, + broadcast_push_after_ack bool default false, + server_id character varying(50), + constraint uk_device_device unique (device_id) +); + +create table IF NOT EXISTS wvp_device_alarm +( + id bigint primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + alarm_priority character varying(50), + alarm_method character varying(50), + alarm_time character varying(50), + alarm_description character varying(255), + longitude double precision, + latitude double precision, + alarm_type character varying(50), + create_time character varying(50) not null +); + +create table IF NOT EXISTS wvp_device_mobile_position +( + id bigint primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + device_name character varying(255), + time character varying(50), + longitude double precision, + latitude double precision, + altitude double precision, + speed double precision, + direction double precision, + report_source character varying(50), + create_time character varying(50) +); + +create table IF NOT EXISTS wvp_device_channel +( + id bigint primary key, + device_id character varying(50), + name character varying(255), + manufacturer character varying(50), + model character varying(50), + owner character varying(50), + civil_code character varying(50), + block character varying(50), + address character varying(50), + parental integer, + parent_id character varying(50), + safety_way integer, + register_way integer, + cert_num character varying(50), + certifiable integer, + err_code integer, + end_time character varying(50), + secrecy integer, + ip_address character varying(50), + port integer, + password character varying(255), + status character varying(50), + longitude double precision, + latitude double precision, + ptz_type integer, + position_type integer, + room_type integer, + use_type integer, + supply_light_type integer, + direction_type integer, + resolution character varying(255), + business_group_id character varying(255), + download_speed character varying(255), + svc_space_support_mod integer, + svc_time_support_mode integer, + create_time character varying(50) not null, + update_time character varying(50) not null, + sub_count integer, + stream_id character varying(255), + has_audio bool default false, + gps_time character varying(50), + stream_identification character varying(50), + channel_type int default 0 not null, + gb_device_id character varying(50), + gb_name character varying(255), + gb_manufacturer character varying(255), + gb_model character varying(255), + gb_owner character varying(255), + gb_civil_code character varying(255), + gb_block character varying(255), + gb_address character varying(255), + gb_parental integer, + gb_parent_id character varying(255), + gb_safety_way integer, + gb_register_way integer, + gb_cert_num character varying(50), + gb_certifiable integer, + gb_err_code integer, + gb_end_time character varying(50), + gb_secrecy integer, + gb_ip_address character varying(50), + gb_port integer, + gb_password character varying(50), + gb_status character varying(50), + gb_longitude double, + gb_latitude double, + gb_business_group_id character varying(50), + gb_ptz_type integer, + gb_position_type integer, + gb_room_type integer, + gb_use_type integer, + gb_supply_light_type integer, + gb_direction_type integer, + gb_resolution character varying(255), + gb_download_speed character varying(255), + gb_svc_space_support_mod integer, + gb_svc_time_support_mode integer, + record_plan_id integer, + data_type integer not null, + data_device_id integer not null, + gps_speed double precision, + gps_altitude double precision, + gps_direction double precision, + constraint uk_wvp_unique_channel unique (gb_device_id) +); + +create table IF NOT EXISTS wvp_media_server +( + id character varying(255) primary key, + ip character varying(50), + hook_ip character varying(50), + sdp_ip character varying(50), + stream_ip character varying(50), + http_port integer, + http_ssl_port integer, + rtmp_port integer, + rtmp_ssl_port integer, + rtp_proxy_port integer, + rtsp_port integer, + rtsp_ssl_port integer, + flv_port integer, + flv_ssl_port integer, + ws_flv_port integer, + ws_flv_ssl_port integer, + auto_config bool default false, + secret character varying(50), + type character varying(50) default 'zlm', + rtp_enable bool default false, + rtp_port_range character varying(50), + send_rtp_port_range character varying(50), + record_assist_port integer, + default_server bool default false, + create_time character varying(50), + update_time character varying(50), + hook_alive_interval integer, + record_path character varying(255), + record_day integer default 7, + transcode_suffix character varying(255), + server_id character varying(50), + constraint uk_media_server_unique_ip_http_port unique (ip, http_port, server_id) +); + +create table IF NOT EXISTS wvp_platform +( + id bigint primary key, + enable bool default false, + name character varying(255), + server_gb_id character varying(50), + server_gb_domain character varying(50), + server_ip character varying(50), + server_port integer, + device_gb_id character varying(50), + device_ip character varying(50), + device_port character varying(50), + username character varying(255), + password character varying(50), + expires character varying(50), + keep_timeout character varying(50), + transport character varying(50), + civil_code character varying(50), + manufacturer character varying(255), + model character varying(255), + address character varying(255), + character_set character varying(50), + ptz bool default false, + rtcp bool default false, + status bool default false, + catalog_group integer, + register_way integer, + secrecy integer, + create_time character varying(50), + update_time character varying(50), + as_message_channel bool default false, + catalog_with_platform integer default 1, + catalog_with_group integer default 1, + catalog_with_region integer default 1, + auto_push_channel bool default true, + send_stream_ip character varying(50), + server_id character varying(50), + constraint uk_platform_unique_server_gb_id unique (server_gb_id) +); + +create table IF NOT EXISTS wvp_platform_channel +( + id bigint primary key, + platform_id integer, + device_channel_id integer, + custom_device_id character varying(50), + custom_name character varying(255), + custom_manufacturer character varying(50), + custom_model character varying(50), + custom_owner character varying(50), + custom_civil_code character varying(50), + custom_block character varying(50), + custom_address character varying(50), + custom_parental integer, + custom_parent_id character varying(50), + custom_safety_way integer, + custom_register_way integer, + custom_cert_num character varying(50), + custom_certifiable integer, + custom_err_code integer, + custom_end_time character varying(50), + custom_secrecy integer, + custom_ip_address character varying(50), + custom_port integer, + custom_password character varying(255), + custom_status character varying(50), + custom_longitude double precision, + custom_latitude double precision, + custom_ptz_type integer, + custom_position_type integer, + custom_room_type integer, + custom_use_type integer, + custom_supply_light_type integer, + custom_direction_type integer, + custom_resolution character varying(255), + custom_business_group_id character varying(255), + custom_download_speed character varying(255), + custom_svc_space_support_mod integer, + custom_svc_time_support_mode integer, + constraint uk_platform_gb_channel_platform_id_catalog_id_device_channel_id unique (platform_id, device_channel_id), + constraint uk_platform_gb_channel_device_id unique (custom_device_id) +); + +create table IF NOT EXISTS wvp_platform_group +( + id bigint primary key, + platform_id integer, + group_id integer, + constraint uk_wvp_platform_group_platform_id_group_id unique (platform_id, group_id) +); + +create table IF NOT EXISTS wvp_platform_region +( + id bigint primary key, + platform_id integer, + region_id integer, + constraint uk_wvp_platform_region_platform_id_group_id unique (platform_id, region_id) +); + +create table IF NOT EXISTS wvp_stream_proxy +( + id bigint primary key, + type character varying(50), + app character varying(255), + stream character varying(255), + src_url character varying(255), + timeout integer, + ffmpeg_cmd_key character varying(255), + rtsp_type character varying(50), + media_server_id character varying(50), + enable_audio bool default false, + enable_mp4 bool default false, + pulling bool default false, + enable bool default false, + enable_remove_none_reader bool default false, + create_time character varying(50), + name character varying(255), + update_time character varying(50), + stream_key character varying(255), + server_id character varying(50), + enable_disable_none_reader bool default false, + relates_media_server_id character varying(50), + constraint uk_stream_proxy_app_stream unique (app, stream) +); + +create table IF NOT EXISTS wvp_stream_push +( + id bigint primary key, + app character varying(255), + stream character varying(255), + create_time character varying(50), + media_server_id character varying(50), + server_id character varying(50), + push_time character varying(50), + status bool default false, + update_time character varying(50), + pushing bool default false, + self bool default false, + start_offline_push bool default true, + constraint uk_stream_push_app_stream unique (app, stream) +); + +create table IF NOT EXISTS wvp_cloud_record +( + id bigint primary key, + app character varying(255), + stream character varying(255), + call_id character varying(255), + start_time bigint, + end_time bigint, + media_server_id character varying(50), + server_id character varying(50), + file_name character varying(255), + folder character varying(500), + file_path character varying(500), + collect bool default false, + file_size bigint, + time_len double precision +); + +create table IF NOT EXISTS wvp_user +( + id bigint primary key, + username character varying(255), + password character varying(255), + role_id integer, + create_time character varying(50), + update_time character varying(50), + push_key character varying(50), + constraint uk_user_username unique (username) +); + +create table IF NOT EXISTS wvp_user_role +( + id bigint primary key, + name character varying(50), + authority character varying(50), + create_time character varying(50), + update_time character varying(50) +); + +create table IF NOT EXISTS wvp_user_api_key +( + id bigint primary key, + user_id bigint, + app character varying(255), + api_key text, + expired_at bigint, + remark character varying(255), + enable bool default true, + create_time character varying(50), + update_time character varying(50) +); + +create table IF NOT EXISTS wvp_common_group +( + id bigint primary key, + device_id varchar(50) NOT NULL, + name varchar(255) NOT NULL, + parent_id int, + parent_device_id varchar(50) DEFAULT NULL, + business_group varchar(50) NOT NULL, + create_time varchar(50) NOT NULL, + update_time varchar(50) NOT NULL, + civil_code varchar(50) default null, + constraint uk_common_group_device_platform unique (device_id) +); + +create table IF NOT EXISTS wvp_common_region +( + id bigint primary key, + device_id varchar(50) NOT NULL, + name varchar(255) NOT NULL, + parent_id int, + parent_device_id varchar(50) DEFAULT NULL, + create_time varchar(50) NOT NULL, + update_time varchar(50) NOT NULL, + constraint uk_common_region_device_id unique (device_id) +); + +create table IF NOT EXISTS wvp_record_plan +( + id bigint primary key, + snap bool default false, + name varchar(255) NOT NULL, + create_time character varying(50), + update_time character varying(50) +); + +create table IF NOT EXISTS wvp_record_plan_item +( + id bigint primary key, + start int, + stop int, + week_day int, + plan_id int, + create_time character varying(50), + update_time character varying(50) +); diff --git a/数据库/2.7.4/初始化-mysql-2.7.4.sql b/数据库/2.7.4/初始化-mysql-2.7.4.sql new file mode 100644 index 0000000..ffc13b3 --- /dev/null +++ b/数据库/2.7.4/初始化-mysql-2.7.4.sql @@ -0,0 +1,508 @@ +/*建表*/ +drop table IF EXISTS wvp_device; +create table IF NOT EXISTS wvp_device +( + id serial primary key, + device_id character varying(50) not null, + name character varying(255), + manufacturer character varying(255), + model character varying(255), + firmware character varying(255), + transport character varying(50), + stream_mode character varying(50), + on_line bool default false, + register_time character varying(50), + keepalive_time character varying(50), + ip character varying(50), + create_time character varying(50), + update_time character varying(50), + port integer, + expires integer, + subscribe_cycle_for_catalog integer DEFAULT 0, + subscribe_cycle_for_mobile_position integer DEFAULT 0, + mobile_position_submission_interval integer DEFAULT 5, + subscribe_cycle_for_alarm integer DEFAULT 0, + host_address character varying(50), + charset character varying(50), + ssrc_check bool default false, + geo_coord_sys character varying(50), + media_server_id character varying(50) default 'auto', + custom_name character varying(255), + sdp_ip character varying(50), + local_ip character varying(50), + password character varying(255), + as_message_channel bool default false, + heart_beat_interval integer, + heart_beat_count integer, + position_capability integer, + broadcast_push_after_ack bool default false, + server_id character varying(50), + constraint uk_device_device unique (device_id) +); + +drop table IF EXISTS wvp_device_alarm; +create table IF NOT EXISTS wvp_device_alarm +( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + alarm_priority character varying(50), + alarm_method character varying(50), + alarm_time character varying(50), + alarm_description character varying(255), + longitude double precision, + latitude double precision, + alarm_type character varying(50), + create_time character varying(50) not null +); + +drop table IF EXISTS wvp_device_mobile_position; +create table IF NOT EXISTS wvp_device_mobile_position +( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + device_name character varying(255), + time character varying(50), + longitude double precision, + latitude double precision, + altitude double precision, + speed double precision, + direction double precision, + report_source character varying(50), + create_time character varying(50) +); + +drop table IF EXISTS wvp_device_channel; +create table IF NOT EXISTS wvp_device_channel +( + id serial primary key, + device_id character varying(50), + name character varying(255), + manufacturer character varying(50), + model character varying(50), + owner character varying(50), + civil_code character varying(50), + block character varying(50), + address character varying(50), + parental integer, + parent_id character varying(50), + safety_way integer, + register_way integer, + cert_num character varying(50), + certifiable integer, + err_code integer, + end_time character varying(50), + secrecy integer, + ip_address character varying(50), + port integer, + password character varying(255), + status character varying(50), + longitude double precision, + latitude double precision, + ptz_type integer, + position_type integer, + room_type integer, + use_type integer, + supply_light_type integer, + direction_type integer, + resolution character varying(255), + business_group_id character varying(255), + download_speed character varying(255), + svc_space_support_mod integer, + svc_time_support_mode integer, + create_time character varying(50) not null, + update_time character varying(50) not null, + sub_count integer, + stream_id character varying(255), + has_audio bool default false, + gps_time character varying(50), + stream_identification character varying(50), + channel_type int default 0 not null, + map_level int default 0, + gb_device_id character varying(50), + gb_name character varying(255), + gb_manufacturer character varying(255), + gb_model character varying(255), + gb_owner character varying(255), + gb_civil_code character varying(255), + gb_block character varying(255), + gb_address character varying(255), + gb_parental integer, + gb_parent_id character varying(255), + gb_safety_way integer, + gb_register_way integer, + gb_cert_num character varying(50), + gb_certifiable integer, + gb_err_code integer, + gb_end_time character varying(50), + gb_secrecy integer, + gb_ip_address character varying(50), + gb_port integer, + gb_password character varying(50), + gb_status character varying(50), + gb_longitude double, + gb_latitude double, + gb_business_group_id character varying(50), + gb_ptz_type integer, + gb_position_type integer, + gb_room_type integer, + gb_use_type integer, + gb_supply_light_type integer, + gb_direction_type integer, + gb_resolution character varying(255), + gb_download_speed character varying(255), + gb_svc_space_support_mod integer, + gb_svc_time_support_mode integer, + record_plan_id integer, + data_type integer not null, + data_device_id integer not null, + gps_speed double precision, + gps_altitude double precision, + gps_direction double precision, + enable_broadcast integer default 0, + index (data_type), + index (data_device_id), + constraint uk_wvp_unique_channel unique (gb_device_id) +); + +drop table IF EXISTS wvp_media_server; +create table IF NOT EXISTS wvp_media_server +( + id character varying(255) primary key, + ip character varying(50), + hook_ip character varying(50), + sdp_ip character varying(50), + stream_ip character varying(50), + http_port integer, + http_ssl_port integer, + rtmp_port integer, + rtmp_ssl_port integer, + rtp_proxy_port integer, + rtsp_port integer, + rtsp_ssl_port integer, + flv_port integer, + flv_ssl_port integer, + mp4_port integer, + mp4_ssl_port integer, + ws_flv_port integer, + ws_flv_ssl_port integer, + jtt_proxy_port integer, + auto_config bool default false, + secret character varying(50), + type character varying(50) default 'zlm', + rtp_enable bool default false, + rtp_port_range character varying(50), + send_rtp_port_range character varying(50), + record_assist_port integer, + default_server bool default false, + create_time character varying(50), + update_time character varying(50), + hook_alive_interval integer, + record_path character varying(255), + record_day integer default 7, + transcode_suffix character varying(255), + server_id character varying(50), + constraint uk_media_server_unique_ip_http_port unique (ip, http_port, server_id) +); + +drop table IF EXISTS wvp_platform; +create table IF NOT EXISTS wvp_platform +( + id serial primary key, + enable bool default false, + name character varying(255), + server_gb_id character varying(50), + server_gb_domain character varying(50), + server_ip character varying(50), + server_port integer, + device_gb_id character varying(50), + device_ip character varying(50), + device_port character varying(50), + username character varying(255), + password character varying(50), + expires character varying(50), + keep_timeout character varying(50), + transport character varying(50), + civil_code character varying(50), + manufacturer character varying(255), + model character varying(255), + address character varying(255), + character_set character varying(50), + ptz bool default false, + rtcp bool default false, + status bool default false, + catalog_group integer, + register_way integer, + secrecy integer, + create_time character varying(50), + update_time character varying(50), + as_message_channel bool default false, + catalog_with_platform integer default 1, + catalog_with_group integer default 1, + catalog_with_region integer default 1, + auto_push_channel bool default true, + send_stream_ip character varying(50), + server_id character varying(50), + constraint uk_platform_unique_server_gb_id unique (server_gb_id) +); + +drop table IF EXISTS wvp_platform_channel; +create table IF NOT EXISTS wvp_platform_channel +( + id serial primary key, + platform_id integer, + device_channel_id integer, + custom_device_id character varying(50), + custom_name character varying(255), + custom_manufacturer character varying(50), + custom_model character varying(50), + custom_owner character varying(50), + custom_civil_code character varying(50), + custom_block character varying(50), + custom_address character varying(50), + custom_parental integer, + custom_parent_id character varying(50), + custom_safety_way integer, + custom_register_way integer, + custom_cert_num character varying(50), + custom_certifiable integer, + custom_err_code integer, + custom_end_time character varying(50), + custom_secrecy integer, + custom_ip_address character varying(50), + custom_port integer, + custom_password character varying(255), + custom_status character varying(50), + custom_longitude double precision, + custom_latitude double precision, + custom_ptz_type integer, + custom_position_type integer, + custom_room_type integer, + custom_use_type integer, + custom_supply_light_type integer, + custom_direction_type integer, + custom_resolution character varying(255), + custom_business_group_id character varying(255), + custom_download_speed character varying(255), + custom_svc_space_support_mod integer, + custom_svc_time_support_mode integer, + constraint uk_platform_gb_channel_platform_id_catalog_id_device_channel_id unique (platform_id, device_channel_id), + constraint uk_platform_gb_channel_device_id unique (custom_device_id) +); + +drop table IF EXISTS wvp_platform_group; +create table IF NOT EXISTS wvp_platform_group +( + id serial primary key, + platform_id integer, + group_id integer, + constraint uk_wvp_platform_group_platform_id_group_id unique (platform_id, group_id) +); + +drop table IF EXISTS wvp_platform_region; +create table IF NOT EXISTS wvp_platform_region +( + id serial primary key, + platform_id integer, + region_id integer, + constraint uk_wvp_platform_region_platform_id_group_id unique (platform_id, region_id) +); + +drop table IF EXISTS wvp_stream_proxy; +create table IF NOT EXISTS wvp_stream_proxy +( + id serial primary key, + type character varying(50), + app character varying(255), + stream character varying(255), + src_url character varying(255), + timeout integer, + ffmpeg_cmd_key character varying(255), + rtsp_type character varying(50), + media_server_id character varying(50), + enable_audio bool default false, + enable_mp4 bool default false, + pulling bool default false, + enable bool default false, + create_time character varying(50), + name character varying(255), + update_time character varying(50), + stream_key character varying(255), + server_id character varying(50), + enable_disable_none_reader bool default false, + relates_media_server_id character varying(50), + constraint uk_stream_proxy_app_stream unique (app, stream) +); + +drop table IF EXISTS wvp_stream_push; +create table IF NOT EXISTS wvp_stream_push +( + id serial primary key, + app character varying(255), + stream character varying(255), + create_time character varying(50), + media_server_id character varying(50), + server_id character varying(50), + push_time character varying(50), + status bool default false, + update_time character varying(50), + pushing bool default false, + self bool default false, + start_offline_push bool default true, + constraint uk_stream_push_app_stream unique (app, stream) +); + +drop table IF EXISTS wvp_cloud_record; +create table IF NOT EXISTS wvp_cloud_record +( + id serial primary key, + app character varying(255), + stream character varying(255), + call_id character varying(255), + start_time bigint, + end_time bigint, + media_server_id character varying(50), + server_id character varying(50), + file_name character varying(255), + folder character varying(500), + file_path character varying(500), + collect bool default false, + file_size bigint, + time_len double precision +); + +drop table IF EXISTS wvp_user; +create table IF NOT EXISTS wvp_user +( + id serial primary key, + username character varying(255), + password character varying(255), + role_id integer, + create_time character varying(50), + update_time character varying(50), + push_key character varying(50), + constraint uk_user_username unique (username) +); + +drop table IF EXISTS wvp_user_role; +create table IF NOT EXISTS wvp_user_role +( + id serial primary key, + name character varying(50), + authority character varying(50), + create_time character varying(50), + update_time character varying(50) +); + + +drop table IF EXISTS wvp_user_api_key; +create table IF NOT EXISTS wvp_user_api_key +( + id serial primary key, + user_id bigint, + app character varying(255), + api_key text, + expired_at bigint, + remark character varying(255), + enable bool default true, + create_time character varying(50), + update_time character varying(50) +); + + +/*初始数据*/ +INSERT INTO wvp_user +VALUES (1, 'admin', '21232f297a57a5a743894a0e4a801fc3', 1, '2021-04-13 14:14:57', '2021-04-13 14:14:57', + '3e80d1762a324d5b0ff636e0bd16f1e3'); +INSERT INTO wvp_user_role +VALUES (1, 'admin', '0', '2021-04-13 14:14:57', '2021-04-13 14:14:57'); + +drop table IF EXISTS wvp_common_group; +create table IF NOT EXISTS wvp_common_group +( + id serial primary key, + device_id varchar(50) NOT NULL, + name varchar(255) NOT NULL, + parent_id int, + parent_device_id varchar(50) DEFAULT NULL, + business_group varchar(50) NOT NULL, + create_time varchar(50) NOT NULL, + update_time varchar(50) NOT NULL, + civil_code varchar(50) default null, + alias varchar(255) default null, + constraint uk_common_group_device_platform unique (device_id) +); + +drop table IF EXISTS wvp_common_region; +create table IF NOT EXISTS wvp_common_region +( + id serial primary key, + device_id varchar(50) NOT NULL, + name varchar(255) NOT NULL, + parent_id int, + parent_device_id varchar(50) DEFAULT NULL, + create_time varchar(50) NOT NULL, + update_time varchar(50) NOT NULL, + constraint uk_common_region_device_id unique (device_id) +); + +drop table IF EXISTS wvp_record_plan; +create table IF NOT EXISTS wvp_record_plan +( + id serial primary key, + snap bool default false, + name varchar(255) NOT NULL, + create_time character varying(50), + update_time character varying(50) +); + +drop table IF EXISTS wvp_record_plan_item; +create table IF NOT EXISTS wvp_record_plan_item +( + id serial primary key, + start int, + stop int, + week_day int, + plan_id int, + create_time character varying(50), + update_time character varying(50) +); + +drop table IF EXISTS wvp_jt_terminal; +create table IF NOT EXISTS wvp_jt_terminal ( + id serial primary key, + phone_number character varying(50), + terminal_id character varying(50), + province_id character varying(50), + province_text character varying(100), + city_id character varying(50), + city_text character varying(100), + maker_id character varying(50), + model character varying(50), + plate_color character varying(50), + plate_no character varying(50), + longitude double precision, + latitude double precision, + status bool default false, + register_time character varying(50) default null, + update_time character varying(50) not null, + create_time character varying(50) not null, + geo_coord_sys character varying(50), + media_server_id character varying(50) default 'auto', + sdp_ip character varying(50), + constraint uk_jt_device_id_device_id unique (id, phone_number) +); + +drop table IF EXISTS wvp_jt_channel; +create table IF NOT EXISTS wvp_jt_channel ( + id serial primary key, + terminal_db_id integer, + channel_id integer, + has_audio bool default false, + name character varying(255), + update_time character varying(50) not null, + create_time character varying(50) not null, + constraint uk_jt_channel_id_device_id unique (terminal_db_id, channel_id) +); + diff --git a/数据库/2.7.4/初始化-postgresql-kingbase-2.7.4.sql b/数据库/2.7.4/初始化-postgresql-kingbase-2.7.4.sql new file mode 100644 index 0000000..a437b58 --- /dev/null +++ b/数据库/2.7.4/初始化-postgresql-kingbase-2.7.4.sql @@ -0,0 +1,508 @@ +/*建表*/ +drop table IF EXISTS wvp_device; +create table IF NOT EXISTS wvp_device +( + id serial primary key, + device_id character varying(50) not null, + name character varying(255), + manufacturer character varying(255), + model character varying(255), + firmware character varying(255), + transport character varying(50), + stream_mode character varying(50), + on_line bool default false, + register_time character varying(50), + keepalive_time character varying(50), + ip character varying(50), + create_time character varying(50), + update_time character varying(50), + port integer, + expires integer, + subscribe_cycle_for_catalog integer DEFAULT 0, + subscribe_cycle_for_mobile_position integer DEFAULT 0, + mobile_position_submission_interval integer DEFAULT 5, + subscribe_cycle_for_alarm integer DEFAULT 0, + host_address character varying(50), + charset character varying(50), + ssrc_check bool default false, + geo_coord_sys character varying(50), + media_server_id character varying(50) default 'auto', + custom_name character varying(255), + sdp_ip character varying(50), + local_ip character varying(50), + password character varying(255), + as_message_channel bool default false, + heart_beat_interval integer, + heart_beat_count integer, + position_capability integer, + broadcast_push_after_ack bool default false, + server_id character varying(50), + constraint uk_device_device unique (device_id) +); + +drop table IF EXISTS wvp_device_alarm; +create table IF NOT EXISTS wvp_device_alarm +( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + alarm_priority character varying(50), + alarm_method character varying(50), + alarm_time character varying(50), + alarm_description character varying(255), + longitude double precision, + latitude double precision, + alarm_type character varying(50), + create_time character varying(50) not null +); + +drop table IF EXISTS wvp_device_mobile_position; +create table IF NOT EXISTS wvp_device_mobile_position +( + id serial primary key, + device_id character varying(50) not null, + channel_id character varying(50) not null, + device_name character varying(255), + time character varying(50), + longitude double precision, + latitude double precision, + altitude double precision, + speed double precision, + direction double precision, + report_source character varying(50), + create_time character varying(50) +); + +drop table IF EXISTS wvp_device_channel; +create table IF NOT EXISTS wvp_device_channel +( + id serial primary key, + device_id character varying(50), + name character varying(255), + manufacturer character varying(50), + model character varying(50), + owner character varying(50), + civil_code character varying(50), + block character varying(50), + address character varying(50), + parental integer, + parent_id character varying(50), + safety_way integer, + register_way integer, + cert_num character varying(50), + certifiable integer, + err_code integer, + end_time character varying(50), + secrecy integer, + ip_address character varying(50), + port integer, + password character varying(255), + status character varying(50), + longitude double precision, + latitude double precision, + ptz_type integer, + position_type integer, + room_type integer, + use_type integer, + supply_light_type integer, + direction_type integer, + resolution character varying(255), + business_group_id character varying(255), + download_speed character varying(255), + svc_space_support_mod integer, + svc_time_support_mode integer, + create_time character varying(50) not null, + update_time character varying(50) not null, + sub_count integer, + stream_id character varying(255), + has_audio bool default false, + gps_time character varying(50), + stream_identification character varying(50), + channel_type int default 0 not null, + map_level int default 0, + gb_device_id character varying(50), + gb_name character varying(255), + gb_manufacturer character varying(255), + gb_model character varying(255), + gb_owner character varying(255), + gb_civil_code character varying(255), + gb_block character varying(255), + gb_address character varying(255), + gb_parental integer, + gb_parent_id character varying(255), + gb_safety_way integer, + gb_register_way integer, + gb_cert_num character varying(50), + gb_certifiable integer, + gb_err_code integer, + gb_end_time character varying(50), + gb_secrecy integer, + gb_ip_address character varying(50), + gb_port integer, + gb_password character varying(50), + gb_status character varying(50), + gb_longitude double precision, + gb_latitude double precision, + gb_business_group_id character varying(50), + gb_ptz_type integer, + gb_position_type integer, + gb_room_type integer, + gb_use_type integer, + gb_supply_light_type integer, + gb_direction_type integer, + gb_resolution character varying(255), + gb_download_speed character varying(255), + gb_svc_space_support_mod integer, + gb_svc_time_support_mode integer, + record_plan_id integer, + data_type integer not null, + data_device_id integer not null, + gps_speed double precision, + gps_altitude double precision, + gps_direction double precision, + enable_broadcast integer default 0, + constraint uk_wvp_unique_channel unique (gb_device_id) +); + +CREATE INDEX idx_data_type ON wvp_device_channel (data_type); +CREATE INDEX idx_data_device_id ON wvp_device_channel (data_device_id); + +drop table IF EXISTS wvp_media_server; +create table IF NOT EXISTS wvp_media_server +( + id character varying(255) primary key, + ip character varying(50), + hook_ip character varying(50), + sdp_ip character varying(50), + stream_ip character varying(50), + http_port integer, + http_ssl_port integer, + rtmp_port integer, + rtmp_ssl_port integer, + rtp_proxy_port integer, + rtsp_port integer, + rtsp_ssl_port integer, + flv_port integer, + flv_ssl_port integer, + mp4_port integer, + mp4_ssl_port integer, + ws_flv_port integer, + ws_flv_ssl_port integer, + jtt_proxy_port integer, + auto_config bool default false, + secret character varying(50), + type character varying(50) default 'zlm', + rtp_enable bool default false, + rtp_port_range character varying(50), + send_rtp_port_range character varying(50), + record_assist_port integer, + default_server bool default false, + create_time character varying(50), + update_time character varying(50), + hook_alive_interval integer, + record_path character varying(255), + record_day integer default 7, + transcode_suffix character varying(255), + server_id character varying(50), + constraint uk_media_server_unique_ip_http_port unique (ip, http_port, server_id) +); + +drop table IF EXISTS wvp_platform; +create table IF NOT EXISTS wvp_platform +( + id serial primary key, + enable bool default false, + name character varying(255), + server_gb_id character varying(50), + server_gb_domain character varying(50), + server_ip character varying(50), + server_port integer, + device_gb_id character varying(50), + device_ip character varying(50), + device_port character varying(50), + username character varying(255), + password character varying(50), + expires character varying(50), + keep_timeout character varying(50), + transport character varying(50), + civil_code character varying(50), + manufacturer character varying(255), + model character varying(255), + address character varying(255), + character_set character varying(50), + ptz bool default false, + rtcp bool default false, + status bool default false, + catalog_group integer, + register_way integer, + secrecy integer, + create_time character varying(50), + update_time character varying(50), + as_message_channel bool default false, + catalog_with_platform integer default 1, + catalog_with_group integer default 1, + catalog_with_region integer default 1, + auto_push_channel bool default true, + send_stream_ip character varying(50), + server_id character varying(50), + constraint uk_platform_unique_server_gb_id unique (server_gb_id) +); + +drop table IF EXISTS wvp_platform_channel; +create table IF NOT EXISTS wvp_platform_channel +( + id serial primary key, + platform_id integer, + device_channel_id integer, + custom_device_id character varying(50), + custom_name character varying(255), + custom_manufacturer character varying(50), + custom_model character varying(50), + custom_owner character varying(50), + custom_civil_code character varying(50), + custom_block character varying(50), + custom_address character varying(50), + custom_parental integer, + custom_parent_id character varying(50), + custom_safety_way integer, + custom_register_way integer, + custom_cert_num character varying(50), + custom_certifiable integer, + custom_err_code integer, + custom_end_time character varying(50), + custom_secrecy integer, + custom_ip_address character varying(50), + custom_port integer, + custom_password character varying(255), + custom_status character varying(50), + custom_longitude double precision, + custom_latitude double precision, + custom_ptz_type integer, + custom_position_type integer, + custom_room_type integer, + custom_use_type integer, + custom_supply_light_type integer, + custom_direction_type integer, + custom_resolution character varying(255), + custom_business_group_id character varying(255), + custom_download_speed character varying(255), + custom_svc_space_support_mod integer, + custom_svc_time_support_mode integer, + constraint uk_platform_gb_channel_platform_id_catalog_id_device_channel_id unique (platform_id, device_channel_id), + constraint uk_platform_gb_channel_device_id unique (custom_device_id) +); + +drop table IF EXISTS wvp_platform_group; +create table IF NOT EXISTS wvp_platform_group +( + id serial primary key, + platform_id integer, + group_id integer, + constraint uk_wvp_platform_group_platform_id_group_id unique (platform_id, group_id) +); + +drop table IF EXISTS wvp_platform_region; +create table IF NOT EXISTS wvp_platform_region +( + id serial primary key, + platform_id integer, + region_id integer, + constraint uk_wvp_platform_region_platform_id_group_id unique (platform_id, region_id) +); + +drop table IF EXISTS wvp_stream_proxy; +create table IF NOT EXISTS wvp_stream_proxy +( + id serial primary key, + type character varying(50), + app character varying(255), + stream character varying(255), + src_url character varying(255), + timeout integer, + ffmpeg_cmd_key character varying(255), + rtsp_type character varying(50), + media_server_id character varying(50), + enable_audio bool default false, + enable_mp4 bool default false, + pulling bool default false, + enable bool default false, + create_time character varying(50), + name character varying(255), + update_time character varying(50), + stream_key character varying(255), + server_id character varying(50), + enable_disable_none_reader bool default false, + relates_media_server_id character varying(50), + constraint uk_stream_proxy_app_stream unique (app, stream) +); + +drop table IF EXISTS wvp_stream_push; +create table IF NOT EXISTS wvp_stream_push +( + id serial primary key, + app character varying(255), + stream character varying(255), + create_time character varying(50), + media_server_id character varying(50), + server_id character varying(50), + push_time character varying(50), + status bool default false, + update_time character varying(50), + pushing bool default false, + self bool default false, + start_offline_push bool default true, + constraint uk_stream_push_app_stream unique (app, stream) +); + +drop table IF EXISTS wvp_cloud_record; +create table IF NOT EXISTS wvp_cloud_record +( + id serial primary key, + app character varying(255), + stream character varying(255), + call_id character varying(255), + start_time int8, + end_time int8, + media_server_id character varying(50), + server_id character varying(50), + file_name character varying(255), + folder character varying(500), + file_path character varying(500), + collect bool default false, + file_size int8, + time_len double precision +); + +drop table IF EXISTS wvp_user; +create table IF NOT EXISTS wvp_user +( + id serial primary key, + username character varying(255), + password character varying(255), + role_id integer, + create_time character varying(50), + update_time character varying(50), + push_key character varying(50), + constraint uk_user_username unique (username) +); + +drop table IF EXISTS wvp_user_role; +create table IF NOT EXISTS wvp_user_role +( + id serial primary key, + name character varying(50), + authority character varying(50), + create_time character varying(50), + update_time character varying(50) +); + + +drop table IF EXISTS wvp_user_api_key; +create table IF NOT EXISTS wvp_user_api_key +( + id serial primary key, + user_id int8, + app character varying(255), + api_key text, + expired_at int8, + remark character varying(255), + enable bool default true, + create_time character varying(50), + update_time character varying(50) +); + + +/*初始数据*/ +INSERT INTO wvp_user +VALUES (1, 'admin', '21232f297a57a5a743894a0e4a801fc3', 1, '2021-04-13 14:14:57', '2021-04-13 14:14:57', + '3e80d1762a324d5b0ff636e0bd16f1e3'); +INSERT INTO wvp_user_role +VALUES (1, 'admin', '0', '2021-04-13 14:14:57', '2021-04-13 14:14:57'); + +drop table IF EXISTS wvp_common_group; +create table IF NOT EXISTS wvp_common_group +( + id serial primary key, + device_id varchar(50) NOT NULL, + name varchar(255) NOT NULL, + parent_id int, + parent_device_id varchar(50) DEFAULT NULL, + business_group varchar(50) NOT NULL, + create_time varchar(50) NOT NULL, + update_time varchar(50) NOT NULL, + civil_code varchar(50) default null, + alias varchar(255) default null, + constraint uk_common_group_device_platform unique (device_id) +); + +drop table IF EXISTS wvp_common_region; +create table IF NOT EXISTS wvp_common_region +( + id serial primary key, + device_id varchar(50) NOT NULL, + name varchar(255) NOT NULL, + parent_id int, + parent_device_id varchar(50) DEFAULT NULL, + create_time varchar(50) NOT NULL, + update_time varchar(50) NOT NULL, + constraint uk_common_region_device_id unique (device_id) +); + +drop table IF EXISTS wvp_record_plan; +create table IF NOT EXISTS wvp_record_plan +( + id serial primary key, + snap bool default false, + name varchar(255) NOT NULL, + create_time character varying(50), + update_time character varying(50) +); + +drop table IF EXISTS wvp_record_plan_item; +create table IF NOT EXISTS wvp_record_plan_item +( + id serial primary key, + "start" int, + stop int, + week_day int, + plan_id int, + create_time character varying(50), + update_time character varying(50) +); + +drop table IF EXISTS wvp_jt_terminal; +create table IF NOT EXISTS wvp_jt_terminal ( + id serial primary key, + phone_number character varying(50), + terminal_id character varying(50), + province_id character varying(50), + province_text character varying(100), + city_id character varying(50), + city_text character varying(100), + maker_id character varying(50), + model character varying(50), + plate_color character varying(50), + plate_no character varying(50), + longitude double precision, + latitude double precision, + status bool default false, + register_time character varying(50) default null, + update_time character varying(50) not null, + create_time character varying(50) not null, + geo_coord_sys character varying(50), + media_server_id character varying(50) default 'auto', + sdp_ip character varying(50), + constraint uk_jt_device_id_device_id unique (id, phone_number) +); +drop table IF EXISTS wvp_jt_channel; +create table IF NOT EXISTS wvp_jt_channel ( + id serial primary key, + terminal_db_id integer, + channel_id integer, + has_audio bool default false, + name character varying(255), + update_time character varying(50) not null, + create_time character varying(50) not null, + constraint uk_jt_channel_id_device_id unique (terminal_db_id, channel_id) +); + diff --git a/数据库/2.7.4/更新-mysql-2.7.4.sql b/数据库/2.7.4/更新-mysql-2.7.4.sql new file mode 100644 index 0000000..7cca80e --- /dev/null +++ b/数据库/2.7.4/更新-mysql-2.7.4.sql @@ -0,0 +1,127 @@ +drop table IF EXISTS wvp_jt_terminal; +create table IF NOT EXISTS wvp_jt_terminal ( + id serial primary key, + phone_number character varying(50), + terminal_id character varying(50), + province_id character varying(50), + province_text character varying(100), + city_id character varying(50), + city_text character varying(100), + maker_id character varying(50), + model character varying(50), + plate_color character varying(50), + plate_no character varying(50), + longitude double precision, + latitude double precision, + status bool default false, + register_time character varying(50) default null, + update_time character varying(50) not null, + create_time character varying(50) not null, + geo_coord_sys character varying(50), + media_server_id character varying(50) default 'auto', + sdp_ip character varying(50), + constraint uk_jt_device_id_device_id unique (id, phone_number) +); + +drop table IF EXISTS wvp_jt_channel; +create table IF NOT EXISTS wvp_jt_channel ( + id serial primary key, + terminal_db_id integer, + channel_id integer, + has_audio bool default false, + name character varying(255), + update_time character varying(50) not null, + create_time character varying(50) not null, + constraint uk_jt_channel_id_device_id unique (terminal_db_id, channel_id) +); + +/* +* 20250708 +*/ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20250708`() +BEGIN + IF NOT EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'jtt_proxy_port') + THEN + ALTER TABLE wvp_media_server ADD jtt_proxy_port integer; + END IF; +END; // +call wvp_20250708(); +DROP PROCEDURE wvp_20250708; +DELIMITER ; + +/* +* 20250917 +*/ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20250917`() +BEGIN + IF NOT EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'mp4_port') + THEN + ALTER TABLE wvp_media_server ADD mp4_port integer; + END IF; + + IF NOT EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_media_server' and column_name = 'mp4_ssl_port') + THEN + ALTER TABLE wvp_media_server ADD mp4_ssl_port integer; + END IF; +END; // +call wvp_20250917(); +DROP PROCEDURE wvp_20250917; +DELIMITER ; + +/* +* 20250924 +*/ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20250924`() +BEGIN + IF NOT EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'enable_broadcast') + THEN + ALTER TABLE wvp_device_channel ADD enable_broadcast integer default 0; + END IF; + + IF NOT EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_device_channel' and column_name = 'map_level') + THEN + ALTER TABLE wvp_device_channel ADD map_level integer default 0; + END IF; + + IF NOT EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_common_group' and column_name = 'alias') + THEN + ALTER TABLE wvp_common_group ADD alias varchar(255) default null; + END IF; +END; // +call wvp_20250924(); +DROP PROCEDURE wvp_20250924; +DELIMITER ; + + +/* +* 20251027 +*/ +DELIMITER // -- 重定义分隔符避免分号冲突 +CREATE PROCEDURE `wvp_20251027`() +BEGIN + IF EXISTS (SELECT column_name FROM information_schema.columns + WHERE TABLE_SCHEMA = (SELECT DATABASE()) and table_name = 'wvp_stream_proxy' and column_name = 'enable_remove_none_reader') + THEN + ALTER TABLE wvp_stream_proxy DROP enable_remove_none_reader; + END IF; +END; // +call wvp_20251027(); +DROP PROCEDURE wvp_20251027; +DELIMITER ; + + + + + + + + diff --git a/数据库/2.7.4/更新-postgresql-kingbase-2.7.4.sql b/数据库/2.7.4/更新-postgresql-kingbase-2.7.4.sql new file mode 100644 index 0000000..2dedb90 --- /dev/null +++ b/数据库/2.7.4/更新-postgresql-kingbase-2.7.4.sql @@ -0,0 +1,44 @@ +drop table IF EXISTS wvp_jt_terminal; +create table IF NOT EXISTS wvp_jt_terminal ( + id serial primary key, + phone_number character varying(50), + terminal_id character varying(50), + province_id character varying(50), + province_text character varying(100), + city_id character varying(50), + city_text character varying(100), + maker_id character varying(50), + model character varying(50), + plate_color character varying(50), + plate_no character varying(50), + longitude double precision, + latitude double precision, + status bool default false, + register_time character varying(50) default null, + update_time character varying(50) not null, + create_time character varying(50) not null, + geo_coord_sys character varying(50), + media_server_id character varying(50) default 'auto', + sdp_ip character varying(50), + constraint uk_jt_device_id_device_id unique (id, phone_number) +); +drop table IF EXISTS wvp_jt_channel; +create table IF NOT EXISTS wvp_jt_channel ( + id serial primary key, + terminal_db_id integer, + channel_id integer, + has_audio bool default false, + name character varying(255), + update_time character varying(50) not null, + create_time character varying(50) not null, + constraint uk_jt_channel_id_device_id unique (terminal_db_id, channel_id) +); + +ALTER table wvp_media_server ADD COLUMN IF NOT EXISTS jtt_proxy_port integer; +ALTER table wvp_media_server ADD COLUMN IF NOT EXISTS mp4_port integer; +ALTER table wvp_media_server ADD COLUMN IF NOT EXISTS mp4_ssl_port integer; + +ALTER table wvp_device_channel ADD COLUMN IF NOT EXISTS enable_broadcast integer default 0; +ALTER table wvp_device_channel ADD COLUMN IF NOT EXISTS map_level integer default 0; +ALTER table wvp_common_group ADD COLUMN IF NOT EXISTS alias varchar(255) default null; +ALTER table wvp_stream_proxy DROP COLUMN IF NOT EXISTS enable_remove_none_reader; diff --git a/数据库/old/2.6.6-2.6.7更新.sql b/数据库/old/2.6.6-2.6.7更新.sql new file mode 100755 index 0000000..285d573 --- /dev/null +++ b/数据库/old/2.6.6-2.6.7更新.sql @@ -0,0 +1,13 @@ +alter table device + add asMessageChannel int default 0; + +alter table parent_platform + add asMessageChannel int default 0; + +alter table device + add mediaServerId varchar(50) default null; + +ALTER TABLE device + ADD COLUMN `switchPrimarySubStream` bit(1) NOT NULL DEFAULT b'0' COMMENT '开启主子码流切换的开关(0-不开启,1-开启)现在已知支持设备为 大华、TP——LINK全系设备' AFTER `keepalive_interval_time` + + diff --git a/数据库/old/2.6.8升级2.6.9.sql b/数据库/old/2.6.8升级2.6.9.sql new file mode 100644 index 0000000..49436ce --- /dev/null +++ b/数据库/old/2.6.8升级2.6.9.sql @@ -0,0 +1,502 @@ +alter table device + change deviceId device_id varchar(50) not null; + +alter table device + change streamMode stream_mode varchar(50) null; + +alter table device + change registerTime register_time varchar(50) null; + +alter table device + change keepaliveTime keepalive_time varchar(50) null; + +alter table device + change createTime create_time varchar(50) not null; + +alter table device + change updateTime update_time varchar(50) not null; + +alter table device + change subscribeCycleForCatalog subscribe_cycle_for_catalog bool default false; + +alter table device + change subscribeCycleForMobilePosition subscribe_cycle_for_mobile_position bool default false; + +alter table device + change mobilePositionSubmissionInterval mobile_position_submission_interval int default 5 not null; + +alter table device + change subscribeCycleForAlarm subscribe_cycle_for_alarm bool default false; + +alter table device + change hostAddress host_address varchar(50) null; + +alter table device + change ssrcCheck ssrc_check bool default false; + +alter table device + change geoCoordSys geo_coord_sys varchar(50) not null; + +alter table device + drop column treeType; + +alter table device + change mediaServerId media_server_id varchar(50) default 'auto' null; + +alter table device + change sdpIp sdp_ip varchar(50) null; + +alter table device + change localIp local_ip varchar(50) null; + +alter table device + change asMessageChannel as_message_channel bool default false; + +alter table device + change keepaliveIntervalTime keepalive_interval_time int null; + +alter table device + change online on_line varchar(50) null; + +alter table device + add COLUMN switch_primary_sub_stream bool default false comment '开启主子码流切换的开关(0-不开启,1-开启)现在已知支持设备为 大华、TP——LINK全系设备' + + +alter table device_alarm + change deviceId device_id varchar(50) not null; + +alter table device_alarm + change channelId channel_id varchar(50) not null; + +alter table device_alarm + change alarmPriority alarm_priority varchar(50) not null; + +alter table device_alarm + change alarmMethod alarm_method varchar(50) null; + +alter table device_alarm + change alarmTime alarm_time varchar(50) not null; + +alter table device_alarm + change alarmDescription alarm_description varchar(255) null; + +alter table device_alarm + change alarmType alarm_type varchar(50) null; + +alter table device_alarm + change createTime create_time varchar(50) null; + +alter table device_channel + change channelId channel_id varchar(50) not null; + +alter table device_channel + change civilCode civil_code varchar(50) null; + +alter table device_channel + change parentId parent_id varchar(50) null; + +alter table device_channel + change safetyWay safety_way int null; + +alter table device_channel + change registerWay register_way int null; + +alter table device_channel + change certNum cert_num varchar(50) null; + +alter table device_channel + change errCode err_code int null; + +alter table device_channel + change endTime end_time varchar(50) null; + +alter table device_channel + change ipAddress ip_address varchar(50) null; + +alter table device_channel + change PTZType ptz_type int null; + +alter table device_channel + change status status bool default false; + +alter table device_channel + change streamId stream_id varchar(255) null; + +alter table device_channel + change deviceId device_id varchar(50) not null; + + +alter table device_channel + change hasAudio has_audio bool default false; + +alter table device_channel + change createTime create_time varchar(50) not null; + +alter table device_channel + change updateTime update_time varchar(50) not null; + +alter table device_channel + change subCount sub_count int default 0 null; + +alter table device_channel + change longitudeGcj02 longitude_gcj02 double null; + +alter table device_channel + change latitudeGcj02 latitude_gcj02 double null; + +alter table device_channel + change longitudeWgs84 longitude_wgs84 double null; + +alter table device_channel + change latitudeWgs84 latitude_wgs84 double null; + +alter table device_channel + change businessGroupId business_group_id varchar(50) null; + +alter table device_channel + change gpsTime gps_time varchar(50) null; + +alter table device_mobile_position + change deviceId device_id varchar(50) not null; + +alter table device_mobile_position + change channelId channel_id varchar(50) not null; + +alter table device_mobile_position + change deviceName device_name varchar(255) null; + +alter table device_mobile_position + change reportSource report_source varchar(50) null; + +alter table device_mobile_position + change longitudeGcj02 longitude_gcj02 double null; + +alter table device_mobile_position + change latitudeGcj02 latitude_gcj02 double null; + +alter table device_mobile_position + change longitudeWgs84 longitude_wgs84 double null; + +alter table device_mobile_position + change latitudeWgs84 latitude_wgs84 double null; + +alter table device_mobile_position + change createTime create_time varchar(50) null; + +alter table gb_stream + change gbStreamId gb_stream_id int auto_increment; + +alter table gb_stream + change gbId gb_id varchar(50) not null; + +alter table gb_stream + change streamType stream_type varchar(50) null; + +alter table gb_stream + change mediaServerId media_server_id varchar(50) null; + +alter table gb_stream + change createTime create_time varchar(50) null; + +alter table log + change createTime create_time varchar(50) not null; + +alter table media_server + change hookIp hook_ip varchar(50) not null; + +alter table media_server + add send_rtp_port_range varchar(50) not null; + +alter table media_server + add column send_rtp_port_range varchar(50) default null; + +alter table media_server + change sdpIp sdp_ip varchar(50) not null; + +alter table media_server + change streamIp stream_ip varchar(50) not null; + +alter table media_server + change httpPort http_port int not null; + +alter table media_server + change httpSSlPort http_ssl_port int not null; + +alter table media_server + change rtmpPort rtmp_port int not null; + +alter table media_server + change rtmpSSlPort rtmp_ssl_port int not null; + +alter table media_server + change rtpProxyPort rtp_proxy_port int not null; + +alter table media_server + change rtspPort rtsp_port int not null; + +alter table media_server + change rtspSSLPort rtsp_ssl_port int not null; + +alter table media_server + change autoConfig auto_config bool default true; + +alter table media_server + change rtpEnable rtp_enable bool default false; + +alter table media_server + change rtpPortRange rtp_port_range varchar(50) not null; + +alter table media_server + change recordAssistPort record_assist_port int not null; + +alter table media_server + change defaultServer default_server bool default false; + +alter table media_server + change createTime create_time varchar(50) not null; + +alter table media_server + change updateTime update_time varchar(50) not null; + +alter table media_server + change hookAliveInterval hook_alive_interval int not null; + +alter table parent_platform + change serverGBId server_gb_id varchar(50) not null; + +alter table parent_platform + change serverGBDomain server_gb_domain varchar(50) null; + +alter table parent_platform + change serverIP server_ip varchar(50) null; + +alter table parent_platform + change serverPort server_port int null; + +alter table parent_platform + change deviceGBId device_gb_id varchar(50) not null; + +alter table parent_platform + change deviceIp device_ip varchar(50) null; + +alter table parent_platform + change devicePort device_port varchar(50) null; + +alter table parent_platform + change keepTimeout keep_timeout varchar(50) null; + +alter table parent_platform + change characterSet character_set varchar(50) null; + +alter table parent_platform + change catalogId catalog_id varchar(50) not null; + +alter table parent_platform + change startOfflinePush start_offline_push bool default false; + +alter table parent_platform + change administrativeDivision administrative_division varchar(50) not null; + +alter table parent_platform + change catalogGroup catalog_group int default 1 null; + +alter table parent_platform + change createTime create_time varchar(50) null; + +alter table parent_platform + change updateTime update_time varchar(50) null; + +alter table parent_platform + drop column treeType; + +alter table parent_platform + change asMessageChannel as_message_channel bool default false; + +alter table parent_platform + change enable enable bool default false; + +alter table parent_platform + change ptz ptz bool default false; + +alter table parent_platform + change rtcp rtcp bool default false; + +alter table parent_platform + change status status bool default false; + +alter table parent_platform + change status status bool default false; + +alter table platform_catalog + change platformId platform_id varchar(50) not null; + +alter table platform_catalog + change parentId parent_id varchar(50) null; + +alter table platform_catalog + change civilCode civil_code varchar(50) null; + +alter table platform_catalog + change businessGroupId business_group_id varchar(50) null; + +alter table platform_gb_channel + change platformId platform_id varchar(50) not null; + +alter table platform_gb_channel + change catalogId catalog_id varchar(50) not null; + +alter table platform_gb_channel + change deviceChannelId device_channel_id int not null; + +alter table platform_gb_stream + change platformId platform_id varchar(50) not null; + +alter table platform_gb_stream + change catalogId catalog_id varchar(50) not null; + +alter table platform_gb_stream + change gbStreamId gb_stream_id int not null; + +alter table stream_proxy + change mediaServerId media_server_id varchar(50) null; + +alter table stream_proxy + change createTime create_time varchar(50) not null; + +alter table stream_proxy + change updateTime update_time varchar(50) null; + +alter table stream_proxy + change enable_remove_none_reader enable_remove_none_reader bool default false; + +alter table stream_proxy + change enable_disable_none_reader enable_disable_none_reader bool default false; + +alter table stream_proxy + change enable_audio enable_audio bool default false; + +alter table stream_proxy + change enable_mp4 enable_mp4 bool default false; + +alter table stream_proxy + change enable enable bool default false; + +alter table stream_push + change totalReaderCount total_reader_count varchar(50) null; + +alter table stream_push + change originType origin_type int null; + +alter table stream_push + change originTypeStr origin_type_str varchar(50) null; + +alter table stream_push + change createTime create_time varchar(50) null; + +alter table stream_push + change aliveSecond alive_second int null; + +alter table stream_push + change mediaServerId media_server_id varchar(50) null; + +alter table stream_push + change status status bool default false; + +alter table stream_push + change pushTime push_time varchar(50) null; + +alter table stream_push + change updateTime update_time varchar(50) null; + +alter table stream_push + change pushIng push_ing bool default false; + +alter table stream_push + change status status bool default false; + +alter table stream_push + change self self bool default false; + +alter table stream_push + drop column serverId; + + +alter table user + change roleId role_id int not null; + +alter table user + change createTime create_time varchar(50) not null; + +alter table user + change updateTime update_time varchar(50) not null; + +alter table user + change pushKey push_key varchar(50) null; + +alter table user_role + change createTime create_time varchar(50) not null; + +alter table user_role + change updateTime update_time varchar(50) not null; + +rename table device to wvp_device; +rename table device_alarm to wvp_device_alarm; +rename table device_channel to wvp_device_channel; +rename table device_mobile_position to wvp_device_mobile_position; +rename table gb_stream to wvp_gb_stream; +rename table log to wvp_log; +rename table media_server to wvp_media_server; +rename table parent_platform to wvp_platform; +rename table platform_catalog to wvp_platform_catalog; +rename table platform_gb_channel to wvp_platform_channel; +rename table platform_gb_stream to wvp_platform_gb_stream; +rename table stream_proxy to wvp_stream_proxy; +rename table stream_push to wvp_stream_push; +rename table user to wvp_user; +rename table user_role to wvp_user_role; + +alter table wvp_device add column broadcast_push_after_ack bool default false; +alter table wvp_device_channel add column custom_name varchar(255) null ; +alter table wvp_device_channel add column custom_longitude double null ; +alter table wvp_device_channel add column custom_latitude double null ; +alter table wvp_device_channel add column custom_ptz_type int null ; + +create table wvp_resources_tree ( + id serial primary key , + is_catalog bool default true, + device_channel_id integer , + gb_stream_id integer, + name character varying(255), + parentId integer, + path character varying(255) +); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/数据库/old/2.6.8补丁更新.sql b/数据库/old/2.6.8补丁更新.sql new file mode 100644 index 0000000..8ce9d54 --- /dev/null +++ b/数据库/old/2.6.8补丁更新.sql @@ -0,0 +1,2 @@ +alter table media_server + add sendRtpPortRange varchar(50) not null; \ No newline at end of file diff --git a/数据库/old/clean.sql b/数据库/old/clean.sql new file mode 100644 index 0000000..b49fa1c --- /dev/null +++ b/数据库/old/clean.sql @@ -0,0 +1,13 @@ +delete from wvp-device; +delete from wvp-device_alarm; +delete from wvp-device_channel; +delete from wvp-device_mobile_position; +delete from wvp-gb_stream; +delete from wvp-log; +delete from wvp-media_server; +delete from wvp-parent_platform; +delete from wvp-platform_catalog; +delete from wvp-platform_gb_channel; +delete from wvp-platform_gb_stream; +delete from wvp-stream_proxy; +delete from wvp-stream_push; \ No newline at end of file